fix: complete codex tool sanitization coverage

This commit is contained in:
Tam Nhu Tran
2026-03-07 10:35:41 +07:00
parent 8cfd86f1d1
commit 4baed01d49
2 changed files with 74 additions and 50 deletions
+20 -3
View File
@@ -54,15 +54,32 @@ const GEMINI_UNSUPPORTED_TOOL_FIELDS = new Set([
'defer_loading', '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 { function isKnownCodexModelId(model: string | undefined): boolean {
const normalizedModel = model?.trim().toLowerCase(); const normalizedModel = canonicalizeCodexModelId(model);
if (!normalizedModel) { if (!normalizedModel) {
return false; 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( function getUnsupportedToolFields(
@@ -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({ const proxy = new ToolSanitizationProxy({
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`,
}); });
@@ -422,7 +422,7 @@ describe('ToolSanitizationProxy Integration', () => {
expect(sentTools[0].name).toBe('codex_tool'); expect(sentTools[0].name).toBe('codex_tool');
expect(sentTools[0].description).toBe('Codex test'); expect(sentTools[0].description).toBe('Codex test');
expect(sentTools[0].cache_control).toBeUndefined(); 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({ expect(sentTools[0].input_schema).toEqual({
type: 'object', type: 'object',
properties: { properties: {
@@ -436,57 +436,64 @@ describe('ToolSanitizationProxy Integration', () => {
} }
}); });
it('strips Codex-unsupported top-level tool fields on root model-routed requests', async () => { for (const model of [
const proxy = new ToolSanitizationProxy({ 'gpt-5.3-codex-xhigh',
upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, 'gpt-5.1-codex-mini',
}); 'gpt-5.1-codex',
const port = await proxy.start(); '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 { try {
await fetch(`http://127.0.0.1:${port}/v1/messages`, { await fetch(`http://127.0.0.1:${port}/v1/messages`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
model: 'gpt-5.3-codex-xhigh', model,
tools: [ tools: [
{ {
name: 'root_codex_tool', name: 'root_codex_tool',
description: 'Root-routed Codex test', description: 'Root-routed Codex test',
cache_control: { type: 'ephemeral' }, cache_control: { type: 'ephemeral' },
defer_loading: true, defer_loading: true,
input_schema: { input_schema: {
type: 'object', type: 'object',
properties: { properties: {
prompt: { prompt: {
type: 'string', type: 'string',
examples: ['fix the failing test'], examples: ['fix the failing test'],
},
}, },
}, },
}, },
}, ],
], }),
}), });
});
const sentTools = (lastRequest!.body as Record<string, unknown>).tools as Array< const sentTools = (lastRequest!.body as Record<string, unknown>).tools as Array<
Record<string, unknown> Record<string, unknown>
>; >;
expect(sentTools[0].name).toBe('root_codex_tool'); expect(sentTools[0].name).toBe('root_codex_tool');
expect(sentTools[0].description).toBe('Root-routed Codex test'); expect(sentTools[0].description).toBe('Root-routed Codex test');
expect(sentTools[0].cache_control).toBeUndefined(); 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({ expect(sentTools[0].input_schema).toEqual({
type: 'object', type: 'object',
properties: { properties: {
prompt: { prompt: {
type: 'string', type: 'string',
},
}, },
}, });
}); } finally {
} finally { proxy.stop();
proxy.stop(); }
} });
}); }
it('preserves top-level tool fields for non-target root routes', async () => { it('preserves top-level tool fields for non-target root routes', async () => {
const proxy = new ToolSanitizationProxy({ const proxy = new ToolSanitizationProxy({