diff --git a/src/cliproxy/tool-sanitization-proxy.ts b/src/cliproxy/tool-sanitization-proxy.ts index 2ef0a572..ace07e4b 100644 --- a/src/cliproxy/tool-sanitization-proxy.ts +++ b/src/cliproxy/tool-sanitization-proxy.ts @@ -54,15 +54,32 @@ const GEMINI_UNSUPPORTED_TOOL_FIELDS = new Set([ 'defer_loading', ]); -const CODEX_UNSUPPORTED_TOOL_FIELDS = new Set(['cache_control', 'defer_loading']); +const CODEX_UNSUPPORTED_TOOL_FIELDS = new Set(['cache_control']); +const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i; +const LEGACY_CODEX_MODEL_ID_REGEX = /^gpt-5(?:\.\d+)?-codex(?:-(?:mini|max))?$/i; + +function canonicalizeCodexModelId(model: string | undefined): string | null { + const normalizedModel = model?.trim().toLowerCase(); + if (!normalizedModel) { + return null; + } + + const withoutExtendedContext = normalizedModel.replace(EXTENDED_CONTEXT_SUFFIX_REGEX, '').trim(); + return stripCodexEffortSuffix(withoutExtendedContext); +} function isKnownCodexModelId(model: string | undefined): boolean { - const normalizedModel = model?.trim().toLowerCase(); + const normalizedModel = canonicalizeCodexModelId(model); if (!normalizedModel) { return false; } - return getModelMaxLevel('codex', stripCodexEffortSuffix(normalizedModel)) !== undefined; + // Root-routed requests can carry Codex model IDs that CCS uses outside the + // small interactive catalog (for example image analysis and Cursor defaults). + return ( + LEGACY_CODEX_MODEL_ID_REGEX.test(normalizedModel) || + getModelMaxLevel('codex', normalizedModel) !== undefined + ); } function getUnsupportedToolFields( diff --git a/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts b/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts index 8bb9a66a..4f4c6890 100644 --- a/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts +++ b/tests/unit/cliproxy/tool-sanitization-proxy-integration.test.ts @@ -384,7 +384,7 @@ describe('ToolSanitizationProxy Integration', () => { } }); - it('strips Codex-unsupported top-level tool fields before forwarding', async () => { + it('strips only Codex-unsupported top-level tool fields before forwarding', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); @@ -422,7 +422,7 @@ describe('ToolSanitizationProxy Integration', () => { expect(sentTools[0].name).toBe('codex_tool'); expect(sentTools[0].description).toBe('Codex test'); expect(sentTools[0].cache_control).toBeUndefined(); - expect(sentTools[0].defer_loading).toBeUndefined(); + expect(sentTools[0].defer_loading).toBe(true); expect(sentTools[0].input_schema).toEqual({ type: 'object', properties: { @@ -436,57 +436,64 @@ describe('ToolSanitizationProxy Integration', () => { } }); - it('strips Codex-unsupported top-level tool fields on root model-routed requests', async () => { - const proxy = new ToolSanitizationProxy({ - upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, - }); - const port = await proxy.start(); + for (const model of [ + 'gpt-5.3-codex-xhigh', + 'gpt-5.1-codex-mini', + 'gpt-5.1-codex', + 'gpt-5-codex', + ]) { + it(`strips only Codex-unsupported top-level tool fields on root model-routed request (${model})`, async () => { + const proxy = new ToolSanitizationProxy({ + upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, + }); + const port = await proxy.start(); - try { - await fetch(`http://127.0.0.1:${port}/v1/messages`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: 'gpt-5.3-codex-xhigh', - tools: [ - { - name: 'root_codex_tool', - description: 'Root-routed Codex test', - cache_control: { type: 'ephemeral' }, - defer_loading: true, - input_schema: { - type: 'object', - properties: { - prompt: { - type: 'string', - examples: ['fix the failing test'], + try { + await fetch(`http://127.0.0.1:${port}/v1/messages`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model, + tools: [ + { + name: 'root_codex_tool', + description: 'Root-routed Codex test', + cache_control: { type: 'ephemeral' }, + defer_loading: true, + input_schema: { + type: 'object', + properties: { + prompt: { + type: 'string', + examples: ['fix the failing test'], + }, }, }, }, - }, - ], - }), - }); + ], + }), + }); - const sentTools = (lastRequest!.body as Record).tools as Array< - Record - >; - expect(sentTools[0].name).toBe('root_codex_tool'); - expect(sentTools[0].description).toBe('Root-routed Codex test'); - expect(sentTools[0].cache_control).toBeUndefined(); - expect(sentTools[0].defer_loading).toBeUndefined(); - expect(sentTools[0].input_schema).toEqual({ - type: 'object', - properties: { - prompt: { - type: 'string', + const sentTools = (lastRequest!.body as Record).tools as Array< + Record + >; + expect(sentTools[0].name).toBe('root_codex_tool'); + expect(sentTools[0].description).toBe('Root-routed Codex test'); + expect(sentTools[0].cache_control).toBeUndefined(); + expect(sentTools[0].defer_loading).toBe(true); + expect(sentTools[0].input_schema).toEqual({ + type: 'object', + properties: { + prompt: { + type: 'string', + }, }, - }, - }); - } finally { - proxy.stop(); - } - }); + }); + } finally { + proxy.stop(); + } + }); + } it('preserves top-level tool fields for non-target root routes', async () => { const proxy = new ToolSanitizationProxy({