Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs-site/src/content/docs/guides/codex-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,30 @@ 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 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.

`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, 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`,
`GET /v1/models`, `GET /healthz`, and the `/api/*` management surface.
Expand Down
21 changes: 21 additions & 0 deletions src/cli/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1000,6 +1002,23 @@ 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<string, unknown> | undefined,
): string | null {
const openai = providers?.openai;
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)) {
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.";
}

export async function runDoctor(args: string[] = []): Promise<void> {
if (args.includes("--fix-codex-runtime")) {
const resolved = resolveCodexRuntime();
Expand Down Expand Up @@ -1330,6 +1349,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {

// 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,
Expand Down
45 changes: 45 additions & 0 deletions tests/codex-integration/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
collectConfiguredProxy,
collectProxyEnv,
collectRunningProxyEnv,
chatgptPublicEndpointHint,
collectWslDualInstall,
fetchServiceMemory,
formatResponseTempLines,
Expand Down Expand Up @@ -641,6 +642,34 @@ 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 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("assumed");
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();
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();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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");
Expand Down Expand Up @@ -957,4 +986,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 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", authMode: "forward", baseUrl: "https://chatgpt.com/backend-api/codex" } } }),
"utf8",
);

await runDoctor([]);

const output = logged.join("\n");
expect(output).toContain("public ChatGPT endpoint");
expect(output).toContain("assumed");
});
});
Loading