diff --git a/src/lib/shadow-call.ts b/src/lib/shadow-call.ts index 829ea69119..8275b8f93a 100644 --- a/src/lib/shadow-call.ts +++ b/src/lib/shadow-call.ts @@ -32,15 +32,23 @@ export function isShadowSourceModel(modelId: string, configured?: unknown): bool /** * Decide whether a matching source model should use the opt-in intercept. * - * Before Codex 0.147.0 this checked x-codex-turn-metadata and exempted - * request_kind "turn". Codex 0.147.0 can label background helper calls as - * "turn", causing them to bypass the intercept (#1684). The fix is to - * intercept every configured shadow source model unconditionally — the model - * slug alone is a sufficient signal. + * Codex identifies normal user turns and maintenance requests in + * x-codex-turn-metadata. Only an explicit normal turn bypasses interception; + * missing or unrecognized metadata retains the legacy opt-in prefix behavior. */ export function shouldInterceptShadowCall( modelId: string, configured: unknown, + headers: Headers, ): boolean { - return isShadowSourceModel(modelId, configured); + if (!isShadowSourceModel(modelId, configured)) return false; + const rawMetadata = headers.get("x-codex-turn-metadata"); + if (rawMetadata === null) return true; + + try { + const parsed = JSON.parse(rawMetadata) as { request_kind?: unknown }; + return parsed?.request_kind !== "turn"; + } catch { + return true; + } } diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index cc2a6c7a7a..0ab95ec795 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -1598,6 +1598,7 @@ async function handleResponsesInner( if (_sci?.enabled && _sci.model && shouldInterceptShadowCall( parsed.modelId, _sci.sourceModels, + req.headers, )) { const _sciOriginal = parsed.modelId; parsed.modelId = _sci.model; diff --git a/tests/responses-shadow-intercept.test.ts b/tests/responses-shadow-intercept.test.ts index 7950f86c60..dfec76c74e 100644 --- a/tests/responses-shadow-intercept.test.ts +++ b/tests/responses-shadow-intercept.test.ts @@ -58,19 +58,32 @@ describe("isShadowSourceModel", () => { }); describe("shouldInterceptShadowCall", () => { - test("intercepts every shadow source model unconditionally (#1684)", () => { - expect(shouldInterceptShadowCall("gpt-5.6-luna", undefined)).toBe(true); - expect(shouldInterceptShadowCall("gpt-5.6-luna-2026-08", undefined)).toBe(true); + test("does not intercept an explicit foreground turn", () => { + const headers = new Headers({ + "x-codex-turn-metadata": JSON.stringify({ request_kind: "turn" }), + }); + expect(shouldInterceptShadowCall("gpt-5.6-luna", undefined, headers)).toBe(false); + expect(shouldInterceptShadowCall("gpt-5.6-luna-2026-08", undefined, headers)).toBe(false); + }); + + test("intercepts maintenance, headerless, and malformed-metadata requests", () => { + const maintenance = new Headers({ + "x-codex-turn-metadata": JSON.stringify({ request_kind: "memory" }), + }); + const malformed = new Headers({ "x-codex-turn-metadata": "{not json" }); + expect(shouldInterceptShadowCall("gpt-5.6-luna", undefined, maintenance)).toBe(true); + expect(shouldInterceptShadowCall("gpt-5.6-luna", undefined, new Headers())).toBe(true); + expect(shouldInterceptShadowCall("gpt-5.6-luna", undefined, malformed)).toBe(true); }); test("does not intercept non-source models", () => { - expect(shouldInterceptShadowCall("gpt-5.6-terra", undefined)).toBe(false); - expect(shouldInterceptShadowCall("gpt-5.5", undefined)).toBe(false); + expect(shouldInterceptShadowCall("gpt-5.6-terra", undefined, new Headers())).toBe(false); + expect(shouldInterceptShadowCall("gpt-5.5", undefined, new Headers())).toBe(false); }); test("respects configured sourceModels override", () => { - expect(shouldInterceptShadowCall("custom-helper-v2", ["custom-helper"])).toBe(true); - expect(shouldInterceptShadowCall("gpt-5.6-luna", ["custom-helper"])).toBe(false); + expect(shouldInterceptShadowCall("custom-helper-v2", ["custom-helper"], new Headers())).toBe(true); + expect(shouldInterceptShadowCall("gpt-5.6-luna", ["custom-helper"], new Headers())).toBe(false); }); }); @@ -128,7 +141,7 @@ describe("shadow call intercept request path (issue #311)", () => { expect(effort).toBe("low"); }); - test("rewrites a gpt-5.6-luna turn request too (#1684)", async () => { + test("does not rewrite a gpt-5.6-luna foreground turn", async () => { const bodies: Array> = []; globalThis.fetch = (async (_url: unknown, init?: RequestInit) => { bodies.push(JSON.parse(String(init?.body ?? "{}")) as Record); @@ -140,8 +153,7 @@ describe("shadow call intercept request path (issue #311)", () => { await post(interceptConfig(), "gpt-5.6-luna", "turn"); - expect(bodies.length).toBe(1); - expect(String(bodies[0]?.model ?? "")).toContain("grok-4.5"); + expect(bodies.length).toBe(0); }); test("leaves gpt-5.6-terra requests unrewritten", async () => {