From db281c5175f6e45cfaea30b1b5649db64e0bf90d Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 24 Jun 2026 22:19:12 +0700 Subject: [PATCH] fix: unwrap double-nested opencode.ai responses in proxy opencode.ai returns the real OpenAI-compatible response as a JSON string inside choices[0].message.content instead of an object. Added an adaptResponse handler that detects and unwraps this double-nesting, merging inner usage/token counts and reasoning_content into the standard envelope. Also added build/ to .gitignore. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + src/lib/ai-proxy.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/.gitignore b/.gitignore index 09319a4..4596d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules # output out dist +build *.tgz .next/ # code coverage diff --git a/src/lib/ai-proxy.ts b/src/lib/ai-proxy.ts index 1ffaa3b..ccda9cf 100644 --- a/src/lib/ai-proxy.ts +++ b/src/lib/ai-proxy.ts @@ -99,6 +99,48 @@ export const MODEL_ROUTES: Record = { headers: { "Content-Type": "application/json", }, + adaptResponse: (raw: any) => { + // opencode.ai wraps the real response as a JSON string inside choices[0].message.content. + // Detect this double-nesting and unwrap it so downstream Anthropic/OpenAI adapters + // see the real content and usage instead of a raw stringified blob. + const outerChoice = raw.choices?.[0]; + const innerContent = outerChoice?.message?.content; + if (typeof innerContent === "string" && innerContent.startsWith("{")) { + try { + const inner = JSON.parse(innerContent); + const innerChoice = inner.choices?.[0]; + if (innerChoice) { + // Merge: use the inner response as the source of truth for content and usage + const innerUsage = inner.usage ?? {}; + return { + ...raw, + choices: [ + { + ...outerChoice, + message: { + role: innerChoice.message?.role ?? "assistant", + // Prefer reasoning_content (thinking) when present; fall back to content + content: innerChoice.message?.reasoning_content + ? `${innerChoice.message.reasoning_content}${innerChoice.message.content ?? ""}` + : (innerChoice.message?.content ?? innerContent), + }, + finish_reason: innerChoice.finish_reason ?? outerChoice.finish_reason, + }, + ], + // Prefer inner usage (has real token counts) over outer (often zeros) + usage: { + prompt_tokens: innerUsage.prompt_tokens ?? innerUsage.input_tokens ?? raw.usage?.prompt_tokens ?? 0, + completion_tokens: innerUsage.completion_tokens ?? innerUsage.output_tokens ?? raw.usage?.completion_tokens ?? 0, + total_tokens: innerUsage.total_tokens ?? raw.usage?.total_tokens ?? 0, + }, + }; + } + } catch { + // Not valid JSON — treat innerContent as plain text + } + } + return raw; + }, }, // -- surfsense.com (custom format) -------------------------------------------