fix(proxy): harden Anthropic request transformation semantics

- enforce strict tool_result ordering and pairing against assistant tool_use ids
- reject tool_result image payloads that cannot map to OpenAI tool messages
- preserve raw tool schemas on the /v1/messages proxy path instead of silently tightening them
- forward Anthropic tool_choice semantics and cover adaptive routing plus upstream payload checks
This commit is contained in:
Tam Nhu Tran committed 2026-04-18 19:39:13 -04:00
1 parent 22ab58b02e
commit ebc92194bb
4 files changed
+422 -48

No files matched your search

@@ -116,13 +116,63 @@ describe('openai proxy messages endpoint', () => {
const parsedUpstream = upstreamBody as {
messages?: Array<{ role: string; content: string }>;
tool_choice?: unknown;
tools?: Array<{ type: string; function: { name: string } }>;
};
expect(parsedUpstream.messages?.[0]).toEqual({ role: 'user', content: 'Find docs' });
expect(parsedUpstream.tool_choice).toBe('auto');
expect(parsedUpstream.tools?.[0]?.type).toBe('function');
expect(parsedUpstream.tools?.[0]?.function.name).toBe('search');
});
it('preserves tool schemas and forwards explicit tool_choice semantics upstream', async () => {
const response = await requestProxy({
model: 'hf-model',
messages: [{ role: 'user', content: [{ type: 'text', text: 'Search docs' }] }],
tools: [
{
name: 'search',
description: 'Search docs',
input_schema: {
type: 'object',
properties: {
q: { type: 'string', pattern: '^[a-z]+$' },
},
required: ['q'],
additionalProperties: true,
},
},
],
tool_choice: {
type: 'tool',
name: 'search',
disable_parallel_tool_use: true,
},
});
expect(response.status).toBe(200);
const parsedUpstream = upstreamBody as {
tool_choice?: unknown;
parallel_tool_calls?: boolean;
tools?: Array<{ type: string; function: { parameters: Record<string, unknown> } }>;
};
expect(parsedUpstream.tool_choice).toEqual({
type: 'function',
function: { name: 'search' },
});
expect(parsedUpstream.parallel_tool_calls).toBe(false);
expect(parsedUpstream.tools?.[0]?.function.parameters).toEqual({
type: 'object',
properties: {
q: { type: 'string', pattern: '^[a-z]+$' },
},
required: ['q'],
additionalProperties: true,
});
});
it('falls back to Anthropic JSON for non-streaming requests', async () => {
const response = await requestProxy({
model: 'hf-model',
@@ -155,6 +205,24 @@ describe('openai proxy messages endpoint', () => {
expect(body.error?.message).toContain('Invalid JSON');
});
it('returns invalid_request_error for orphan tool_result blocks', async () => {
const response = await requestProxy({
model: 'hf-model',
messages: [
{
role: 'user',
content: [{ type: 'tool_result', tool_use_id: 'toolu_orphan', content: 'orphan' }],
},
],
});
const body = (await response.json()) as { error?: { type?: string; message?: string } };
expect(response.status).toBe(400);
expect(body.error?.type).toBe('invalid_request_error');
expect(body.error?.message).toContain('tool_result requires a preceding assistant tool_use');
});
it('rejects requests without the local proxy auth token', async () => {
const response = await fetch(`http://127.0.0.1:${proxyPort}/v1/messages`, {
method: 'POST',
@@ -214,4 +214,75 @@ describe('openai proxy request routing', () => {
expect(hits).toEqual(['thinker']);
expect(bodies[0]?.body).toMatchObject({ model: 'deepseek-reasoner' });
});
it('routes adaptive thinking requests through the configured think scenario', async () => {
const primaryPort = await getPort();
const thinkPort = await getPort();
const hits: string[] = [];
const bodies: Array<{ label: string; body: unknown }> = [];
await startMockUpstream(primaryPort, 'primary', hits, bodies);
await startMockUpstream(thinkPort, 'thinker', hits, bodies);
const primarySettings = writeSettings('hf', {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${primaryPort}`,
ANTHROPIC_AUTH_TOKEN: 'hf_token',
ANTHROPIC_MODEL: 'hf-default',
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
});
const thinkSettings = writeSettings('thinker', {
ANTHROPIC_BASE_URL: `http://127.0.0.1:${thinkPort}`,
ANTHROPIC_AUTH_TOKEN: 'think_token',
ANTHROPIC_MODEL: 'deepseek-reasoner',
CCS_DROID_PROVIDER: 'generic-chat-completion-api',
});
fs.writeFileSync(
path.join(tempDir, '.ccs', 'config.json'),
JSON.stringify(
{
profiles: { hf: primarySettings, thinker: thinkSettings },
proxy: {
routing: {
think: 'thinker:deepseek-reasoner',
},
},
},
null,
2
),
'utf8'
);
const profile: OpenAICompatProfileConfig = {
profileName: 'hf',
settingsPath: primarySettings,
baseUrl: `http://127.0.0.1:${primaryPort}`,
apiKey: 'hf_token',
provider: 'generic-chat-completion-api',
model: 'hf-default',
};
proxyServer = startOpenAICompatProxyServer({
profile,
port: proxyPort,
authToken: 'test-proxy-token',
});
const response = await requestProxy({
model: 'hf-default',
thinking: { type: 'adaptive' },
output_config: { effort: 'max' },
messages: [{ role: 'user', content: 'think adaptively' }],
});
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
content: [{ type: 'text', text: 'Reply from thinker' }],
});
expect(hits).toEqual(['thinker']);
expect(bodies[0]?.body).toMatchObject({
model: 'deepseek-reasoner',
reasoning_effort: 'high',
reasoning: { enabled: true, effort: 'high' },
});
});
});