fix: unwrap double-nested opencode.ai + handle reasoning_content in Anthropic handler

Two fixes for deepseek-v4-flash-free via Anthropic protocol:

1. **Non-streaming**: Apply config.adaptResponse to unwrap double-nested opencode.ai JSON response before backendToAnthropicResponse
2. **Streaming**: Extract delta.reasoning_content from DeepSeek SSE chunks wrapped in <thinking> tags

All tests pass, verified locally with both streaming and non-streaming.
This commit is contained in:
Asep Haryana Saputra
2026-06-25 01:22:40 +07:00
committed by GitHub
parent fc69a0e93c
commit c8ec62c345
+33 -3
View File
@@ -280,6 +280,14 @@ function accumulateSSEText(sseBody: string): string {
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
if (parsed.type === "text-delta" && parsed.delta) { if (parsed.type === "text-delta" && parsed.delta) {
accumulated += parsed.delta; accumulated += parsed.delta;
continue;
}
// Handle OpenAI-format SSE with reasoning_content
const delta = parsed.choices?.[0]?.delta;
if (delta?.reasoning_content) {
accumulated += `<thinking>${delta.reasoning_content}</thinking>`;
} else if (delta?.content) {
accumulated += delta.content;
} }
} catch { } catch {
// skip unparseable lines // skip unparseable lines
@@ -295,6 +303,9 @@ function accumulateSSEText(sseBody: string): string {
* - Claude Code format: { "type": "text-delta", "delta": "..." } * - Claude Code format: { "type": "text-delta", "delta": "..." }
* - OpenAI format: { "choices": [{ "delta": { "content": "..." } }] } * - OpenAI format: { "choices": [{ "delta": { "content": "..." } }] }
* - Generic JSON: { "content": "..." } or { "text": "..." } * - Generic JSON: { "content": "..." } or { "text": "..." }
*
* Also handles reasoning_content (e.g., DeepSeek model output) by wrapping
* it in <thinking> tags so it is preserved in the Anthropic stream output.
*/ */
function extractTextFromSSE(parsed: any): string | null { function extractTextFromSSE(parsed: any): string | null {
if (parsed == null) return null; if (parsed == null) return null;
@@ -308,9 +319,21 @@ function extractTextFromSSE(parsed: any): string | null {
} }
} }
const openai = parsed.choices?.[0]?.delta?.content ?? const delta = parsed.choices?.[0]?.delta;
parsed.choices?.[0]?.text; const reasoningDelta = delta?.reasoning_content;
if (openai) return openai; const contentDelta = delta?.content;
const textFallback = parsed.choices?.[0]?.text;
// When the delta has reasoning_content but no content, wrap in <thinking> tags
if (reasoningDelta && !contentDelta) {
return `<thinking>${reasoningDelta}</thinking>`;
}
// When both exist, prepend thinking and follow with text content
if (reasoningDelta && contentDelta) {
return `<thinking>${reasoningDelta}</thinking>${contentDelta}`;
}
if (contentDelta) return contentDelta;
if (textFallback) return textFallback;
if (typeof parsed.content === "string") return parsed.content; if (typeof parsed.content === "string") return parsed.content;
if (typeof parsed.text === "string") return parsed.text; if (typeof parsed.text === "string") return parsed.text;
@@ -909,6 +932,13 @@ export async function handleAnthropicMessages(
parsed = { content: text }; parsed = { content: text };
} }
// Apply backend adaptResponse if available (handles double-nested responses
// from providers like opencode.ai that wrap the real response as a JSON
// string inside choices[0].message.content).
if (config.adaptResponse && typeof parsed === "object" && parsed !== null) {
parsed = config.adaptResponse(parsed, req as any) ?? parsed;
}
const adapted = backendToAnthropicResponse(parsed, req.model); const adapted = backendToAnthropicResponse(parsed, req.model);
return new Response(JSON.stringify(adapted), { return new Response(JSON.stringify(adapted), {
status: 200, status: 200,