forked from boos/BoosAPI
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:
@@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user