mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Preserve DeepSeek reasoning history for OpenAI chat proxies
DeepSeek-compatible OpenAI Chat providers require assistant reasoning_content to be replayed on later tool-call turns when thinking mode is active. The Anthropic-to-OpenAI chat transform now keeps that replay behavior behind an explicit compatibility option, and the proxy only enables it for DeepSeek/OpenCode base URLs so generic OpenAI-compatible providers keep their previous request shape. Constraint: DeepSeek thinking mode rejects follow-up requests when prior assistant reasoning_content is omitted Rejected: Trigger compatibility by model name | would affect unrelated OpenAI-compatible providers using DeepSeek model ids Confidence: high Scope-risk: narrow Directive: Do not broaden this compatibility gate without proving other OpenAI-compatible providers accept these non-standard fields Tested: Fixed-port Web UI harness with copied DeepSeek OpenAI Chat provider reproduced the 400 and passed through local proxy after the fix Tested: bun test src/server/__tests__/proxy-transform.test.ts Tested: bun run check:server Tested: git diff --check Not-tested: Direct OpenCode Go subscription endpoint, unavailable locally
This commit is contained in:
parent
dc0307f4c1
commit
63ef7b1a5b
@ -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',
|
||||
|
||||
@ -109,7 +109,11 @@ async function handleOpenaiChat(
|
||||
apiKey: string,
|
||||
isStream: boolean,
|
||||
): Promise<Response> {
|
||||
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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user