The old claude-3-5-haiku-20241022 model ID returns 404 from the Anthropic API. Updated proxy mapping and test to use claude-haiku-4-5-20251001. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
6.0 KiB
TypeScript
189 lines
6.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// Mock proxy request/response logic without actually spawning processes
|
|
describe('Proxy Integration', () => {
|
|
describe('SSE streaming format', () => {
|
|
it('should produce valid SSE content_block_delta events', () => {
|
|
const text = 'Hello, world!'
|
|
const sseData = {
|
|
type: 'content_block_delta',
|
|
delta: { type: 'text_delta', text },
|
|
}
|
|
const sseString = `data: ${JSON.stringify(sseData)}\n\n`
|
|
|
|
expect(sseString).toMatch(/^data: /)
|
|
expect(sseString).toMatch(/\n\n$/)
|
|
|
|
const parsed = JSON.parse(sseString.replace('data: ', '').trim())
|
|
expect(parsed.type).toBe('content_block_delta')
|
|
expect(parsed.delta.type).toBe('text_delta')
|
|
expect(parsed.delta.text).toBe(text)
|
|
})
|
|
|
|
it('should produce valid DONE event', () => {
|
|
const done = 'data: [DONE]\n\n'
|
|
expect(done).toBe('data: [DONE]\n\n')
|
|
})
|
|
|
|
it('should produce valid error events', () => {
|
|
const errData = {
|
|
type: 'error',
|
|
error: { message: 'Anthropic API 401: Unauthorized' },
|
|
}
|
|
const sseString = `data: ${JSON.stringify(errData)}\n\n`
|
|
const parsed = JSON.parse(sseString.replace('data: ', '').trim())
|
|
expect(parsed.type).toBe('error')
|
|
expect(parsed.error.message).toContain('401')
|
|
})
|
|
})
|
|
|
|
describe('Model mapping', () => {
|
|
function mapModelToApi(model: string): string {
|
|
if (model?.includes('opus')) return 'claude-opus-4-20250514'
|
|
if (model?.includes('haiku')) return 'claude-haiku-4-5-20251001'
|
|
return 'claude-sonnet-4-20250514'
|
|
}
|
|
|
|
it('should map sonnet model correctly', () => {
|
|
expect(mapModelToApi('sonnet')).toBe('claude-sonnet-4-20250514')
|
|
expect(mapModelToApi('claude-sonnet')).toBe('claude-sonnet-4-20250514')
|
|
})
|
|
|
|
it('should map opus model correctly', () => {
|
|
expect(mapModelToApi('opus')).toBe('claude-opus-4-20250514')
|
|
expect(mapModelToApi('claude-opus')).toBe('claude-opus-4-20250514')
|
|
})
|
|
|
|
it('should map haiku model correctly', () => {
|
|
expect(mapModelToApi('haiku')).toBe('claude-haiku-4-5-20251001')
|
|
})
|
|
|
|
it('should default to sonnet for unknown models', () => {
|
|
expect(mapModelToApi('unknown')).toBe('claude-sonnet-4-20250514')
|
|
})
|
|
})
|
|
|
|
describe('Request validation', () => {
|
|
it('should reject non-POST requests', () => {
|
|
const method = 'GET' as string
|
|
const isValid = method === 'POST'
|
|
expect(isValid).toBe(false)
|
|
})
|
|
|
|
it('should reject unknown paths', () => {
|
|
const validPaths = ['/v1/messages', '/v1/openrouter']
|
|
expect(validPaths.includes('/v1/unknown')).toBe(false)
|
|
expect(validPaths.includes('/v1/messages')).toBe(true)
|
|
expect(validPaths.includes('/v1/openrouter')).toBe(true)
|
|
})
|
|
|
|
it('should parse request body correctly', () => {
|
|
const body = JSON.stringify({
|
|
model: 'sonnet',
|
|
messages: [{ role: 'user', content: 'Hello' }],
|
|
system: 'You are helpful.',
|
|
webSearch: true,
|
|
})
|
|
const parsed = JSON.parse(body)
|
|
expect(parsed.model).toBe('sonnet')
|
|
expect(parsed.messages).toHaveLength(1)
|
|
expect(parsed.system).toBe('You are helpful.')
|
|
expect(parsed.webSearch).toBe(true)
|
|
})
|
|
|
|
it('should handle malformed JSON', () => {
|
|
const badBody = 'not json'
|
|
expect(() => JSON.parse(badBody)).toThrow()
|
|
})
|
|
})
|
|
|
|
describe('Tool use round-trips', () => {
|
|
it('should format search_web tool correctly', () => {
|
|
const tool = {
|
|
name: 'search_web',
|
|
description: 'Search the web for current information.',
|
|
input_schema: {
|
|
type: 'object',
|
|
properties: {
|
|
query: { type: 'string', description: 'Search query' },
|
|
},
|
|
required: ['query'],
|
|
},
|
|
}
|
|
expect(tool.name).toBe('search_web')
|
|
expect(tool.input_schema.properties.query.type).toBe('string')
|
|
})
|
|
|
|
it('should construct tool_result messages correctly', () => {
|
|
const toolResult = {
|
|
type: 'tool_result',
|
|
tool_use_id: 'toolu_123',
|
|
content: '1. [Bitcoin price](https://example.com) — Current price is...',
|
|
}
|
|
expect(toolResult.type).toBe('tool_result')
|
|
expect(toolResult.tool_use_id).toBe('toolu_123')
|
|
expect(toolResult.content).toContain('Bitcoin')
|
|
})
|
|
|
|
it('should limit tool rounds to 5', () => {
|
|
const maxToolRounds = 5
|
|
let rounds = 0
|
|
while (rounds < maxToolRounds) {
|
|
rounds++
|
|
}
|
|
expect(rounds).toBe(5)
|
|
})
|
|
})
|
|
|
|
describe('Error handling', () => {
|
|
it('should handle 401 unauthorized', () => {
|
|
const status = 401
|
|
const errMsg = `Anthropic API ${status}: Unauthorized`
|
|
expect(errMsg).toContain('401')
|
|
})
|
|
|
|
it('should handle 429 rate limit', () => {
|
|
const status = 429
|
|
const errMsg = `Anthropic API ${status}: Rate limited`
|
|
expect(errMsg).toContain('429')
|
|
})
|
|
|
|
it('should handle 500 server error', () => {
|
|
const status = 500
|
|
const errMsg = `Anthropic API ${status}: Internal Server Error`
|
|
expect(errMsg).toContain('500')
|
|
})
|
|
})
|
|
|
|
describe('OAuth token detection', () => {
|
|
const isOAuthToken = (s: string) => /^sk-ant-oat/.test(s)
|
|
|
|
it('should detect OAuth tokens', () => {
|
|
expect(isOAuthToken('sk-ant-oat-abc123')).toBe(true)
|
|
})
|
|
|
|
it('should not flag API keys as OAuth', () => {
|
|
expect(isOAuthToken('sk-ant-api03-abc123')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Client disconnect handling', () => {
|
|
it('should track client disconnection', () => {
|
|
const state = { clientDisconnected: false }
|
|
// Simulate disconnect
|
|
state.clientDisconnected = true
|
|
expect(state.clientDisconnected).toBe(true)
|
|
})
|
|
|
|
it('should not write after disconnect', () => {
|
|
const clientDisconnected = true
|
|
const writes: string[] = []
|
|
const write = (data: string) => {
|
|
if (!clientDisconnected) writes.push(data)
|
|
}
|
|
write('should not appear')
|
|
expect(writes).toHaveLength(0)
|
|
})
|
|
})
|
|
})
|