Skip to content

Commit e4a5c1d

Browse files
authored
🤖 fix: strip unsupported truncation from Codex OAuth requests (#3153)
## Summary Strip the unsupported `truncation` field from Codex OAuth Responses requests before rerouting them to ChatGPT's Codex backend. ## Background A recent regression in Mike's `2662aedd8` change (`🤖 fix: align GitHub Copilot model routing (#3104)`) started preserving OpenAI's `truncation` field inside `normalizeCodexResponsesBody()`. A user then hit the backend error: ```json { "detail": "Unsupported parameter: truncation" } ``` The public OpenAI Responses API accepts `truncation`, but the ChatGPT Codex endpoint used by OAuth (`https://chatgpt.com/backend-api/codex/responses`) does not. ## Implementation - remove `truncation` from the Codex-compatible allowlist - explicitly delete `json.truncation` during Codex request normalization - update the regression tests to assert that Codex-normalized bodies strip `truncation` - clarify the fetch-wrapper comments so they describe Codex request normalization instead of defaulting truncation ## Validation - `bun test src/node/services/providerModelFactory.test.ts` - `bun test src/node/services/codexOauthService.test.ts` - `make static-check` ## Risks Low. The change only affects the Codex OAuth request-normalization path and restores the pre-regression behavior for a field that the target endpoint rejects. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$1.79`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=1.79 -->
1 parent 257f440 commit e4a5c1d

2 files changed

Lines changed: 22 additions & 16 deletions

File tree

src/node/services/providerModelFactory.test.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ async function withTempConfig(
3939
}
4040

4141
describe("normalizeCodexResponsesBody", () => {
42-
it("enforces Codex-compatible fields and lifts system prompts into instructions", () => {
42+
it("enforces Codex-compatible fields, strips truncation, and lifts system prompts into instructions", () => {
4343
const normalized = JSON.parse(
4444
normalizeCodexResponsesBody(
4545
JSON.stringify({
@@ -67,19 +67,19 @@ describe("normalizeCodexResponsesBody", () => {
6767
store: boolean;
6868
temperature: number;
6969
text: unknown;
70-
truncation: string;
70+
truncation?: unknown;
7171
};
7272

7373
expect(normalized.store).toBe(false);
74-
expect(normalized.truncation).toBe("disabled");
74+
expect(normalized.truncation).toBeUndefined();
7575
expect(normalized.temperature).toBe(0.2);
7676
expect(normalized.text).toEqual({ format: { type: "json_schema", name: "result" } });
7777
expect(normalized.metadata).toBeUndefined();
7878
expect(normalized.instructions).toBe("Follow project rules.\n\nUse concise updates.");
7979
expect(normalized.input).toEqual([{ role: "user", content: "Ship the fix." }]);
8080
});
8181

82-
it("preserves explicit auto truncation", () => {
82+
it("strips explicit truncation because the Codex endpoint rejects it", () => {
8383
const normalized = JSON.parse(
8484
normalizeCodexResponsesBody(
8585
JSON.stringify({
@@ -88,9 +88,9 @@ describe("normalizeCodexResponsesBody", () => {
8888
truncation: "auto",
8989
})
9090
)
91-
) as { truncation: string; store: boolean };
91+
) as { truncation?: unknown; store: boolean };
9292

93-
expect(normalized.truncation).toBe("auto");
93+
expect(normalized.truncation).toBeUndefined();
9494
expect(normalized.store).toBe(false);
9595
});
9696
});
@@ -399,6 +399,12 @@ describe("ProviderModelFactory GitHub Copilot", () => {
399399
expect(requests).toHaveLength(1);
400400
expect(requests[0]?.input).toBe(CODEX_ENDPOINT);
401401
expect(requests[0]?.init?.body).toBe(normalizeCodexResponsesBody(originalBody));
402+
const normalizedBody = JSON.parse(
403+
(requests[0]?.init?.body as string | undefined) ?? "{}"
404+
) as {
405+
truncation?: unknown;
406+
};
407+
expect(normalizedBody.truncation).toBeUndefined();
402408

403409
const headers = new Headers(requests[0]?.init?.headers);
404410
expect(headers.get("authorization")).toBe("Bearer test-access-token");

src/node/services/providerModelFactory.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,6 @@ const CODEX_ALLOWED_PARAMS = new Set([
662662
"top_p",
663663
"include",
664664
"text", // structured output via Output.object -> text.format
665-
"truncation",
666665
]);
667666

668667
// ---------------------------------------------------------------------------
@@ -693,10 +692,10 @@ function extractTextContent(content: unknown): string {
693692

694693
export function normalizeCodexResponsesBody(body: string): string {
695694
const json = JSON.parse(body) as Record<string, unknown>;
696-
const truncation = json.truncation;
697-
if (truncation !== "auto" && truncation !== "disabled") {
698-
json.truncation = "disabled";
699-
}
695+
696+
// ChatGPT's Codex endpoint is stricter than the public OpenAI Responses API
697+
// and currently rejects the `truncation` field entirely.
698+
delete json.truncation;
700699

701700
// Codex-compatible Responses requests must disable storage and strip unsupported params.
702701
json.store = false;
@@ -1185,9 +1184,9 @@ export class ProviderModelFactory {
11851184
const baseFetch = getProviderFetch(providerConfig);
11861185
const codexOauthService = this.codexOauthService;
11871186

1188-
// Wrap fetch to default truncation to "disabled" for OpenAI Responses API calls.
1189-
// This preserves our compaction handling while still allowing explicit truncation (e.g., auto).
1190-
const fetchWithOpenAITruncation = Object.assign(
1187+
// Wrap fetch so Codex OAuth Responses requests are normalized before
1188+
// they are rerouted from api.openai.com to chatgpt.com's Codex backend.
1189+
const fetchWithOpenAICodexNormalization = Object.assign(
11911190
async (
11921191
input: Parameters<typeof fetch>[0],
11931192
init?: Parameters<typeof fetch>[1]
@@ -1218,7 +1217,8 @@ export class ProviderModelFactory {
12181217

12191218
const body = init?.body;
12201219
// Only parse the JSON body when routing through Codex OAuth, since Codex
1221-
// requires instruction lifting, store=false, and Responses truncation.
1220+
// requires instruction lifting, store=false, and stripping unsupported
1221+
// Responses fields like `truncation`.
12221222
if (
12231223
shouldRouteThroughCodexOauth &&
12241224
isOpenAIResponses &&
@@ -1281,7 +1281,7 @@ export class ProviderModelFactory {
12811281

12821282
// Lazy-load OpenAI provider to reduce startup time
12831283
const { createOpenAI } = await PROVIDER_REGISTRY.openai();
1284-
const providerFetch = fetchWithOpenAITruncation as typeof fetch;
1284+
const providerFetch = fetchWithOpenAICodexNormalization as typeof fetch;
12851285
const provider = createOpenAI({
12861286
...configWithCreds,
12871287
// Cast is safe: our fetch implementation is compatible with the SDK's fetch type.

0 commit comments

Comments
 (0)