Harden cross-protocol request compatibility

Several downstream clients send non-cache compatibility fields across Chat, Responses, Anthropic Messages, and Responses WebSocket surfaces. The proxy now normalizes legacy functions/function_call, maps web_search tools across protocols, rejects unsupported HTTP Responses continuation shapes before upstream dispatch, and applies configurable service_tier policy consistently before requests leave the server.

Constraint: Publication flow keeps day-to-day commits on dev and opens main PRs from a release branch.

Constraint: No new dependencies; web_search-only simulation reuses the existing local search route.

Rejected: Forward external HTTP previous_response_id to strict upstreams | it produces ambiguous continuation failures and stale retry behavior outside WebSocket state.

Rejected: Add a dedicated search dependency | existing /v1/search route is sufficient for synthetic server-tool responses.

Confidence: high

Scope-risk: moderate

Directive: Keep Responses HTTP continuation checks aligned with WebSocket continuation support before relaxing these guards.

Tested: npm run typecheck:server

Tested: npx vitest run --root . src/server/proxy-core/responsesPreflight.test.ts src/server/proxy-core/serviceTierPolicy.test.ts src/server/test-fixtures/protocol/non-cache-field-matrix.test.ts src/server/transformers/openai/responses/conversion.test.ts src/server/transformers/anthropic/messages/conversion.test.ts src/server/routes/proxy/chat.stream.test.ts src/server/routes/proxy/responses.codex-oauth.test.ts

Tested: npx vitest run --root . src/server/routes/proxy/responses.websocket.test.ts

Tested: npm run repo:drift-check

Not-tested: Full application build and full Vitest suite

Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
cita-777
2026-05-08 00:30:48 +08:00
parent c94e3148dd
commit 2491a9af5a
18 changed files with 1701 additions and 71 deletions
@@ -0,0 +1,93 @@
import { describe, expect, it } from 'vitest';
import { convertOpenAiBodyToResponsesBody } from '../../transformers/openai/responses/conversion.js';
import {
convertAnthropicToolsToOpenAi,
convertOpenAiBodyToAnthropicMessagesBody,
convertOpenAiToolsToAnthropic,
} from '../../transformers/anthropic/messages/conversion.js';
import { convertClaudeRequestToOpenAiBody } from '../../transformers/shared/chatFormatsCore.js';
import { validateExternalResponsesHttpRequest } from '../../proxy-core/responsesPreflight.js';
import { applyOpenAiServiceTierPolicy } from '../../proxy-core/serviceTierPolicy.js';
describe('non-cache cross-protocol field matrix', () => {
it('audits web_search, legacy functions/function_call, continuation diagnostics, function outputs and service_tier', () => {
const legacyChatToResponses = convertOpenAiBodyToResponsesBody(
{
model: 'gpt-5',
messages: [
{ role: 'user', content: 'hello' },
{ role: 'function', name: 'legacy_tool', content: 'done' },
],
functions: [{ name: 'legacy_tool', parameters: { type: 'object' } }],
function_call: { name: 'legacy_tool' },
},
'gpt-5',
false,
);
expect(legacyChatToResponses).toMatchObject({
tools: [{ type: 'function', name: 'legacy_tool' }],
tool_choice: { type: 'function', name: 'legacy_tool' },
});
expect((legacyChatToResponses.input as any[]).some((item) => item.type === 'function_call_output')).toBe(true);
expect(convertOpenAiToolsToAnthropic([
{ type: 'web_search' },
{ type: 'google_search' },
])).toEqual([
{ type: 'web_search_20250305', name: 'web_search' },
{ type: 'web_search_20250305', name: 'web_search' },
]);
expect(convertAnthropicToolsToOpenAi([
{ type: 'web_search_20250305' },
])).toEqual([
{ type: 'web_search', name: 'web_search' },
]);
const claudeToChat = convertClaudeRequestToOpenAiBody({
model: 'claude-opus',
max_tokens: 256,
tools: [{ type: 'web_search_20250305' }],
messages: [{ role: 'user', content: 'search' }],
});
expect(claudeToChat.payload.tools).toEqual([{ type: 'web_search', name: 'web_search' }]);
const openAiToClaude = convertOpenAiBodyToAnthropicMessagesBody(
{
model: 'gpt-5',
messages: [{ role: 'user', content: 'search' }],
tools: [{ type: 'web_search' }],
},
'claude-opus',
false,
);
expect(openAiToClaude.tools).toEqual([{ type: 'web_search_20250305', name: 'web_search' }]);
const previousResponseDiagnostic = validateExternalResponsesHttpRequest({
model: 'gpt-5',
previous_response_id: 'msg_wrong',
input: 'hello',
});
expect(previousResponseDiagnostic.ok).toBe(false);
if (!previousResponseDiagnostic.ok) {
expect(previousResponseDiagnostic.payload.error.message).toContain('msg_*');
}
const functionOutputDiagnostic = validateExternalResponsesHttpRequest({
model: 'gpt-5',
input: [{ type: 'function_call_output', call_id: 'call_missing', output: 'done' }],
});
expect(functionOutputDiagnostic.ok).toBe(false);
if (!functionOutputDiagnostic.ok) {
expect(functionOutputDiagnostic.payload.error.message).toContain('Responses WebSocket v2');
}
expect(applyOpenAiServiceTierPolicy({
body: { model: 'gpt-5', service_tier: 'fast' },
})).toMatchObject({
ok: true,
body: { model: 'gpt-5', service_tier: 'priority' },
});
});
});