cc-haha/src/server/__tests__/proxy-transform.test.ts
2026-05-04 16:45:24 +08:00

493 lines
16 KiB
TypeScript

/**
* Unit tests for proxy protocol transformation
*/
import { describe, test, expect } from 'bun:test'
import { anthropicToOpenaiChat } from '../proxy/transform/anthropicToOpenaiChat.js'
import { anthropicToOpenaiResponses } from '../proxy/transform/anthropicToOpenaiResponses.js'
import { openaiChatToAnthropic } from '../proxy/transform/openaiChatToAnthropic.js'
import { openaiResponsesToAnthropic } from '../proxy/transform/openaiResponsesToAnthropic.js'
import type { AnthropicRequest, OpenAIChatResponse, OpenAIResponsesResponse } from '../proxy/transform/types.js'
// ─── anthropicToOpenaiChat ──────────────────────────────────────
describe('anthropicToOpenaiChat', () => {
test('basic text message', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello' }],
}
const result = anthropicToOpenaiChat(req)
expect(result.model).toBe('gpt-4')
expect(result.max_tokens).toBeUndefined()
expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }])
})
test('system prompt string', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
system: 'You are helpful',
messages: [{ role: 'user', content: 'Hi' }],
}
const result = anthropicToOpenaiChat(req)
expect(result.messages[0]).toEqual({ role: 'system', content: 'You are helpful' })
expect(result.messages[1]).toEqual({ role: 'user', content: 'Hi' })
})
test('system prompt array', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
system: [{ type: 'text', text: 'Part 1' }, { type: 'text', text: 'Part 2' }],
messages: [{ role: 'user', content: 'Hi' }],
}
const result = anthropicToOpenaiChat(req)
expect(result.messages[0]).toEqual({ role: 'system', content: 'Part 1\nPart 2' })
})
test('stop_sequences → stop', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
stop_sequences: ['END', 'STOP'],
messages: [{ role: 'user', content: 'Hi' }],
}
const result = anthropicToOpenaiChat(req)
expect(result.stop).toEqual(['END', 'STOP'])
})
test('tools conversion', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
tools: [{
name: 'get_weather',
description: 'Get weather',
input_schema: { type: 'object', properties: { city: { type: 'string' } } },
}],
}
const result = anthropicToOpenaiChat(req)
expect(result.tools).toHaveLength(1)
expect(result.tools![0].type).toBe('function')
expect(result.tools![0].function.name).toBe('get_weather')
expect(result.tools![0].function.parameters).toEqual({ type: 'object', properties: { city: { type: 'string' } } })
})
test('filters BatchTool', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
tools: [
{ name: 'BatchTool', input_schema: {} },
{ name: 'real_tool', input_schema: {} },
],
}
const result = anthropicToOpenaiChat(req)
expect(result.tools).toHaveLength(1)
expect(result.tools![0].function.name).toBe('real_tool')
})
test('tool_choice conversion', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
tool_choice: { type: 'any' },
}
const result = anthropicToOpenaiChat(req)
expect(result.tool_choice).toBe('required')
})
test('tool_choice type=tool', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
tool_choice: { type: 'tool', name: 'get_weather' },
}
const result = anthropicToOpenaiChat(req)
expect(result.tool_choice).toEqual({ type: 'function', function: { name: 'get_weather' } })
})
test('thinking budget → reasoning_effort', () => {
const lowReq: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
thinking: { type: 'enabled', budget_tokens: 512 },
}
expect(anthropicToOpenaiChat(lowReq).reasoning_effort).toBe('low')
const medReq: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
thinking: { type: 'enabled', budget_tokens: 4096 },
}
expect(anthropicToOpenaiChat(medReq).reasoning_effort).toBe('medium')
const highReq: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
thinking: { type: 'enabled', budget_tokens: 16000 },
}
expect(anthropicToOpenaiChat(highReq).reasoning_effort).toBe('high')
})
test('assistant message with tool_use', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{
role: 'assistant',
content: [
{ type: 'text', text: 'Let me check' },
{ type: 'tool_use', id: 'tc_1', name: 'get_weather', input: { city: 'NYC' } },
],
}],
}
const result = anthropicToOpenaiChat(req)
const msg = result.messages[0]
expect(msg.role).toBe('assistant')
expect(msg.content).toBe('Let me check')
expect(msg.tool_calls).toHaveLength(1)
expect(msg.tool_calls![0].id).toBe('tc_1')
expect(msg.tool_calls![0].function.name).toBe('get_weather')
expect(msg.tool_calls![0].function.arguments).toBe('{"city":"NYC"}')
})
test('user message with tool_result', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{
role: 'user',
content: [
{ type: 'tool_result', tool_use_id: 'tc_1', content: 'Sunny, 72°F' },
],
}],
}
const result = anthropicToOpenaiChat(req)
expect(result.messages[0].role).toBe('tool')
expect(result.messages[0].tool_call_id).toBe('tc_1')
expect(result.messages[0].content).toBe('Sunny, 72°F')
})
test('image content conversion', () => {
const req: AnthropicRequest = {
model: 'gpt-4',
max_tokens: 100,
messages: [{
role: 'user',
content: [
{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'abc123' } },
],
}],
}
const result = anthropicToOpenaiChat(req)
const content = result.messages[0].content as Array<{ type: string; image_url?: { url: string } }>
expect(content[0].type).toBe('image_url')
expect(content[0].image_url!.url).toBe('data:image/png;base64,abc123')
})
})
// ─── openaiChatToAnthropic ──────────────────────────────────────
describe('openaiChatToAnthropic', () => {
test('basic text response', () => {
const res: OpenAIChatResponse = {
id: 'chatcmpl-1',
object: 'chat.completion',
created: 1234567890,
model: 'gpt-4',
choices: [{
index: 0,
message: { role: 'assistant', content: 'Hello!' },
finish_reason: 'stop',
}],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
}
const result = openaiChatToAnthropic(res, 'gpt-4')
expect(result.type).toBe('message')
expect(result.role).toBe('assistant')
expect(result.content).toEqual([{ type: 'text', text: 'Hello!' }])
expect(result.stop_reason).toBe('end_turn')
expect(result.usage.input_tokens).toBe(10)
expect(result.usage.output_tokens).toBe(5)
})
test('tool_calls response', () => {
const res: OpenAIChatResponse = {
id: 'chatcmpl-2',
object: 'chat.completion',
created: 1234567890,
model: 'gpt-4',
choices: [{
index: 0,
message: {
role: 'assistant',
content: null,
tool_calls: [{
id: 'call_1',
type: 'function',
function: { name: 'get_weather', arguments: '{"city":"NYC"}' },
}],
},
finish_reason: 'tool_calls',
}],
}
const result = openaiChatToAnthropic(res, 'gpt-4')
expect(result.stop_reason).toBe('tool_use')
expect(result.content).toHaveLength(1)
expect(result.content[0].type).toBe('tool_use')
if (result.content[0].type === 'tool_use') {
expect(result.content[0].id).toBe('call_1')
expect(result.content[0].name).toBe('get_weather')
expect(result.content[0].input).toEqual({ city: 'NYC' })
}
})
test('finish_reason mapping', () => {
const make = (reason: string) => ({
id: 'x', object: 'chat.completion', created: 0, model: 'gpt-4',
choices: [{ index: 0, message: { role: 'assistant', content: 'hi' }, finish_reason: reason }],
} as OpenAIChatResponse)
expect(openaiChatToAnthropic(make('stop'), 'gpt-4').stop_reason).toBe('end_turn')
expect(openaiChatToAnthropic(make('length'), 'gpt-4').stop_reason).toBe('max_tokens')
expect(openaiChatToAnthropic(make('tool_calls'), 'gpt-4').stop_reason).toBe('tool_use')
expect(openaiChatToAnthropic(make('content_filter'), 'gpt-4').stop_reason).toBe('end_turn')
})
test('empty choices', () => {
const res: OpenAIChatResponse = {
id: 'x', object: 'chat.completion', created: 0, model: 'gpt-4',
choices: [],
}
const result = openaiChatToAnthropic(res, 'gpt-4')
expect(result.content).toEqual([{ type: 'text', text: '' }])
expect(result.stop_reason).toBe('end_turn')
})
test('cached tokens mapping', () => {
const res: OpenAIChatResponse = {
id: 'x', object: 'chat.completion', created: 0, model: 'gpt-4',
choices: [{ index: 0, message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }],
usage: {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
prompt_tokens_details: { cached_tokens: 80 },
},
}
const result = openaiChatToAnthropic(res, 'gpt-4')
expect(result.usage.cache_read_input_tokens).toBe(80)
})
})
// ─── anthropicToOpenaiResponses ─────────────────────────────────
describe('anthropicToOpenaiResponses', () => {
test('basic message', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 1024,
system: 'Be helpful',
messages: [{ role: 'user', content: 'Hello' }],
}
const result = anthropicToOpenaiResponses(req)
expect(result.model).toBe('gpt-4o')
expect(result.instructions).toBe('Be helpful')
expect(result.store).toBe(false)
expect(result.tools).toBeUndefined()
expect(result.max_output_tokens).toBeUndefined()
expect(result.input).toEqual([{ type: 'message', role: 'user', content: 'Hello' }])
})
test('tools conversion uses top-level name', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
tools: [{
name: 'get_weather',
description: 'Get weather',
input_schema: { type: 'object', properties: { city: { type: 'string' } } },
}],
}
const result = anthropicToOpenaiResponses(req)
expect(result.tools).toHaveLength(1)
expect(result.tools![0]).toEqual({
type: 'function',
name: 'get_weather',
description: 'Get weather',
parameters: { type: 'object', properties: { city: { type: 'string' } } },
})
})
test('tool_use lifted to function_call', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 100,
messages: [{
role: 'assistant',
content: [
{ type: 'tool_use', id: 'tc_1', name: 'search', input: { q: 'test' } },
],
}],
}
const result = anthropicToOpenaiResponses(req)
const fc = result.input.find((i) => i.type === 'function_call')
expect(fc).toBeDefined()
if (fc && fc.type === 'function_call') {
expect(fc.call_id).toBe('tc_1')
expect(fc.name).toBe('search')
expect(fc.arguments).toBe('{"q":"test"}')
}
})
test('tool_result lifted to function_call_output', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 100,
messages: [{
role: 'user',
content: [
{ type: 'tool_result', tool_use_id: 'tc_1', content: 'found it' },
],
}],
}
const result = anthropicToOpenaiResponses(req)
const fco = result.input.find((i) => i.type === 'function_call_output')
expect(fco).toBeDefined()
if (fco && fco.type === 'function_call_output') {
expect(fco.call_id).toBe('tc_1')
expect(fco.output).toBe('found it')
}
})
test('thinking → reasoning', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
thinking: { type: 'enabled', budget_tokens: 10000 },
}
const result = anthropicToOpenaiResponses(req)
expect(result.reasoning).toEqual({ effort: 'high' })
})
test('stop_sequences dropped', () => {
const req: AnthropicRequest = {
model: 'gpt-4o',
max_tokens: 100,
messages: [{ role: 'user', content: 'Hi' }],
stop_sequences: ['END'],
}
const result = anthropicToOpenaiResponses(req)
expect((result as Record<string, unknown>).stop).toBeUndefined()
expect((result as Record<string, unknown>).stop_sequences).toBeUndefined()
})
})
// ─── openaiResponsesToAnthropic ─────────────────────────────────
describe('openaiResponsesToAnthropic', () => {
test('basic text response', () => {
const res: OpenAIResponsesResponse = {
id: 'resp_1',
object: 'response',
created_at: 1234567890,
model: 'gpt-4o',
status: 'completed',
output: [{
type: 'message',
role: 'assistant',
content: [{ type: 'output_text', text: 'Hello!' }],
}],
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
}
const result = openaiResponsesToAnthropic(res, 'gpt-4o')
expect(result.content).toEqual([{ type: 'text', text: 'Hello!' }])
expect(result.stop_reason).toBe('end_turn')
expect(result.usage.input_tokens).toBe(10)
expect(result.usage.output_tokens).toBe(5)
})
test('function_call → tool_use', () => {
const res: OpenAIResponsesResponse = {
id: 'resp_2',
object: 'response',
created_at: 0,
model: 'gpt-4o',
status: 'completed',
output: [{
type: 'function_call',
id: 'fc_1',
call_id: 'call_1',
name: 'search',
arguments: '{"q":"test"}',
}],
}
const result = openaiResponsesToAnthropic(res, 'gpt-4o')
expect(result.stop_reason).toBe('tool_use')
expect(result.content[0].type).toBe('tool_use')
if (result.content[0].type === 'tool_use') {
expect(result.content[0].id).toBe('call_1')
expect(result.content[0].input).toEqual({ q: 'test' })
}
})
test('reasoning → thinking', () => {
const res: OpenAIResponsesResponse = {
id: 'resp_3',
object: 'response',
created_at: 0,
model: 'gpt-4o',
status: 'completed',
output: [
{ type: 'reasoning', id: 'r_1', summary: [{ type: 'text', text: 'Thinking...' }] },
{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'Result' }] },
],
}
const result = openaiResponsesToAnthropic(res, 'gpt-4o')
expect(result.content).toHaveLength(2)
expect(result.content[0].type).toBe('thinking')
if (result.content[0].type === 'thinking') {
expect(result.content[0].thinking).toBe('Thinking...')
}
expect(result.content[1].type).toBe('text')
})
test('status incomplete → max_tokens', () => {
const res: OpenAIResponsesResponse = {
id: 'resp_4',
object: 'response',
created_at: 0,
model: 'gpt-4o',
status: 'incomplete',
output: [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'partial' }] }],
}
const result = openaiResponsesToAnthropic(res, 'gpt-4o')
expect(result.stop_reason).toBe('max_tokens')
})
test('empty output', () => {
const res: OpenAIResponsesResponse = {
id: 'resp_5',
object: 'response',
created_at: 0,
model: 'gpt-4o',
status: 'completed',
output: [],
}
const result = openaiResponsesToAnthropic(res, 'gpt-4o')
expect(result.content).toEqual([{ type: 'text', text: '' }])
})
})