mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-08-01 16:43:37 +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')
|
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', () => {
|
test('assistant message with tool_use', () => {
|
||||||
const req: AnthropicRequest = {
|
const req: AnthropicRequest = {
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
@ -161,6 +173,32 @@ describe('anthropicToOpenaiChat', () => {
|
|||||||
expect(msg.tool_calls![0].function.arguments).toBe('{"city":"NYC"}')
|
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', () => {
|
test('user message with tool_result', () => {
|
||||||
const req: AnthropicRequest = {
|
const req: AnthropicRequest = {
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
|
|||||||
@ -109,7 +109,11 @@ async function handleOpenaiChat(
|
|||||||
apiKey: string,
|
apiKey: string,
|
||||||
isStream: boolean,
|
isStream: boolean,
|
||||||
): Promise<Response> {
|
): 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 url = `${baseUrl}/v1/chat/completions`
|
||||||
|
|
||||||
const upstream = await fetch(url, {
|
const upstream = await fetch(url, {
|
||||||
@ -160,6 +164,13 @@ async function handleOpenaiChat(
|
|||||||
return Response.json(anthropicResponse)
|
return Response.json(anthropicResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldUseDeepSeekReasoningCompat(baseUrl: string): boolean {
|
||||||
|
return (
|
||||||
|
/(^|[./-])deepseek([./-]|$)/i.test(baseUrl) ||
|
||||||
|
/(^|[./-])opencode\.ai([:/]|$)/i.test(baseUrl)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleOpenaiResponses(
|
async function handleOpenaiResponses(
|
||||||
body: AnthropicRequest,
|
body: AnthropicRequest,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|
|||||||
@ -18,7 +18,10 @@ import type {
|
|||||||
/**
|
/**
|
||||||
* Convert Anthropic Messages request to OpenAI Chat Completions request.
|
* 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[] = []
|
const messages: OpenAIChatMessage[] = []
|
||||||
|
|
||||||
// Convert system prompt
|
// Convert system prompt
|
||||||
@ -33,7 +36,7 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest
|
|||||||
|
|
||||||
// Convert messages
|
// Convert messages
|
||||||
for (const msg of body.messages) {
|
for (const msg of body.messages) {
|
||||||
convertMessage(msg, messages)
|
convertMessage(msg, messages, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build request
|
// Build request
|
||||||
@ -85,12 +88,19 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest
|
|||||||
} else if (body.thinking.type === 'enabled') {
|
} else if (body.thinking.type === 'enabled') {
|
||||||
result.reasoning_effort = 'high'
|
result.reasoning_effort = 'high'
|
||||||
}
|
}
|
||||||
|
if (options.passThinkingToggle) {
|
||||||
|
result.thinking = { type: body.thinking.type }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[]): void {
|
function convertMessage(
|
||||||
|
msg: AnthropicMessage,
|
||||||
|
output: OpenAIChatMessage[],
|
||||||
|
options: { roundTripReasoningContent?: boolean },
|
||||||
|
): void {
|
||||||
const content = msg.content
|
const content = msg.content
|
||||||
|
|
||||||
// Simple string content
|
// Simple string content
|
||||||
@ -108,7 +118,7 @@ function convertMessage(msg: AnthropicMessage, output: OpenAIChatMessage[]): voi
|
|||||||
if (msg.role === 'user') {
|
if (msg.role === 'user') {
|
||||||
convertUserMessage(content, output)
|
convertUserMessage(content, output)
|
||||||
} else {
|
} 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 textContent = ''
|
||||||
|
let reasoningContent = ''
|
||||||
const toolCalls: OpenAIToolCall[] = []
|
const toolCalls: OpenAIToolCall[] = []
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
if (block.type === 'text') {
|
if (block.type === 'text') {
|
||||||
textContent += block.text
|
textContent += block.text
|
||||||
|
} else if (block.type === 'thinking' && options.roundTripReasoningContent) {
|
||||||
|
reasoningContent += block.thinking
|
||||||
} else if (block.type === 'tool_use') {
|
} else if (block.type === 'tool_use') {
|
||||||
toolCalls.push({
|
toolCalls.push({
|
||||||
id: block.id,
|
id: block.id,
|
||||||
@ -164,7 +181,6 @@ function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAI
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Skip thinking blocks — no OpenAI equivalent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const msg: OpenAIChatMessage = {
|
const msg: OpenAIChatMessage = {
|
||||||
@ -175,6 +191,9 @@ function convertAssistantMessage(blocks: AnthropicContentBlock[], output: OpenAI
|
|||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
msg.tool_calls = toolCalls
|
msg.tool_calls = toolCalls
|
||||||
}
|
}
|
||||||
|
if (reasoningContent) {
|
||||||
|
msg.reasoning_content = reasoningContent
|
||||||
|
}
|
||||||
|
|
||||||
output.push(msg)
|
output.push(msg)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export type OpenAIChatMessage = {
|
|||||||
role: 'system' | 'user' | 'assistant' | 'tool'
|
role: 'system' | 'user' | 'assistant' | 'tool'
|
||||||
content?: string | OpenAIChatContentPart[] | null
|
content?: string | OpenAIChatContentPart[] | null
|
||||||
name?: string
|
name?: string
|
||||||
|
reasoning_content?: string
|
||||||
tool_calls?: OpenAIToolCall[]
|
tool_calls?: OpenAIToolCall[]
|
||||||
tool_call_id?: string
|
tool_call_id?: string
|
||||||
}
|
}
|
||||||
@ -48,6 +49,7 @@ export type OpenAIChatRequest = {
|
|||||||
tools?: OpenAITool[]
|
tools?: OpenAITool[]
|
||||||
tool_choice?: unknown
|
tool_choice?: unknown
|
||||||
reasoning_effort?: 'low' | 'medium' | 'high'
|
reasoning_effort?: 'low' | 'medium' | 'high'
|
||||||
|
thinking?: { type: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OpenAIChatResponse = {
|
export type OpenAIChatResponse = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user