[codex] strip codex responses max_output_tokens (#399)
* strip codex responses max_output_tokens * address codex responses review comments
This commit is contained in:
@@ -98,6 +98,15 @@ describe('proxy route architecture boundaries', () => {
|
|||||||
expect(source).not.toContain('shouldInjectDerivedPromptCacheKey');
|
expect(source).not.toContain('shouldInjectDerivedPromptCacheKey');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps codex responses normalization behind transformer helpers', () => {
|
||||||
|
const source = readSource('./upstreamEndpoint.ts');
|
||||||
|
expect(source).toContain("from '../../transformers/openai/responses/codexCompatibility.js'");
|
||||||
|
expect(source).not.toContain('function ensureCodexResponsesInstructions(');
|
||||||
|
expect(source).not.toContain('function ensureCodexResponsesStoreFalse(');
|
||||||
|
expect(source).not.toContain('function stripCodexUnsupportedResponsesFields(');
|
||||||
|
expect(source).not.toContain('function applyCodexResponsesCompatibility(');
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps endpoint runtime snapshot helper out of the route layer', () => {
|
it('keeps endpoint runtime snapshot helper out of the route layer', () => {
|
||||||
const source = readSource('./upstreamEndpoint.ts');
|
const source = readSource('./upstreamEndpoint.ts');
|
||||||
expect(source).not.toContain('function getUpstreamEndpointRuntimeStateSnapshot(');
|
expect(source).not.toContain('function getUpstreamEndpointRuntimeStateSnapshot(');
|
||||||
|
|||||||
@@ -210,7 +210,8 @@ describe('chat proxy codex oauth compatibility', () => {
|
|||||||
expect(forwardedBody.store).toBe(false);
|
expect(forwardedBody.store).toBe(false);
|
||||||
expect(forwardedBody.parallel_tool_calls).toBeUndefined();
|
expect(forwardedBody.parallel_tool_calls).toBeUndefined();
|
||||||
expect(forwardedBody.include).toBeUndefined();
|
expect(forwardedBody.include).toBeUndefined();
|
||||||
expect(forwardedBody.max_output_tokens).toBe(256);
|
expect(forwardedBody.max_output_tokens).toBeUndefined();
|
||||||
|
expect(forwardedBody.max_tokens).toBeUndefined();
|
||||||
expect(forwardedBody.max_completion_tokens).toBeUndefined();
|
expect(forwardedBody.max_completion_tokens).toBeUndefined();
|
||||||
|
|
||||||
const body = response.json();
|
const body = response.json();
|
||||||
|
|||||||
@@ -736,7 +736,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
|||||||
expect(request.body.store).toBe(false);
|
expect(request.body.store).toBe(false);
|
||||||
expect(request.body.parallel_tool_calls).toBeUndefined();
|
expect(request.body.parallel_tool_calls).toBeUndefined();
|
||||||
expect(request.body.include).toBeUndefined();
|
expect(request.body.include).toBeUndefined();
|
||||||
expect(request.body.max_output_tokens).toBe(4096);
|
expect(request.body.max_output_tokens).toBeUndefined();
|
||||||
|
expect(request.body.max_tokens).toBeUndefined();
|
||||||
|
expect(request.body.max_completion_tokens).toBeUndefined();
|
||||||
expect(request.body.temperature).toBe(0.2);
|
expect(request.body.temperature).toBe(0.2);
|
||||||
expect(request.body.top_p).toBe(0.9);
|
expect(request.body.top_p).toBe(0.9);
|
||||||
expect(request.body.user).toBe('drop-me');
|
expect(request.body.user).toBe('drop-me');
|
||||||
@@ -834,7 +836,7 @@ describe('buildUpstreamEndpointRequest', () => {
|
|||||||
expect(request.body.prompt_cache_key).toBe('codex-cache-123');
|
expect(request.body.prompt_cache_key).toBe('codex-cache-123');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves native codex responses continuity and request fields without compatibility rewrites', () => {
|
it('preserves native codex responses continuity while stripping unsupported token limit fields', () => {
|
||||||
const request = buildUpstreamEndpointRequest({
|
const request = buildUpstreamEndpointRequest({
|
||||||
endpoint: 'responses',
|
endpoint: 'responses',
|
||||||
modelName: 'gpt-5.4',
|
modelName: 'gpt-5.4',
|
||||||
@@ -854,6 +856,8 @@ describe('buildUpstreamEndpointRequest', () => {
|
|||||||
previous_response_id: 'resp_prev_123',
|
previous_response_id: 'resp_prev_123',
|
||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
top_p: 0.8,
|
top_p: 0.8,
|
||||||
|
max_completion_tokens: 256,
|
||||||
|
max_tokens: 128,
|
||||||
max_output_tokens: 512,
|
max_output_tokens: 512,
|
||||||
},
|
},
|
||||||
providerHeaders: {
|
providerHeaders: {
|
||||||
@@ -873,7 +877,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
|||||||
expect(request.body.previous_response_id).toBe('resp_prev_123');
|
expect(request.body.previous_response_id).toBe('resp_prev_123');
|
||||||
expect(request.body.temperature).toBe(0.3);
|
expect(request.body.temperature).toBe(0.3);
|
||||||
expect(request.body.top_p).toBe(0.8);
|
expect(request.body.top_p).toBe(0.8);
|
||||||
expect(request.body.max_output_tokens).toBe(512);
|
expect(request.body.max_completion_tokens).toBeUndefined();
|
||||||
|
expect(request.body.max_tokens).toBeUndefined();
|
||||||
|
expect(request.body.max_output_tokens).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies configured codex header defaults with CLIProxyAPI-compatible precedence', () => {
|
it('applies configured codex header defaults with CLIProxyAPI-compatible precedence', () => {
|
||||||
@@ -1002,6 +1008,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
|||||||
models: [{ name: 'gpt-5.4', protocol: 'codex' }],
|
models: [{ name: 'gpt-5.4', protocol: 'codex' }],
|
||||||
params: {
|
params: {
|
||||||
'text.verbosity': 'low',
|
'text.verbosity': 'low',
|
||||||
|
max_completion_tokens: 48,
|
||||||
|
max_tokens: 32,
|
||||||
|
max_output_tokens: 64,
|
||||||
store: true,
|
store: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1038,6 +1047,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
|||||||
expect(request.body.reasoning).toEqual({ effort: 'high' });
|
expect(request.body.reasoning).toEqual({ effort: 'high' });
|
||||||
expect(request.body.text).toEqual({ verbosity: 'low' });
|
expect(request.body.text).toEqual({ verbosity: 'low' });
|
||||||
expect(request.body.safety_identifier).toBeUndefined();
|
expect(request.body.safety_identifier).toBeUndefined();
|
||||||
|
expect(request.body.max_completion_tokens).toBeUndefined();
|
||||||
|
expect(request.body.max_tokens).toBeUndefined();
|
||||||
|
expect(request.body.max_output_tokens).toBeUndefined();
|
||||||
expect(request.body.store).toBe(false);
|
expect(request.body.store).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
convertOpenAiBodyToResponsesBody as convertOpenAiBodyToResponsesBodyViaTransformer,
|
convertOpenAiBodyToResponsesBody as convertOpenAiBodyToResponsesBodyViaTransformer,
|
||||||
sanitizeResponsesBodyForProxy as sanitizeResponsesBodyForProxyViaTransformer,
|
sanitizeResponsesBodyForProxy as sanitizeResponsesBodyForProxyViaTransformer,
|
||||||
} from '../../transformers/openai/responses/conversion.js';
|
} from '../../transformers/openai/responses/conversion.js';
|
||||||
|
import { normalizeCodexResponsesBodyForProxy } from '../../transformers/openai/responses/codexCompatibility.js';
|
||||||
import {
|
import {
|
||||||
convertOpenAiBodyToAnthropicMessagesBody,
|
convertOpenAiBodyToAnthropicMessagesBody,
|
||||||
sanitizeAnthropicMessagesBody,
|
sanitizeAnthropicMessagesBody,
|
||||||
@@ -370,61 +371,6 @@ function toFiniteNumber(value: unknown): number | null {
|
|||||||
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureCodexResponsesInstructions(
|
|
||||||
body: Record<string, unknown>,
|
|
||||||
sitePlatform: string,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
if (sitePlatform !== 'codex') return body;
|
|
||||||
if (typeof body.instructions === 'string') return body;
|
|
||||||
return {
|
|
||||||
...body,
|
|
||||||
instructions: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCodexResponsesStoreFalse(
|
|
||||||
body: Record<string, unknown>,
|
|
||||||
sitePlatform: string,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
if (sitePlatform !== 'codex') return body;
|
|
||||||
return {
|
|
||||||
...body,
|
|
||||||
store: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertCodexSystemRoleToDeveloper(input: unknown): unknown {
|
|
||||||
if (!Array.isArray(input)) return input;
|
|
||||||
return input.map((item) => {
|
|
||||||
if (!isRecord(item)) return item;
|
|
||||||
if (asTrimmedString(item.type).toLowerCase() !== 'message') return item;
|
|
||||||
if (asTrimmedString(item.role).toLowerCase() !== 'system') return item;
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
role: 'developer',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyCodexResponsesCompatibility(
|
|
||||||
body: Record<string, unknown>,
|
|
||||||
sitePlatform: string,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
if (sitePlatform !== 'codex') return body;
|
|
||||||
|
|
||||||
const next: Record<string, unknown> = {
|
|
||||||
...body,
|
|
||||||
input: convertCodexSystemRoleToDeveloper(body.input),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof next.instructions !== 'string') {
|
|
||||||
next.instructions = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function normalizeEndpointTypes(value: unknown): UpstreamEndpoint[] {
|
function normalizeEndpointTypes(value: unknown): UpstreamEndpoint[] {
|
||||||
const raw = asTrimmedString(value).toLowerCase();
|
const raw = asTrimmedString(value).toLowerCase();
|
||||||
if (!raw) return [];
|
if (!raw) return [];
|
||||||
@@ -957,17 +903,11 @@ export function buildUpstreamEndpointRequest(input: {
|
|||||||
if (preserveWebsocketIncrementalMode && rawBody.generate === false) {
|
if (preserveWebsocketIncrementalMode && rawBody.generate === false) {
|
||||||
sanitizedResponsesBody.generate = false;
|
sanitizedResponsesBody.generate = false;
|
||||||
}
|
}
|
||||||
const body = ensureCodexResponsesStoreFalse(
|
const body = normalizeCodexResponsesBodyForProxy(
|
||||||
ensureCodexResponsesInstructions(
|
sanitizedResponsesBody,
|
||||||
applyCodexResponsesCompatibility(
|
|
||||||
sanitizedResponsesBody,
|
|
||||||
sitePlatform,
|
|
||||||
),
|
|
||||||
sitePlatform,
|
|
||||||
),
|
|
||||||
sitePlatform,
|
sitePlatform,
|
||||||
);
|
);
|
||||||
const configuredResponsesBody = ensureCodexResponsesStoreFalse(
|
const configuredResponsesBody = normalizeCodexResponsesBodyForProxy(
|
||||||
applyConfiguredPayloadRules(body),
|
applyConfiguredPayloadRules(body),
|
||||||
sitePlatform,
|
sitePlatform,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { normalizeCodexResponsesBodyForProxy } from './codexCompatibility.js';
|
||||||
|
|
||||||
|
describe('normalizeCodexResponsesBodyForProxy', () => {
|
||||||
|
it('normalizes codex responses bodies before proxying upstream', () => {
|
||||||
|
const body = normalizeCodexResponsesBodyForProxy({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
type: 'message',
|
||||||
|
role: 'system',
|
||||||
|
content: [{ type: 'input_text', text: 'be precise' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
max_output_tokens: 512,
|
||||||
|
max_completion_tokens: 256,
|
||||||
|
max_tokens: 128,
|
||||||
|
temperature: 0.3,
|
||||||
|
store: true,
|
||||||
|
}, 'codex');
|
||||||
|
|
||||||
|
expect(body).toEqual({
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
type: 'message',
|
||||||
|
role: 'developer',
|
||||||
|
content: [{ type: 'input_text', text: 'be precise' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
instructions: '',
|
||||||
|
store: false,
|
||||||
|
temperature: 0.3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves non-codex bodies untouched', () => {
|
||||||
|
const source = {
|
||||||
|
input: 'hello',
|
||||||
|
max_output_tokens: 512,
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = normalizeCodexResponsesBodyForProxy(source, 'openai');
|
||||||
|
|
||||||
|
expect(body).toBe(source);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return !!value && typeof value === 'object';
|
||||||
|
}
|
||||||
|
|
||||||
|
function asTrimmedString(value: unknown): string {
|
||||||
|
return typeof value === 'string' ? value.trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCodexResponsesInstructions(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
sitePlatform: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (sitePlatform !== 'codex') return body;
|
||||||
|
if (typeof body.instructions === 'string') return body;
|
||||||
|
return {
|
||||||
|
...body,
|
||||||
|
instructions: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCodexResponsesStoreFalse(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
sitePlatform: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (sitePlatform !== 'codex') return body;
|
||||||
|
return {
|
||||||
|
...body,
|
||||||
|
store: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripCodexUnsupportedResponsesFields(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
sitePlatform: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (sitePlatform !== 'codex') return body;
|
||||||
|
const next = { ...body };
|
||||||
|
delete next.max_output_tokens;
|
||||||
|
delete next.max_completion_tokens;
|
||||||
|
delete next.max_tokens;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertCodexSystemRoleToDeveloper(input: unknown): unknown {
|
||||||
|
if (!Array.isArray(input)) return input;
|
||||||
|
return input.map((item) => {
|
||||||
|
if (!isRecord(item)) return item;
|
||||||
|
if (asTrimmedString(item.type).toLowerCase() !== 'message') return item;
|
||||||
|
if (asTrimmedString(item.role).toLowerCase() !== 'system') return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
role: 'developer',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCodexResponsesCompatibility(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
sitePlatform: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (sitePlatform !== 'codex') return body;
|
||||||
|
|
||||||
|
const next: Record<string, unknown> = {
|
||||||
|
...body,
|
||||||
|
input: convertCodexSystemRoleToDeveloper(body.input),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof next.instructions !== 'string') {
|
||||||
|
next.instructions = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCodexResponsesBodyForProxy(
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
sitePlatform: string,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
if (sitePlatform !== 'codex') return body;
|
||||||
|
return ensureCodexResponsesStoreFalse(
|
||||||
|
stripCodexUnsupportedResponsesFields(
|
||||||
|
ensureCodexResponsesInstructions(
|
||||||
|
applyCodexResponsesCompatibility(body, sitePlatform),
|
||||||
|
sitePlatform,
|
||||||
|
),
|
||||||
|
sitePlatform,
|
||||||
|
),
|
||||||
|
sitePlatform,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user