Skip to content
Open
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
18 changes: 12 additions & 6 deletions src/node/services/providerModelFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async function withTempConfig(
}

describe("normalizeCodexResponsesBody", () => {
it("enforces Codex-compatible fields and lifts system prompts into instructions", () => {
it("enforces Codex-compatible fields, strips truncation, and lifts system prompts into instructions", () => {
const normalized = JSON.parse(
normalizeCodexResponsesBody(
JSON.stringify({
Expand Down Expand Up @@ -65,19 +65,19 @@ describe("normalizeCodexResponsesBody", () => {
store: boolean;
temperature: number;
text: unknown;
truncation: string;
truncation?: unknown;
};

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

it("preserves explicit auto truncation", () => {
it("strips explicit truncation because the Codex endpoint rejects it", () => {
const normalized = JSON.parse(
normalizeCodexResponsesBody(
JSON.stringify({
Expand All @@ -86,9 +86,9 @@ describe("normalizeCodexResponsesBody", () => {
truncation: "auto",
})
)
) as { truncation: string; store: boolean };
) as { truncation?: unknown; store: boolean };

expect(normalized.truncation).toBe("auto");
expect(normalized.truncation).toBeUndefined();
expect(normalized.store).toBe(false);
});
});
Expand Down Expand Up @@ -397,6 +397,12 @@ describe("ProviderModelFactory GitHub Copilot", () => {
expect(requests).toHaveLength(1);
expect(requests[0]?.input).toBe(CODEX_ENDPOINT);
expect(requests[0]?.init?.body).toBe(normalizeCodexResponsesBody(originalBody));
const normalizedBody = JSON.parse(
(requests[0]?.init?.body as string | undefined) ?? "{}"
) as {
truncation?: unknown;
};
expect(normalizedBody.truncation).toBeUndefined();

const headers = new Headers(requests[0]?.init?.headers);
expect(headers.get("authorization")).toBe("Bearer test-access-token");
Expand Down
20 changes: 10 additions & 10 deletions src/node/services/providerModelFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,6 @@ const CODEX_ALLOWED_PARAMS = new Set([
"top_p",
"include",
"text", // structured output via Output.object -> text.format
"truncation",
]);

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -628,10 +627,10 @@ function extractTextContent(content: unknown): string {

export function normalizeCodexResponsesBody(body: string): string {
const json = JSON.parse(body) as Record<string, unknown>;
const truncation = json.truncation;
if (truncation !== "auto" && truncation !== "disabled") {
json.truncation = "disabled";
}

// ChatGPT's Codex endpoint is stricter than the public OpenAI Responses API
// and currently rejects the `truncation` field entirely.
delete json.truncation;

// Codex-compatible Responses requests must disable storage and strip unsupported params.
json.store = false;
Expand Down Expand Up @@ -1116,9 +1115,9 @@ export class ProviderModelFactory {
const baseFetch = getProviderFetch(providerConfig);
const codexOauthService = this.codexOauthService;

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

const body = init?.body;
// Only parse the JSON body when routing through Codex OAuth, since Codex
// requires instruction lifting, store=false, and Responses truncation.
// requires instruction lifting, store=false, and stripping unsupported
// Responses fields like `truncation`.
if (
shouldRouteThroughCodexOauth &&
isOpenAIResponses &&
Expand Down Expand Up @@ -1212,7 +1212,7 @@ export class ProviderModelFactory {

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