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:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user