diff --git a/src/server/__tests__/proxy-transform.test.ts b/src/server/__tests__/proxy-transform.test.ts index 8ca4263d..26433ee5 100644 --- a/src/server/__tests__/proxy-transform.test.ts +++ b/src/server/__tests__/proxy-transform.test.ts @@ -139,6 +139,18 @@ describe('anthropicToOpenaiChat', () => { expect(anthropicToOpenaiChat(highReq).reasoning_effort).toBe('high') }) + test('passes explicit thinking toggle for DeepSeek-compatible chat proxies', () => { + const req: AnthropicRequest = { + model: 'deepseek-v4-flash', + max_tokens: 100, + messages: [{ role: 'user', content: 'Hi' }], + thinking: { type: 'disabled' }, + } + + expect(anthropicToOpenaiChat(req).thinking).toBeUndefined() + expect(anthropicToOpenaiChat(req, { passThinkingToggle: true }).thinking).toEqual({ type: 'disabled' }) + }) + test('assistant message with tool_use', () => { const req: AnthropicRequest = { model: 'gpt-4', @@ -161,6 +173,32 @@ describe('anthropicToOpenaiChat', () => { expect(msg.tool_calls![0].function.arguments).toBe('{"city":"NYC"}') }) + test('round-trips assistant thinking as reasoning_content for DeepSeek tool-call history', () => { + const req: AnthropicRequest = { + model: 'deepseek-v4-pro', + max_tokens: 100, + messages: [{ + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'Need the date first. ' }, + { type: 'thinking', thinking: 'Then call weather.' }, + { type: 'text', text: 'Let me check that.' }, + { type: 'tool_use', id: 'call_1', name: 'get_weather', input: { location: 'Hangzhou' } }, + ], + }], + } + + const defaultResult = anthropicToOpenaiChat(req) + expect(defaultResult.messages[0].reasoning_content).toBeUndefined() + + const result = anthropicToOpenaiChat(req, { roundTripReasoningContent: true }) + const msg = result.messages[0] + expect(msg.role).toBe('assistant') + expect(msg.content).toBe('Let me check that.') + expect(msg.reasoning_content).toBe('Need the date first. Then call weather.') + expect(msg.tool_calls?.[0].id).toBe('call_1') + }) + test('user message with tool_result', () => { const req: AnthropicRequest = { model: 'gpt-4', diff --git a/src/server/proxy/handler.ts b/src/server/proxy/handler.ts index 88e00443..f3047af5 100644 --- a/src/server/proxy/handler.ts +++ b/src/server/proxy/handler.ts @@ -109,7 +109,11 @@ async function handleOpenaiChat( apiKey: string, isStream: boolean, ): Promise { - const transformed = anthropicToOpenaiChat(body) + const deepSeekCompatible = shouldUseDeepSeekReasoningCompat(baseUrl) + const transformed = anthropicToOpenaiChat(body, { + roundTripReasoningContent: deepSeekCompatible, + passThinkingToggle: deepSeekCompatible, + }) const url = `${baseUrl}/v1/chat/completions` const upstream = await fetch(url, { @@ -160,6 +164,13 @@ async function handleOpenaiChat( return Response.json(anthropicResponse) } +function shouldUseDeepSeekReasoningCompat(baseUrl: string): boolean { + return ( + /(^|[./-])deepseek([./-]|$)/i.test(baseUrl) || + /(^|[./-])opencode\.ai([:/]|$)/i.test(baseUrl) + ) +} + async function handleOpenaiResponses( body: AnthropicRequest, baseUrl: string, diff --git a/src/server/proxy/transform/anthropicToOpenaiChat.ts b/src/server/proxy/transform/anthropicToOpenaiChat.ts index 47802070..a637df58 100644 --- a/src/server/proxy/transform/anthropicToOpenaiChat.ts +++ b/src/server/proxy/transform/anthropicToOpenaiChat.ts @@ -18,7 +18,10 @@ import type { /** * Convert Anthropic Messages request to OpenAI Chat Completions request. */ -export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest { +export function anthropicToOpenaiChat( + body: AnthropicRequest, + options: { roundTripReasoningContent?: boolean; passThinkingToggle?: boolean } = {}, +): OpenAIChatRequest { const messages: OpenAIChatMessage[] = [] // Convert system prompt @@ -33,7 +36,7 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest // Convert messages for (const msg of body.messages) { - convertMessage(msg, messages) + convertMessage(msg, messages, options) } // Build request @@ -85,12 +88,19 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest } else if (body.thinking.type === 'enabled') { result.reasoning_effort = 'high' } + if (options.passThinkingToggle) { + result.thinking = { type: body.thinking.type } + } } return result } -function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[]): void { +function convertMessage( + msg: AnthropicMessage, + output: OpenAIChatMessage[], + options: { roundTripReasoningContent?: boolean }, +): void { const content = msg.content // Simple string content @@ -108,7 +118,7 @@ function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[]): voi if (msg.role === 'user') { convertUserMessage(content, output) } else { - convertAssistantMessage(content, output) + convertAssistantMessage(content, output, options) } } @@ -147,13 +157,20 @@ function convertUserMessage(blocks: AnthropicContentBlock[], output: OpenAIChatM } } -function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAIChatMessage[]): void { +function convertAssistantMessage( + blocks: AnthropicContentBlock[], + output: OpenAIChatMessage[], + options: { roundTripReasoningContent?: boolean }, +): void { let textContent = '' + let reasoningContent = '' const toolCalls: OpenAIToolCall[] = [] for (const block of blocks) { if (block.type === 'text') { textContent += block.text + } else if (block.type === 'thinking' && options.roundTripReasoningContent) { + reasoningContent += block.thinking } else if (block.type === 'tool_use') { toolCalls.push({ id: block.id, @@ -164,7 +181,6 @@ function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAI }, }) } - // Skip thinking blocks — no OpenAI equivalent } const msg: OpenAIChatMessage = { @@ -175,6 +191,9 @@ function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAI if (toolCalls.length > 0) { msg.tool_calls = toolCalls } + if (reasoningContent) { + msg.reasoning_content = reasoningContent + } output.push(msg) } diff --git a/src/server/proxy/transform/types.ts b/src/server/proxy/transform/types.ts index 6196989f..fd6e6de9 100644 --- a/src/server/proxy/transform/types.ts +++ b/src/server/proxy/transform/types.ts @@ -10,6 +10,7 @@ export type OpenAIChatMessage = { role: 'system' | 'user' | 'assistant' | 'tool' content?: string | OpenAIChatContentPart[] | null name?: string + reasoning_content?: string tool_calls?: OpenAIToolCall[] tool_call_id?: string } @@ -48,6 +49,7 @@ export type OpenAIChatRequest = { tools?: OpenAITool[] tool_choice?: unknown reasoning_effort?: 'low' | 'medium' | 'high' + thinking?: { type: string } } export type OpenAIChatResponse = {