[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');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
const source = readSource('./upstreamEndpoint.ts');
|
||||
expect(source).not.toContain('function getUpstreamEndpointRuntimeStateSnapshot(');
|
||||
|
||||
@@ -210,7 +210,8 @@ describe('chat proxy codex oauth compatibility', () => {
|
||||
expect(forwardedBody.store).toBe(false);
|
||||
expect(forwardedBody.parallel_tool_calls).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();
|
||||
|
||||
const body = response.json();
|
||||
|
||||
@@ -736,7 +736,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
||||
expect(request.body.store).toBe(false);
|
||||
expect(request.body.parallel_tool_calls).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.top_p).toBe(0.9);
|
||||
expect(request.body.user).toBe('drop-me');
|
||||
@@ -834,7 +836,7 @@ describe('buildUpstreamEndpointRequest', () => {
|
||||
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({
|
||||
endpoint: 'responses',
|
||||
modelName: 'gpt-5.4',
|
||||
@@ -854,6 +856,8 @@ describe('buildUpstreamEndpointRequest', () => {
|
||||
previous_response_id: 'resp_prev_123',
|
||||
temperature: 0.3,
|
||||
top_p: 0.8,
|
||||
max_completion_tokens: 256,
|
||||
max_tokens: 128,
|
||||
max_output_tokens: 512,
|
||||
},
|
||||
providerHeaders: {
|
||||
@@ -873,7 +877,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
||||
expect(request.body.previous_response_id).toBe('resp_prev_123');
|
||||
expect(request.body.temperature).toBe(0.3);
|
||||
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', () => {
|
||||
@@ -1002,6 +1008,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
||||
models: [{ name: 'gpt-5.4', protocol: 'codex' }],
|
||||
params: {
|
||||
'text.verbosity': 'low',
|
||||
max_completion_tokens: 48,
|
||||
max_tokens: 32,
|
||||
max_output_tokens: 64,
|
||||
store: true,
|
||||
},
|
||||
},
|
||||
@@ -1038,6 +1047,9 @@ describe('buildUpstreamEndpointRequest', () => {
|
||||
expect(request.body.reasoning).toEqual({ effort: 'high' });
|
||||
expect(request.body.text).toEqual({ verbosity: 'low' });
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
convertOpenAiBodyToResponsesBody as convertOpenAiBodyToResponsesBodyViaTransformer,
|
||||
sanitizeResponsesBodyForProxy as sanitizeResponsesBodyForProxyViaTransformer,
|
||||
} from '../../transformers/openai/responses/conversion.js';
|
||||
import { normalizeCodexResponsesBodyForProxy } from '../../transformers/openai/responses/codexCompatibility.js';
|
||||
import {
|
||||
convertOpenAiBodyToAnthropicMessagesBody,
|
||||
sanitizeAnthropicMessagesBody,
|
||||
@@ -370,61 +371,6 @@ function toFiniteNumber(value: unknown): number | 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[] {
|
||||
const raw = asTrimmedString(value).toLowerCase();
|
||||
if (!raw) return [];
|
||||
@@ -957,17 +903,11 @@ export function buildUpstreamEndpointRequest(input: {
|
||||
if (preserveWebsocketIncrementalMode && rawBody.generate === false) {
|
||||
sanitizedResponsesBody.generate = false;
|
||||
}
|
||||
const body = ensureCodexResponsesStoreFalse(
|
||||
ensureCodexResponsesInstructions(
|
||||
applyCodexResponsesCompatibility(
|
||||
const body = normalizeCodexResponsesBodyForProxy(
|
||||
sanitizedResponsesBody,
|
||||
sitePlatform,
|
||||
),
|
||||
sitePlatform,
|
||||
),
|
||||
sitePlatform,
|
||||
);
|
||||
const configuredResponsesBody = ensureCodexResponsesStoreFalse(
|
||||
const configuredResponsesBody = normalizeCodexResponsesBodyForProxy(
|
||||
applyConfiguredPayloadRules(body),
|
||||
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