complete codex cliproxyapi parity

This commit is contained in:
cita
2026-03-20 11:23:29 +08:00
parent 5403a946c6
commit adc4fb1be7
28 changed files with 2969 additions and 204 deletions
@@ -187,6 +187,27 @@ describe('sanitizeResponsesBodyForProxy', () => {
},
});
});
it('preserves unknown top-level fields while still normalizing known compatibility fields', () => {
const result = sanitizeResponsesBodyForProxy(
{
model: 'gpt-5',
input: 'hello',
max_completion_tokens: 256,
custom_vendor_flag: 'keep-me',
},
'gpt-5',
false,
);
expect(result).toMatchObject({
model: 'gpt-5',
stream: false,
custom_vendor_flag: 'keep-me',
max_output_tokens: 256,
});
expect(result.max_completion_tokens).toBeUndefined();
});
});
describe('convertOpenAiBodyToResponsesBody', () => {
@@ -432,6 +453,84 @@ describe('convertOpenAiBodyToResponsesBody', () => {
},
]);
});
it('shortens long MCP tool names consistently across tools, tool_choice and assistant tool calls', () => {
const sharedSuffix = 'server__execute_super_long_nested_tool_name_that_needs_shortening';
const firstName = `mcp__alpha_workspace__${sharedSuffix}`;
const secondName = `mcp__beta_workspace__${sharedSuffix}`;
const result = convertOpenAiBodyToResponsesBody(
{
model: 'gpt-5',
messages: [
{
role: 'assistant',
tool_calls: [
{
id: 'call_1',
type: 'function',
function: {
name: firstName,
arguments: '{"city":"shanghai"}',
},
},
],
},
{
role: 'tool',
tool_call_id: 'call_1',
content: 'done',
},
],
tools: [
{
type: 'function',
function: {
name: firstName,
parameters: { type: 'object' },
},
},
{
type: 'function',
function: {
name: secondName,
parameters: { type: 'object' },
},
},
],
tool_choice: {
type: 'function',
function: {
name: secondName,
},
},
},
'gpt-5',
false,
);
const toolNames = Array.isArray(result.tools)
? result.tools.map((tool: any) => tool.name)
: [];
const assistantCall = Array.isArray(result.input)
? result.input.find((item: any) => item.type === 'function_call')
: null;
expect(toolNames).toHaveLength(2);
expect(toolNames[0].length).toBeLessThanOrEqual(64);
expect(toolNames[1].length).toBeLessThanOrEqual(64);
expect(toolNames[0].startsWith('mcp__')).toBe(true);
expect(toolNames[1].startsWith('mcp__')).toBe(true);
expect(toolNames[0]).not.toBe(toolNames[1]);
expect(result.tool_choice).toEqual({
type: 'function',
name: toolNames[1],
});
expect(assistantCall).toMatchObject({
type: 'function_call',
name: toolNames[0],
});
});
});
describe('convertResponsesBodyToOpenAiBody', () => {
@@ -3,6 +3,7 @@ import {
normalizeResponsesMessageItem,
} from './compatibility.js';
import { normalizeInputFileBlock, toOpenAiChatFileBlock } from '../../shared/inputFile.js';
import { buildShortToolNameMap, getShortToolName } from '../../shared/toolNameShortener.js';
function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
@@ -247,7 +248,49 @@ function toResponsesInputMessageFromText(text: string): Record<string, unknown>
};
}
function convertOpenAiToolsToResponses(rawTools: unknown): unknown {
function collectOpenAiToolNames(body: Record<string, unknown>): string[] {
const names: string[] = [];
const pushName = (value: unknown) => {
const name = asTrimmedString(value);
if (name) names.push(name);
};
const rawTools = Array.isArray(body.tools) ? body.tools : [];
for (const item of rawTools) {
if (!isRecord(item)) continue;
const type = asTrimmedString(item.type).toLowerCase();
if (type === 'function' && isRecord(item.function)) {
pushName(item.function.name);
continue;
}
if (type === 'function') {
pushName(item.name);
}
}
const toolChoice = isRecord(body.tool_choice) ? body.tool_choice : null;
if (toolChoice && asTrimmedString(toolChoice.type).toLowerCase() === 'function') {
pushName(isRecord(toolChoice.function) ? toolChoice.function.name : toolChoice.name);
}
const rawMessages = Array.isArray(body.messages) ? body.messages : [];
for (const message of rawMessages) {
if (!isRecord(message) || asTrimmedString(message.role).toLowerCase() !== 'assistant') continue;
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
for (const toolCall of toolCalls) {
if (!isRecord(toolCall)) continue;
const functionPart = isRecord(toolCall.function) ? toolCall.function : null;
pushName(functionPart?.name ?? toolCall.name);
}
}
return names;
}
function convertOpenAiToolsToResponses(
rawTools: unknown,
toolNameMap: Record<string, string>,
): unknown {
if (!Array.isArray(rawTools)) return rawTools;
const converted = rawTools
@@ -262,7 +305,7 @@ function convertOpenAiToolsToResponses(rawTools: unknown): unknown {
const mapped: Record<string, unknown> = {
type: 'function',
name,
name: getShortToolName(name, toolNameMap),
};
const description = asTrimmedString(fn.description);
if (description) mapped.description = description;
@@ -272,7 +315,10 @@ function convertOpenAiToolsToResponses(rawTools: unknown): unknown {
}
if (type === 'function' && asTrimmedString(item.name)) {
return item;
return {
...item,
name: getShortToolName(asTrimmedString(item.name), toolNameMap),
};
}
if (type === 'image_generation') {
@@ -290,7 +336,10 @@ function convertOpenAiToolsToResponses(rawTools: unknown): unknown {
return converted.length > 0 ? converted : rawTools;
}
function convertOpenAiToolChoiceToResponses(rawToolChoice: unknown): unknown {
function convertOpenAiToolChoiceToResponses(
rawToolChoice: unknown,
toolNameMap: Record<string, string>,
): unknown {
if (rawToolChoice === undefined) return undefined;
if (typeof rawToolChoice === 'string') return rawToolChoice;
if (!isRecord(rawToolChoice)) return rawToolChoice;
@@ -299,7 +348,7 @@ function convertOpenAiToolChoiceToResponses(rawToolChoice: unknown): unknown {
if (type === 'function' && isRecord(rawToolChoice.function)) {
const name = asTrimmedString(rawToolChoice.function.name);
if (!name) return 'required';
return { type: 'function', name };
return { type: 'function', name: getShortToolName(name, toolNameMap) };
}
return rawToolChoice;
@@ -330,38 +379,12 @@ export function normalizeResponsesMessageContent(role: string, content: unknown)
return [];
}
const ALLOWED_RESPONSES_FIELDS = new Set([
'model',
'input',
'instructions',
'max_output_tokens',
const RESPONSES_COMPATIBILITY_FILTER_FIELDS = new Set([
'max_completion_tokens',
'temperature',
'top_p',
'truncation',
'tools',
'tool_choice',
'parallel_tool_calls',
'metadata',
'reasoning',
'store',
'safety_identifier',
'stream',
'stream_options',
'user',
'max_tool_calls',
'prompt_cache_key',
'prompt_cache_retention',
'background',
'previous_response_id',
'text',
'audio',
'include',
'messages',
'prompt',
'response_format',
'service_tier',
'top_logprobs',
'stop',
'n',
'verbosity',
]);
export function sanitizeResponsesBodyForProxy(
@@ -397,11 +420,9 @@ export function sanitizeResponsesBodyForProxy(
defaultEncryptedReasoningInclude: options?.defaultEncryptedReasoningInclude,
});
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(normalized)) {
if (!ALLOWED_RESPONSES_FIELDS.has(key)) continue;
if (key === 'max_completion_tokens') continue;
sanitized[key] = value;
const sanitized: Record<string, unknown> = { ...normalized };
for (const key of RESPONSES_COMPATIBILITY_FILTER_FIELDS) {
delete sanitized[key];
}
const maxOutputTokens = toFiniteNumber(normalized.max_output_tokens);
@@ -427,6 +448,7 @@ export function convertOpenAiBodyToResponsesBody(
const rawMessages = Array.isArray(openaiBody.messages) ? openaiBody.messages : [];
const systemContents: string[] = [];
const inputItems: Array<Record<string, unknown>> = [];
const toolNameMap = buildShortToolNameMap(collectOpenAiToolNames(openaiBody));
for (const item of rawMessages) {
if (!isRecord(item)) continue;
@@ -466,7 +488,7 @@ export function convertOpenAiBodyToResponsesBody(
inputItems.push({
type: 'function_call',
call_id: callId,
name,
name: getShortToolName(name, toolNameMap),
arguments: argumentsValue,
});
}
@@ -520,7 +542,7 @@ export function convertOpenAiBodyToResponsesBody(
if (openaiBody.metadata !== undefined) body.metadata = openaiBody.metadata;
if (openaiBody.reasoning !== undefined) body.reasoning = openaiBody.reasoning;
if (openaiBody.parallel_tool_calls !== undefined) body.parallel_tool_calls = openaiBody.parallel_tool_calls;
if (openaiBody.tools !== undefined) body.tools = convertOpenAiToolsToResponses(openaiBody.tools);
if (openaiBody.tools !== undefined) body.tools = convertOpenAiToolsToResponses(openaiBody.tools, toolNameMap);
if (openaiBody.safety_identifier !== undefined) body.safety_identifier = openaiBody.safety_identifier;
if (openaiBody.max_tool_calls !== undefined) body.max_tool_calls = openaiBody.max_tool_calls;
if (openaiBody.prompt_cache_key !== undefined) body.prompt_cache_key = openaiBody.prompt_cache_key;
@@ -546,7 +568,7 @@ export function convertOpenAiBodyToResponsesBody(
body.text = textConfig;
}
const responsesToolChoice = convertOpenAiToolChoiceToResponses(openaiBody.tool_choice);
const responsesToolChoice = convertOpenAiToolChoiceToResponses(openaiBody.tool_choice, toolNameMap);
if (responsesToolChoice !== undefined) body.tool_choice = responsesToolChoice;
return normalizeResponsesBodyForCompatibility(