From 166309cbe6d53629db3ab1f671257d8ab4275027 Mon Sep 17 00:00:00 2001 From: chilung Date: Tue, 8 Sep 2026 20:14:09 +0000 Subject: [PATCH 01/10] feat(doctor): explain ChatGPT public endpoint latency Document the public ChatGPT channel used by proxy pool routing and report the same read-only guidance from ocx doctor without hard-coding historical latency measurements or changing service-tier confirmation semantics. Closes #2455 --- .../src/content/docs/guides/codex-integration.md | 12 ++++++++++++ src/cli/doctor.ts | 13 +++++++++++++ tests/codex-integration/doctor.test.ts | 10 ++++++++++ 3 files changed, 35 insertions(+) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 5b75a21b41..483bce8eff 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -76,6 +76,18 @@ adding a `[features]` table. Fast mode is separate from voice transport. A supported model's service-tier speed description does not guarantee lower microphone, WebRTC, or end-to-end voice latency through OpenCodex. +### ChatGPT-family channel and latency + +Native ChatGPT-family requests routed through opencodex use the public ChatGPT endpoint. The +native Codex app channel is not available through the proxy pool, so the upstream may spend time +queueing a request before the first output even when the local proxy and network path are healthy. +This channel difference can make the same request feel slower than a DeepSeek or Kimi provider. + +`service_tier: priority` is a request preference. It does not prove that the upstream granted that +tier. Check the response tier shown in request logs when you need to distinguish the requested +preference from the backend's decision. For latency-sensitive work, choose a provider with a +shorter observed queue or run Codex natively when the app channel is required. + The proxy listens on port `10100` by default and serves `POST /v1/responses`, `POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`, `GET /v1/models`, `GET /healthz`, and the `/api/*` management surface. diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index d7148530a0..98fce8840d 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -1000,6 +1000,17 @@ export function proxyDownRestartHint(input: { return `The ocx proxy is not running. ${uncleanExit}Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`; } +/** Explain the expected channel and latency trade-off for native ChatGPT routing. */ +export function chatgptPublicEndpointHint( + providers: Record | undefined, +): string | null { + const openai = providers?.openai; + if (!openai || typeof openai !== "object" || (openai as { adapter?: unknown }).adapter !== "openai-responses") { + return null; + } + return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, so upstream queue delay before the first output can be higher than DeepSeek/Kimi. The native Codex app channel is unavailable through the proxy pool; use a latency-sensitive provider or run Codex natively when that channel matters. service_tier=priority is a request preference; inspect response tier in logs to see what the backend granted."; +} + export async function runDoctor(args: string[] = []): Promise { if (args.includes("--fix-codex-runtime")) { const resolved = resolveCodexRuntime(); @@ -1330,6 +1341,8 @@ export async function runDoctor(args: string[] = []): Promise { // Hints, not fixes. const hints: string[] = []; + const chatgptHint = chatgptPublicEndpointHint(doctorConfig.providers); + if (chatgptHint) hints.push(chatgptHint); const proxyDown = proxyDownRestartHint({ proxyRunning: Boolean(live), port: live?.port ?? doctorConfig.port ?? 10100, diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index acb0f1b87e..66bfb9a721 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -12,6 +12,7 @@ import { collectConfiguredProxy, collectProxyEnv, collectRunningProxyEnv, + chatgptPublicEndpointHint, collectWslDualInstall, fetchServiceMemory, formatResponseTempLines, @@ -641,6 +642,15 @@ describe("service memory section (#314 WP4)", () => { expect(hint).toContain("ocx service install"); }); + test("ChatGPT public endpoint hint explains channel latency without claiming a fixed delay", () => { + const hint = chatgptPublicEndpointHint({ openai: { adapter: "openai-responses" } }); + expect(hint).toContain("public ChatGPT endpoint"); + expect(hint).toContain("native Codex app channel"); + expect(hint).toContain("DeepSeek/Kimi"); + expect(hint).not.toContain("11s"); + expect(chatgptPublicEndpointHint({})).toBeNull(); + }); + test("proxyDownRestartHint prefers 'ocx service start' when a service is installed", () => { const hint = proxyDownRestartHint({ proxyRunning: false, port: 12000, serviceViable: true }); expect(hint).toContain("ocx service start"); From 7bd742948167e5e7f1aa6f9db8118ffd39e52817 Mon Sep 17 00:00:00 2001 From: chilung Date: Tue, 8 Sep 2026 20:23:33 +0000 Subject: [PATCH 02/10] docs(codex-integration): clarify ChatGPT channel scope and verify doctor hint wiring --- .../src/content/docs/guides/codex-integration.md | 12 ++++++++---- tests/codex-integration/doctor.test.ts | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 483bce8eff..3595f7e628 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -78,10 +78,14 @@ does not guarantee lower microphone, WebRTC, or end-to-end voice latency through ### ChatGPT-family channel and latency -Native ChatGPT-family requests routed through opencodex use the public ChatGPT endpoint. The -native Codex app channel is not available through the proxy pool, so the upstream may spend time -queueing a request before the first output even when the local proxy and network path are healthy. -This channel difference can make the same request feel slower than a DeepSeek or Kimi provider. +Native ChatGPT-family requests routed through opencodex via the canonical ChatGPT-login `openai` +forward provider (covering both Pool and Direct modes) use the public ChatGPT endpoint. The +native Codex app channel is not available through the proxy pool, and provider routing or account +selection does not bypass the upstream ChatGPT channel. The upstream may spend time queueing a +request before the first output even when the local proxy and network path are healthy. This +behavior is specific to ChatGPT-login routing and does not apply to `openai-apikey` or custom +providers, which connect directly to their respective API endpoints without public ChatGPT channel +queueing. `service_tier: priority` is a request preference. It does not prove that the upstream granted that tier. Check the response tier shown in request logs when you need to distinguish the requested diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index 66bfb9a721..9dc37fd6ff 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -967,4 +967,20 @@ describe("doctor reports an unclean prior proxy exit", () => { expect(logged.join("\n")).not.toContain("may have exited unexpectedly"); }); + + test("runDoctor outputs ChatGPT public endpoint hint when openai adapter is configured", async () => { + const { writeFileSync } = await import("fs"); + const { join } = await import("path"); + writeFileSync( + join(tempHome, "config.json"), + JSON.stringify({ port: 9, codexAutoStart: false, providers: { openai: { adapter: "openai-responses" } } }), + "utf8", + ); + + await runDoctor([]); + + const output = logged.join("\n"); + expect(output).toContain("public ChatGPT endpoint"); + expect(output).toContain("native Codex app channel"); + }); }); From b8853a4a8d4bdfff9ea37494182e507b2d84fc47 Mon Sep 17 00:00:00 2001 From: chilung Date: Tue, 8 Sep 2026 21:33:27 +0000 Subject: [PATCH 03/10] fix(doctor): restrict ChatGPT hint to canonical forward auth and clarify mode parity --- .../src/content/docs/guides/codex-integration.md | 2 +- src/cli/doctor.ts | 16 ++++++++++++++-- tests/codex-integration/doctor.test.ts | 3 +++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 3595f7e628..00f7b65941 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -80,7 +80,7 @@ does not guarantee lower microphone, WebRTC, or end-to-end voice latency through Native ChatGPT-family requests routed through opencodex via the canonical ChatGPT-login `openai` forward provider (covering both Pool and Direct modes) use the public ChatGPT endpoint. The -native Codex app channel is not available through the proxy pool, and provider routing or account +native Codex app channel is not available through OpenCodex routing in either Pool or Direct mode, and provider routing or account selection does not bypass the upstream ChatGPT channel. The upstream may spend time queueing a request before the first output even when the local proxy and network path are healthy. This behavior is specific to ChatGPT-login routing and does not apply to `openai-apikey` or custom diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 98fce8840d..6e87e8e26c 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -1005,10 +1005,22 @@ export function chatgptPublicEndpointHint( providers: Record | undefined, ): string | null { const openai = providers?.openai; - if (!openai || typeof openai !== "object" || (openai as { adapter?: unknown }).adapter !== "openai-responses") { + if (!openai || typeof openai !== "object") { return null; } - return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, so upstream queue delay before the first output can be higher than DeepSeek/Kimi. The native Codex app channel is unavailable through the proxy pool; use a latency-sensitive provider or run Codex natively when that channel matters. service_tier=priority is a request preference; inspect response tier in logs to see what the backend granted."; + const typed = openai as { adapter?: unknown; authMode?: unknown; baseUrl?: unknown }; + if (typed.adapter !== "openai-responses") { + return null; + } + const authMode = typed.authMode ?? "forward"; + if (authMode !== "forward") { + return null; + } + const baseUrl = typeof typed.baseUrl === "string" ? typed.baseUrl : ""; + if (baseUrl && !baseUrl.includes("chatgpt.com")) { + return null; + } + return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, so upstream queue delay before the first output can be higher than DeepSeek/Kimi. The native Codex app channel is unavailable through OpenCodex routing (in both Pool and Direct modes); use a latency-sensitive provider or run Codex natively when that channel matters. service_tier=priority is a request preference; inspect response tier in logs to see what the backend granted."; } export async function runDoctor(args: string[] = []): Promise { diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index 9dc37fd6ff..3aaed6f358 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -647,8 +647,11 @@ describe("service memory section (#314 WP4)", () => { expect(hint).toContain("public ChatGPT endpoint"); expect(hint).toContain("native Codex app channel"); expect(hint).toContain("DeepSeek/Kimi"); + expect(hint).toContain("both Pool and Direct modes"); expect(hint).not.toContain("11s"); expect(chatgptPublicEndpointHint({})).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "key" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://api.openai.com/v1" } })).toBeNull(); }); test("proxyDownRestartHint prefers 'ocx service start' when a service is installed", () => { From 22420a76e395334c337419f6f34ea6f99ffe9d55 Mon Sep 17 00:00:00 2001 From: chilung Date: Wed, 9 Sep 2026 00:28:04 +0000 Subject: [PATCH 04/10] fix(doctor): validate baseUrl hostname for ChatGPT public endpoint hint --- src/cli/doctor.ts | 12 ++++++++++-- tests/codex-integration/doctor.test.ts | 5 +++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 6e87e8e26c..e5eda3afcc 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -1017,8 +1017,16 @@ export function chatgptPublicEndpointHint( return null; } const baseUrl = typeof typed.baseUrl === "string" ? typed.baseUrl : ""; - if (baseUrl && !baseUrl.includes("chatgpt.com")) { - return null; + if (baseUrl) { + let hostname: string; + try { + hostname = new URL(baseUrl).hostname.toLowerCase(); + } catch { + return null; + } + if (hostname !== "chatgpt.com" && !hostname.endsWith(".chatgpt.com")) { + return null; + } } return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, so upstream queue delay before the first output can be higher than DeepSeek/Kimi. The native Codex app channel is unavailable through OpenCodex routing (in both Pool and Direct modes); use a latency-sensitive provider or run Codex natively when that channel matters. service_tier=priority is a request preference; inspect response tier in logs to see what the backend granted."; } diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index 3aaed6f358..f6c1a0baa2 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -652,6 +652,11 @@ describe("service memory section (#314 WP4)", () => { expect(chatgptPublicEndpointHint({})).toBeNull(); expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "key" } })).toBeNull(); expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://api.openai.com/v1" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com.example/v1" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://gateway.example/chatgpt.com/v1" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "not-a-valid-url" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api" } })).not.toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://subdomain.chatgpt.com/v1" } })).not.toBeNull(); }); test("proxyDownRestartHint prefers 'ocx service start' when a service is installed", () => { From 11d6617fd86bd6e43bbf9dac6979612c5f743ffe Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Sep 2026 15:25:36 +0900 Subject: [PATCH 05/10] fix(doctor): gate ChatGPT hint on canonical forward provider, correct tier and transport wording --- .../content/docs/guides/codex-integration.md | 26 ++++++++------- src/cli/doctor.ts | 26 ++++----------- tests/codex-integration/doctor.test.ts | 32 +++++++++++-------- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 00f7b65941..894c4416d4 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -79,18 +79,20 @@ does not guarantee lower microphone, WebRTC, or end-to-end voice latency through ### ChatGPT-family channel and latency Native ChatGPT-family requests routed through opencodex via the canonical ChatGPT-login `openai` -forward provider (covering both Pool and Direct modes) use the public ChatGPT endpoint. The -native Codex app channel is not available through OpenCodex routing in either Pool or Direct mode, and provider routing or account -selection does not bypass the upstream ChatGPT channel. The upstream may spend time queueing a -request before the first output even when the local proxy and network path are healthy. This -behavior is specific to ChatGPT-login routing and does not apply to `openai-apikey` or custom -providers, which connect directly to their respective API endpoints without public ChatGPT channel -queueing. - -`service_tier: priority` is a request preference. It does not prove that the upstream granted that -tier. Check the response tier shown in request logs when you need to distinguish the requested -preference from the backend's decision. For latency-sensitive work, choose a provider with a -shorter observed queue or run Codex natively when the app channel is required. +forward provider (covering both Pool and Direct modes) use the public ChatGPT endpoint. Provider +routing or account selection does not bypass the upstream ChatGPT channel. The upstream may spend +time queueing a request before the first output even when the local proxy and network path are +healthy. Streaming turns already ride the ChatGPT websocket transport — the same +`responses_websockets` lane Codex CLI defaults to — so the remaining gap is the public-endpoint +queue itself, not the transport. This behavior is specific to ChatGPT-login routing and does not +apply to `openai-apikey` or custom providers, which connect directly to their respective API +endpoints without public ChatGPT channel queueing. + +`service_tier: priority` is a request preference. On the ChatGPT backend the echoed +`service_tier` cannot confirm or deny the granted tier: turns scheduled as priority can still +echo `default`, so request logs show the response tier as an observation with confirmation +`assumed`. For latency-sensitive work, choose a provider with a shorter observed queue or run +Codex natively when the native app channel is required. The proxy listens on port `10100` by default and serves `POST /v1/responses`, `POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`, diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index e5eda3afcc..1b435a926d 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -45,6 +45,8 @@ import { probeCodexCoordinatorNamespace, resolveEffectiveUserIdentity, } from "../codex/user-identity"; +import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination"; +import type { OcxProviderConfig } from "../types/provider"; import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings"; import { collectLegacyCodexConfigKeyDiagnostics, @@ -1008,27 +1010,13 @@ export function chatgptPublicEndpointHint( if (!openai || typeof openai !== "object") { return null; } - const typed = openai as { adapter?: unknown; authMode?: unknown; baseUrl?: unknown }; - if (typed.adapter !== "openai-responses") { + // Same classification the router uses: adapter + forward auth + the exact + // canonical ChatGPT-login URL. A hostname lookalike must not get this + // guidance, and a missing authMode is the runtime "key" default, not forward. + if (!isCanonicalOpenAiForwardProvider(openai as OcxProviderConfig)) { return null; } - const authMode = typed.authMode ?? "forward"; - if (authMode !== "forward") { - return null; - } - const baseUrl = typeof typed.baseUrl === "string" ? typed.baseUrl : ""; - if (baseUrl) { - let hostname: string; - try { - hostname = new URL(baseUrl).hostname.toLowerCase(); - } catch { - return null; - } - if (hostname !== "chatgpt.com" && !hostname.endsWith(".chatgpt.com")) { - return null; - } - } - return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, so upstream queue delay before the first output can be higher than DeepSeek/Kimi. The native Codex app channel is unavailable through OpenCodex routing (in both Pool and Direct modes); use a latency-sensitive provider or run Codex natively when that channel matters. service_tier=priority is a request preference; inspect response tier in logs to see what the backend granted."; + return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, so upstream queue delay before the first output can be higher than providers without that public queue. Streaming turns already ride the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to); the remaining gap is the public-endpoint queue itself, in both Pool and Direct modes. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier. For latency-sensitive work, choose a provider with a shorter observed queue or run Codex natively."; } export async function runDoctor(args: string[] = []): Promise { diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index f6c1a0baa2..e91a8bbb30 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -643,20 +643,26 @@ describe("service memory section (#314 WP4)", () => { }); test("ChatGPT public endpoint hint explains channel latency without claiming a fixed delay", () => { - const hint = chatgptPublicEndpointHint({ openai: { adapter: "openai-responses" } }); + const canonical = { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex" }; + const hint = chatgptPublicEndpointHint({ openai: canonical }); expect(hint).toContain("public ChatGPT endpoint"); - expect(hint).toContain("native Codex app channel"); - expect(hint).toContain("DeepSeek/Kimi"); + expect(hint).toContain("assumed"); + expect(hint).toContain("websocket"); expect(hint).toContain("both Pool and Direct modes"); expect(hint).not.toContain("11s"); expect(chatgptPublicEndpointHint({})).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "key" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://api.openai.com/v1" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com.example/v1" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://gateway.example/chatgpt.com/v1" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "not-a-valid-url" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api" } })).not.toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://subdomain.chatgpt.com/v1" } })).not.toBeNull(); + // A missing authMode is the runtime "key" default, not the forward login. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "key", baseUrl: "https://chatgpt.com/backend-api/codex" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://api.openai.com/v1" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com.example/v1" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://gateway.example/chatgpt.com/v1" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "not-a-valid-url" } })).toBeNull(); + // Only the exact canonical URL qualifies: no parent path, no subdomain. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api" } })).toBeNull(); + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://subdomain.chatgpt.com/v1" } })).toBeNull(); + // Trailing slashes still normalize to the canonical URL. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex/" } })).not.toBeNull(); }); test("proxyDownRestartHint prefers 'ocx service start' when a service is installed", () => { @@ -976,12 +982,12 @@ describe("doctor reports an unclean prior proxy exit", () => { expect(logged.join("\n")).not.toContain("may have exited unexpectedly"); }); - test("runDoctor outputs ChatGPT public endpoint hint when openai adapter is configured", async () => { + test("runDoctor outputs ChatGPT public endpoint hint when the canonical openai provider is configured", async () => { const { writeFileSync } = await import("fs"); const { join } = await import("path"); writeFileSync( join(tempHome, "config.json"), - JSON.stringify({ port: 9, codexAutoStart: false, providers: { openai: { adapter: "openai-responses" } } }), + JSON.stringify({ port: 9, codexAutoStart: false, providers: { openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex" } } }), "utf8", ); @@ -989,6 +995,6 @@ describe("doctor reports an unclean prior proxy exit", () => { const output = logged.join("\n"); expect(output).toContain("public ChatGPT endpoint"); - expect(output).toContain("native Codex app channel"); + expect(output).toContain("assumed"); }); }); From 3bd66da64dbfda7983f8a937bebd02add433476e Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Sep 2026 16:12:14 +0900 Subject: [PATCH 06/10] fix(doctor): hedge ChatGPT latency copy to what the classifier can support --- .../content/docs/guides/codex-integration.md | 20 ++++++++++++------- src/cli/doctor.ts | 2 +- tests/codex-integration/doctor.test.ts | 5 +++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 894c4416d4..d4b6329025 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -82,17 +82,23 @@ Native ChatGPT-family requests routed through opencodex via the canonical ChatGP forward provider (covering both Pool and Direct modes) use the public ChatGPT endpoint. Provider routing or account selection does not bypass the upstream ChatGPT channel. The upstream may spend time queueing a request before the first output even when the local proxy and network path are -healthy. Streaming turns already ride the ChatGPT websocket transport — the same -`responses_websockets` lane Codex CLI defaults to — so the remaining gap is the public-endpoint -queue itself, not the transport. This behavior is specific to ChatGPT-login routing and does not -apply to `openai-apikey` or custom providers, which connect directly to their respective API -endpoints without public ChatGPT channel queueing. +healthy. + +Eligible streaming turns dial the ChatGPT websocket transport — the same `responses_websockets` +lane Codex CLI defaults to — and fall back to SSE over HTTP when a turn is not eligible: an +unsupported Bun runtime, an oversized `response.create` frame, or a proxy route that cannot carry +the socket. Local provider pacing can also hold a request before it is dispatched at all. So a slow +first output has several possible contributors, and upstream queueing is only one of them. `ocx +doctor` classifies configuration and measures none of these: compare actual transport, pacing, +network, and provider observations before concluding. This routing behavior is specific to +ChatGPT-login routing and does not apply to `openai-apikey` or custom providers, which connect +directly to their respective API endpoints without public ChatGPT channel queueing. `service_tier: priority` is a request preference. On the ChatGPT backend the echoed `service_tier` cannot confirm or deny the granted tier: turns scheduled as priority can still echo `default`, so request logs show the response tier as an observation with confirmation -`assumed`. For latency-sensitive work, choose a provider with a shorter observed queue or run -Codex natively when the native app channel is required. +`assumed`. For latency-sensitive work, compare observed first-output times across the providers you +actually use rather than assuming any particular channel is faster. The proxy listens on port `10100` by default and serves `POST /v1/responses`, `POST /v1/responses/compact`, `POST /v1/images/generations`, `POST /v1/images/edits`, diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 1b435a926d..91be20d8d5 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -1016,7 +1016,7 @@ export function chatgptPublicEndpointHint( if (!isCanonicalOpenAiForwardProvider(openai as OcxProviderConfig)) { return null; } - return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, so upstream queue delay before the first output can be higher than providers without that public queue. Streaming turns already ride the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to); the remaining gap is the public-endpoint queue itself, in both Pool and Direct modes. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier. For latency-sensitive work, choose a provider with a shorter observed queue or run Codex natively."; + return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, in both Pool and Direct modes. Eligible streaming turns dial the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to) and fall back to SSE over HTTP when a turn is not eligible - an unsupported Bun runtime, an oversized create frame, or a proxy route that cannot carry the socket - and local provider pacing can hold a request before it is dispatched at all. This hint classifies configuration only and measures nothing, so upstream queueing is one possible contributor to a slow first output: compare actual transport, pacing, network, and provider observations before concluding. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier."; } export async function runDoctor(args: string[] = []): Promise { diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index e91a8bbb30..07ec5991ba 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -650,6 +650,11 @@ describe("service memory section (#314 WP4)", () => { expect(hint).toContain("websocket"); expect(hint).toContain("both Pool and Direct modes"); expect(hint).not.toContain("11s"); + // The helper classifies configuration; it measures no latency. The copy has + // to stay hedged because eligible turns can still fall back to SSE and + // local pacing can delay dispatch before any upstream work starts. + expect(hint).toContain("fall back"); + expect(hint).toContain("one possible contributor"); expect(chatgptPublicEndpointHint({})).toBeNull(); // A missing authMode is the runtime "key" default, not the forward login. expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex" } })).toBeNull(); From 50ad78f639fc29bdb026395441aca10cbbe39ecb Mon Sep 17 00:00:00 2001 From: t Date: Wed, 9 Sep 2026 16:17:51 +0900 Subject: [PATCH 07/10] chore(carry): record attribution for the #4063 carry Carries pull request #4063 unchanged, so the contribution keeps its author in the contributor graph after the squash merge. Carried-from: https://github.com/lidge-jun/opencodex/pull/4063 Carried-from-commit: 3bd66da64dbfda7983f8a937bebd02add433476e Co-authored-by: chilung From 5748cdf340bd87f6176c1605e33827c5d68807b4 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 9 Sep 2026 19:00:33 +0900 Subject: [PATCH 08/10] fix(doctor): classify the resolved openai destination and complete the transport docs --- .../content/docs/guides/codex-integration.md | 34 +++++++++++-------- src/cli/doctor.ts | 24 ++++++++++--- tests/codex-integration/doctor.test.ts | 26 ++++++++------ 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index d4b6329025..5902822a3c 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -78,21 +78,25 @@ does not guarantee lower microphone, WebRTC, or end-to-end voice latency through ### ChatGPT-family channel and latency -Native ChatGPT-family requests routed through opencodex via the canonical ChatGPT-login `openai` -forward provider (covering both Pool and Direct modes) use the public ChatGPT endpoint. Provider -routing or account selection does not bypass the upstream ChatGPT channel. The upstream may spend -time queueing a request before the first output even when the local proxy and network path are -healthy. - -Eligible streaming turns dial the ChatGPT websocket transport — the same `responses_websockets` -lane Codex CLI defaults to — and fall back to SSE over HTTP when a turn is not eligible: an -unsupported Bun runtime, an oversized `response.create` frame, or a proxy route that cannot carry -the socket. Local provider pacing can also hold a request before it is dispatched at all. So a slow -first output has several possible contributors, and upstream queueing is only one of them. `ocx -doctor` classifies configuration and measures none of these: compare actual transport, pacing, -network, and provider observations before concluding. This routing behavior is specific to -ChatGPT-login routing and does not apply to `openai-apikey` or custom providers, which connect -directly to their respective API endpoints without public ChatGPT channel queueing. +Requests routed through opencodex via the canonical ChatGPT-login `openai` provider — adapter +`openai-responses`, `authMode: "forward"`, and the `https://chatgpt.com/backend-api/codex` +endpoint, covering both Pool and Direct modes — use the public ChatGPT endpoint. Provider routing +or account selection does not bypass the upstream ChatGPT channel. The upstream may spend time +queueing a request before the first output even when the local proxy and network path are healthy. + +Only some turns take the ChatGPT websocket transport — the same `responses_websockets` lane Codex +CLI defaults to. A turn is eligible when the Bun runtime supports the bounded relay, the request +is a `POST` to the canonical Responses URL or a configured WebSocket route, and its JSON body sets +`stream` to `true` at the root. Everything else stays on SSE over HTTP, and an eligible turn still +falls back to it when the request cannot be prepared, the `response.create` frame exceeds its size +limit, or the proxy route cannot carry the socket. + +Local provider pacing can also hold a request before it is dispatched at all. So a slow first +output has several possible contributors, and upstream queueing is only one of them. `ocx doctor` +classifies configuration and measures none of these: compare actual transport, pacing, network, +and provider observations before concluding. This routing behavior is specific to ChatGPT-login +forwarding and does not apply to `openai-apikey` or custom providers, which connect directly to +their respective API endpoints without public ChatGPT channel queueing. `service_tier: priority` is a request preference. On the ChatGPT backend the echoed `service_tier` cannot confirm or deny the granted tier: turns scheduled as priority can still diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index 91be20d8d5..f35edb7c95 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -47,6 +47,7 @@ import { } from "../codex/user-identity"; import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination"; import type { OcxProviderConfig } from "../types/provider"; +import { routedProviderConfig } from "../router"; import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings"; import { collectLegacyCodexConfigKeyDiagnostics, @@ -1010,10 +1011,25 @@ export function chatgptPublicEndpointHint( if (!openai || typeof openai !== "object") { return null; } - // Same classification the router uses: adapter + forward auth + the exact - // canonical ChatGPT-login URL. A hostname lookalike must not get this - // guidance, and a missing authMode is the runtime "key" default, not forward. - if (!isCanonicalOpenAiForwardProvider(openai as OcxProviderConfig)) { + // A disabled row never routes, so it must not be described as the route in use. + const configured = openai as OcxProviderConfig; + if (configured.disabled === true) { + return null; + } + // Classify the destination the router resolves, not the raw config text. Two things follow + // from the registry entry for the built-in `openai` id: a row that omits `authMode` still + // forwards, and a row carrying some other `baseUrl` has it discarded in favour of the + // canonical ChatGPT endpoint. Both keep using the public endpoint, so both want this hint; + // reading the raw row would have suppressed the first and misjudged the second. Resolution + // can reject a malformed or disallowed destination by throwing, and doctor is read-only + // diagnostics, so an unclassifiable row simply gets no hint. + let routed: OcxProviderConfig; + try { + routed = routedProviderConfig("openai", configured); + } catch { + return null; + } + if (!isCanonicalOpenAiForwardProvider(routed)) { return null; } return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, in both Pool and Direct modes. Eligible streaming turns dial the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to) and fall back to SSE over HTTP when a turn is not eligible - an unsupported Bun runtime, an oversized create frame, or a proxy route that cannot carry the socket - and local provider pacing can hold a request before it is dispatched at all. This hint classifies configuration only and measures nothing, so upstream queueing is one possible contributor to a slow first output: compare actual transport, pacing, network, and provider observations before concluding. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier."; diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index 07ec5991ba..82ac0c481d 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -656,18 +656,24 @@ describe("service memory section (#314 WP4)", () => { expect(hint).toContain("fall back"); expect(hint).toContain("one possible contributor"); expect(chatgptPublicEndpointHint({})).toBeNull(); - // A missing authMode is the runtime "key" default, not the forward login. - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "key", baseUrl: "https://chatgpt.com/backend-api/codex" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://api.openai.com/v1" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com.example/v1" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://gateway.example/chatgpt.com/v1" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "not-a-valid-url" } })).toBeNull(); - // Only the exact canonical URL qualifies: no parent path, no subdomain. - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api" } })).toBeNull(); - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://subdomain.chatgpt.com/v1" } })).toBeNull(); + // Resolution, not raw text. The registry entry for the built-in `openai` id has + // authKind "forward", so a row that omits `authMode` still forwards to ChatGPT and still + // needs the hint. Reading the raw row suppressed it. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex" } })).not.toBeNull(); + // Same reason the other way round: the entry is not key-auth-overridable, so writing + // `authMode: "key"` on this id does not change where requests go. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "key", baseUrl: "https://chatgpt.com/backend-api/codex" } })).not.toBeNull(); + // The entry sets no baseUrl override, so a differing URL is discarded and the request + // still goes to the canonical endpoint. Describing that route is correct, and a lookalike + // host never becomes the destination. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com.example/v1" } })).not.toBeNull(); // Trailing slashes still normalize to the canonical URL. expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex/" } })).not.toBeNull(); + // A disabled row never routes, so it is not the route in use. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", disabled: true } })).toBeNull(); + // A destination that cannot be resolved at all yields no hint rather than throwing out + // of read-only diagnostics. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: " " } })).toBeNull(); }); test("proxyDownRestartHint prefers 'ocx service start' when a service is installed", () => { From 0000689046d6ed85271c96d8a30e07fc2e437e12 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 9 Sep 2026 19:14:54 +0900 Subject: [PATCH 09/10] test(doctor): a blank baseUrl is discarded, not unresolvable --- src/cli/doctor.ts | 9 ++++++--- tests/codex-integration/doctor.test.ts | 8 +++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index f35edb7c95..a769252620 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -1020,9 +1020,12 @@ export function chatgptPublicEndpointHint( // from the registry entry for the built-in `openai` id: a row that omits `authMode` still // forwards, and a row carrying some other `baseUrl` has it discarded in favour of the // canonical ChatGPT endpoint. Both keep using the public endpoint, so both want this hint; - // reading the raw row would have suppressed the first and misjudged the second. Resolution - // can reject a malformed or disallowed destination by throwing, and doctor is read-only - // diagnostics, so an unclassifiable row simply gets no hint. + // reading the raw row would have suppressed the first and misjudged the second. + // + // `routedProviderConfig` throws for an unresolved URL only when the registry entry allows a + // baseUrl override, which this entry does not, so no input reaches that path today. The guard + // is here because doctor is read-only diagnostics: a later registry change must not turn a + // diagnostic into a crash. let routed: OcxProviderConfig; try { routed = routedProviderConfig("openai", configured); diff --git a/tests/codex-integration/doctor.test.ts b/tests/codex-integration/doctor.test.ts index 82ac0c481d..3f52ea4e45 100644 --- a/tests/codex-integration/doctor.test.ts +++ b/tests/codex-integration/doctor.test.ts @@ -671,9 +671,11 @@ describe("service memory section (#314 WP4)", () => { expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex/" } })).not.toBeNull(); // A disabled row never routes, so it is not the route in use. expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex", disabled: true } })).toBeNull(); - // A destination that cannot be resolved at all yields no hint rather than throwing out - // of read-only diagnostics. - expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: " " } })).toBeNull(); + // A blank baseUrl is discarded like any other override on this id, so it resolves to the + // canonical endpoint and still gets the hint. Resolution has no reachable throw here: + // src/router.ts only rejects an unresolved URL when the registry entry allows a baseUrl + // override, and the `openai` entry does not. + expect(chatgptPublicEndpointHint({ openai: { adapter: "openai-responses", authMode: "forward", baseUrl: " " } })).not.toBeNull(); }); test("proxyDownRestartHint prefers 'ocx service start' when a service is installed", () => { From ab031629a63fc7a7402e5be3dfad15a2b9264b69 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 9 Sep 2026 20:01:58 +0900 Subject: [PATCH 10/10] docs(codex): scope the ChatGPT channel claim to the resolved destination The paragraph excluded custom providers by name, but isCanonicalOpenAiForwardProvider classifies on adapter, auth mode and destination, so a custom-named forward entry pointing at the ChatGPT backend takes the same public channel. It also separates the doctor hint, which inspects only the built-in openai row, from the endpoint behaviour it describes. --- .../src/content/docs/guides/codex-integration.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 5902822a3c..19bd5e4a1a 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -94,9 +94,17 @@ limit, or the proxy route cannot carry the socket. Local provider pacing can also hold a request before it is dispatched at all. So a slow first output has several possible contributors, and upstream queueing is only one of them. `ocx doctor` classifies configuration and measures none of these: compare actual transport, pacing, network, -and provider observations before concluding. This routing behavior is specific to ChatGPT-login -forwarding and does not apply to `openai-apikey` or custom providers, which connect directly to -their respective API endpoints without public ChatGPT channel queueing. +and provider observations before concluding. + +What decides whether a request takes that public channel is the destination it resolves to, not +the name of the provider entry. A provider that resolves somewhere else — `openai-apikey`, or a +custom entry pointing at its own API — reaches that endpoint directly and sees no ChatGPT queueing. +A custom-named entry that resolves to `https://chatgpt.com/backend-api/codex` with forward auth +takes the same public channel as the built-in row, because the classification reads the adapter, +auth mode and destination rather than the entry's name. + +The `ocx doctor` hint is narrower than the endpoint behavior it describes: it inspects only the +built-in `openai` row, so its absence tells you nothing about where any other provider resolves. `service_tier: priority` is a request preference. On the ChatGPT backend the echoed `service_tier` cannot confirm or deny the granted tier: turns scheduled as priority can still