fix: normalize Codex provider requests

Fixes #1399
Fixes #1406
This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-29 10:19:51 -04:00
committed by GitHub
parent 065df76820
commit 0f0ad4e6ed
2 changed files with 268 additions and 0 deletions
@@ -436,6 +436,155 @@ describe('ToolSanitizationProxy Integration', () => {
}
});
it('translates Codex fast aliases before forwarding to provider routes', 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}/api/provider/codex/v1/messages?beta=true`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5.5-fast',
messages: [],
}),
});
const sentBody = lastRequest!.body as Record<string, unknown>;
expect(sentBody.model).toBe('gpt-5.5');
expect(sentBody.service_tier).toBe('priority');
} finally {
proxy.stop();
}
});
it('translates Codex effort and fast aliases before forwarding to provider routes', 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}/api/provider/codex/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5.5-fast-high',
messages: [],
reasoning: { summary: 'auto' },
}),
});
const sentBody = lastRequest!.body as Record<string, unknown>;
expect(sentBody.model).toBe('gpt-5.5');
expect((sentBody.reasoning as Record<string, unknown>).summary).toBe('auto');
expect((sentBody.reasoning as Record<string, unknown>).effort).toBe('high');
expect(sentBody.service_tier).toBe('priority');
} finally {
proxy.stop();
}
});
it('folds Codex system messages into the first user message before forwarding', 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}/api/provider/codex/v1/messages?beta=true`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5.4',
system: [{ type: 'text', text: 'Always be concise.' }],
messages: [
{ role: 'system', content: 'Use JSON.' },
{ role: 'user', content: [{ type: 'text', text: 'hello' }] },
],
}),
});
const sentBody = lastRequest!.body as Record<string, unknown>;
const sentMessages = sentBody.messages as Array<Record<string, unknown>>;
expect(sentBody.system).toBeUndefined();
expect(sentMessages).toHaveLength(1);
expect(sentMessages[0].role).toBe('user');
expect(sentMessages[0].content).toEqual([
{ type: 'text', text: 'Always be concise.\n\nUse JSON.' },
{ type: 'text', text: 'hello' },
]);
} finally {
proxy.stop();
}
});
it('strips blank Codex system messages before forwarding', 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}/api/provider/codex/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5.4',
system: ' ',
messages: [
{ role: 'system', content: [{ type: 'text', text: ' ' }] },
{ role: 'user', content: 'hello' },
],
}),
});
const sentBody = lastRequest!.body as Record<string, unknown>;
const sentMessages = sentBody.messages as Array<Record<string, unknown>>;
expect(sentBody.system).toBeUndefined();
expect(sentMessages).toEqual([{ role: 'user', content: 'hello' }]);
} finally {
proxy.stop();
}
});
it('does not apply Codex alias or system rewrites on explicit non-Codex provider routes', 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}/api/provider/gemini/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5.5-fast',
system: 'Keep this as a provider-level system field.',
messages: [
{ role: 'system', content: 'Keep this as a message.' },
{ role: 'user', content: 'hello' },
],
}),
});
const sentBody = lastRequest!.body as Record<string, unknown>;
expect(sentBody.model).toBe('gpt-5.5-fast');
expect(sentBody.service_tier).toBeUndefined();
expect(sentBody.system).toBe('Keep this as a provider-level system field.');
expect(sentBody.messages).toEqual([
{ role: 'system', content: 'Keep this as a message.' },
{ role: 'user', content: 'hello' },
]);
} finally {
proxy.stop();
}
});
for (const model of [
'gpt-5.3-codex-xhigh',
'gpt-5.1-codex-mini',
@@ -22,6 +22,7 @@ import {
extractProviderFromPathname,
getDeniedModelIdReasonForProvider,
normalizeModelIdForRouting,
parseCodexModelTuningAlias,
stripCodexEffortSuffix,
} from '../ai-providers/model-id-normalizer';
import { getModelMaxLevel } from '../model-catalog';
@@ -63,6 +64,7 @@ const GEMINI_UNSUPPORTED_TOOL_FIELDS = new Set([
]);
const CODEX_UNSUPPORTED_TOOL_FIELDS = new Set(['cache_control']);
const CODEX_FAST_SERVICE_TIER = 'priority';
const EXTENDED_CONTEXT_SUFFIX_REGEX = /\[1m\]$/i;
const LEGACY_CODEX_MODEL_ID_REGEX = /^gpt-5(?:\.\d+)?-codex(?:-(?:mini|max))?$/i;
@@ -90,6 +92,118 @@ function isKnownCodexModelId(model: string | undefined): boolean {
);
}
function isCodexRequest(providerFromPath: string | null, model: unknown): boolean {
return (
providerFromPath === 'codex' ||
(providerFromPath === null && typeof model === 'string' && isKnownCodexModelId(model))
);
}
function applyCodexModelTuningAlias(body: Record<string, unknown>): Record<string, unknown> {
if (typeof body.model !== 'string') {
return body;
}
const parsed = parseCodexModelTuningAlias(body.model);
if (!parsed || !isKnownCodexModelId(parsed.baseModel)) {
return body;
}
const tunedBody: Record<string, unknown> = { ...body, model: parsed.baseModel };
if (parsed.effort) {
const existingReasoning = isRecord(body.reasoning) ? body.reasoning : {};
tunedBody.reasoning = {
...existingReasoning,
effort: parsed.effort,
};
}
if (parsed.serviceTier) {
tunedBody.service_tier = CODEX_FAST_SERVICE_TIER;
}
return tunedBody;
}
function extractSystemText(content: unknown): string {
if (typeof content === 'string') {
return content;
}
if (!Array.isArray(content)) {
return '';
}
return content
.filter((block): block is { type: unknown; text?: unknown } => isRecord(block))
.filter((block) => block.type === 'text' && typeof block.text === 'string')
.map((block) => block.text as string)
.join('\n\n');
}
function prependSystemTextToContent(content: unknown, systemText: string): unknown {
if (Array.isArray(content)) {
return [{ type: 'text', text: systemText }, ...content];
}
if (typeof content === 'string') {
return `${systemText}\n\n${content}`;
}
return systemText;
}
function foldCodexSystemMessages(body: Record<string, unknown>): Record<string, unknown> {
const systemTexts: string[] = [];
let removedSystem = false;
const nextBody = { ...body };
if (body.system !== undefined) {
removedSystem = true;
delete nextBody.system;
const systemText = extractSystemText(body.system).trim();
if (systemText) {
systemTexts.push(systemText);
}
}
const rawMessages = Array.isArray(body.messages) ? body.messages : [];
const messages = rawMessages.filter((message) => {
if (!isRecord(message) || message.role !== 'system') {
return true;
}
removedSystem = true;
const systemText = extractSystemText(message.content).trim();
if (systemText) {
systemTexts.push(systemText);
}
return false;
});
if (!removedSystem) {
return body;
}
if (systemTexts.length > 0) {
const systemPrefix = systemTexts.join('\n\n');
const firstUserIndex = messages.findIndex(
(message) => isRecord(message) && message.role === 'user'
);
if (firstUserIndex >= 0 && isRecord(messages[firstUserIndex])) {
const firstUserMessage = messages[firstUserIndex];
messages[firstUserIndex] = {
...firstUserMessage,
content: prependSystemTextToContent(firstUserMessage.content, systemPrefix),
};
} else {
messages.unshift({ role: 'user', content: systemPrefix });
}
}
nextBody.messages = messages;
return nextBody;
}
function getUnsupportedToolFields(
providerFromPath: string | null,
model: string | undefined
@@ -352,6 +466,11 @@ export class ToolSanitizationProxy {
}
}
if (isRecord(modifiedBody) && isCodexRequest(providerFromPath, modifiedBody.model)) {
const tunedBody = applyCodexModelTuningAlias(modifiedBody);
modifiedBody = foldCodexSystemMessages(tunedBody);
}
// Sanitize tools if present
if (isRecord(modifiedBody) && Array.isArray(modifiedBody.tools)) {
// Step 1: Sanitize input_schema properties (remove non-standard JSON Schema properties)