From 672398e86fbffb85cc0bbd6d81b01cc8125821ff Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sun, 31 May 2026 00:41:46 -0300 Subject: [PATCH] fix(routing): replay reasoning_content for OpenCode big-pickle (#2900) big-pickle's OpenCode/Zen upstream runs DeepSeek thinking mode, but the model id reveals no DeepSeek signal, so requiresReasoningReplay (called with allowLegacyFallback:false) never triggered. Follow-up/tool-use turns failed with [400] 'The reasoning_content in the thinking mode must be passed back to the API'. Note: requiresReasoningReplay does not consume supportsReasoning, so the registry flag alone would not have fixed it. Add RegistryModel.interleavedField (mirrors models.dev interleaved_field), declare interleavedField:'reasoning_content' (+ supportsReasoning:true) on big-pickle in both opencode and opencode-zen, and surface the registry value in getResolvedModelCapabilities so requiresReasoningReplay returns true. Closes #2900 --- CHANGELOG.md | 8 ++ open-sse/config/providerRegistry.ts | 17 +++- src/lib/modelCapabilities.ts | 6 +- .../unit/reasoning-replay-big-pickle.test.ts | 79 +++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 tests/unit/reasoning-replay-big-pickle.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c40c6d435e..3105a36e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,14 @@ `api_key_stream_default_mode` `ALTER` at `077`, added a retroactive `isSchemaAlreadyApplied` guard (case `085`), and a regression test enforcing unique migration prefixes. +- **routing/reasoning-replay:** OpenCode `big-pickle` (provider `opencode`/`oc` + and `opencode-zen`) now declares the interleaved `reasoning_content` contract + via a new `RegistryModel.interleavedField` field, so follow-up/tool-use turns + replay reasoning_content. Previously `big-pickle` matched no replay pattern and + failed with `[400] The reasoning_content in the thinking mode must be passed + back to the API` (its DeepSeek-thinking upstream is not detectable from the + model id, and `requiresReasoningReplay` does not consume `supportsReasoning`). + `getResolvedModelCapabilities` now surfaces the registry `interleavedField`. (#2900) ### ✨ New Features diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 405f7ac46e..75cd74a5ed 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -56,6 +56,13 @@ export interface RegistryModel { unsupportedParams?: readonly string[]; /** Maximum context window in tokens */ contextLength?: number; + /** + * Interleaved-reasoning signal, mirroring models.dev's `interleaved_field`. + * Set to "reasoning_content" for models whose upstream runs DeepSeek thinking + * mode (e.g. OpenCode `big-pickle`) so follow-up/tool-use turns replay + * reasoning_content instead of failing with a DeepSeek 400 (#2900). + */ + interleavedField?: string; } // Reasoning models reject temperature, top_p, penalties, logprobs, n. @@ -1290,7 +1297,10 @@ export const REGISTRY: Record = { passthroughModels: true, defaultContextLength: 200000, models: [ - { id: "big-pickle", name: "Big Pickle" }, + // #2900: big-pickle's upstream runs DeepSeek thinking mode — declare the + // interleaved reasoning_content contract so follow-up/tool-use turns replay + // it (otherwise DeepSeek returns 400 "reasoning_content ... must be passed back"). + { id: "big-pickle", name: "Big Pickle", supportsReasoning: true, interleavedField: "reasoning_content" }, { id: "minimax-m2.5-free", name: "MiniMax M2.5 Free", contextLength: 204800 }, { id: "ling-2.6-1t-free", name: "Ling 2.6 Free", contextLength: 262000 }, { @@ -1357,7 +1367,10 @@ export const REGISTRY: Record = { passthroughModels: true, models: [ // ── Chat / Coding ────────────────────────────────────────── - { id: "big-pickle", name: "Big Pickle" }, + // #2900: big-pickle's upstream runs DeepSeek thinking mode — declare the + // interleaved reasoning_content contract so follow-up/tool-use turns replay + // it (otherwise DeepSeek returns 400 "reasoning_content ... must be passed back"). + { id: "big-pickle", name: "Big Pickle", supportsReasoning: true, interleavedField: "reasoning_content" }, { id: "gpt-5-nano", name: "GPT 5 Nano", contextLength: 400000 }, { id: "gpt-5", name: "GPT 5" }, { id: "gpt-5-codex", name: "GPT 5 Codex" }, diff --git a/src/lib/modelCapabilities.ts b/src/lib/modelCapabilities.ts index 8b362dde6a..3a2bd1d1b0 100644 --- a/src/lib/modelCapabilities.ts +++ b/src/lib/modelCapabilities.ts @@ -281,7 +281,11 @@ export function getResolvedModelCapabilities(input: CapabilityInput): ResolvedMo lastUpdated: synced?.last_updated ?? null, modalitiesInput, modalitiesOutput, - interleavedField: synced?.interleaved_field ?? null, + interleavedField: + synced?.interleaved_field ?? + (typeof registryModel?.interleavedField === "string" + ? registryModel.interleavedField + : null), }; } diff --git a/tests/unit/reasoning-replay-big-pickle.test.ts b/tests/unit/reasoning-replay-big-pickle.test.ts new file mode 100644 index 0000000000..a4f5641875 --- /dev/null +++ b/tests/unit/reasoning-replay-big-pickle.test.ts @@ -0,0 +1,79 @@ +/** + * Issue #2900 — OpenCode `big-pickle` fails DeepSeek thinking-mode + * reasoning_content replay. + * + * `big-pickle` (OpenCode free / Zen, endpoint https://opencode.ai/zen/v1) is + * backed by DeepSeek thinking mode upstream, so follow-up/tool-use turns must + * replay `reasoning_content` or DeepSeek returns: + * [400]: The reasoning_content in the thinking mode must be passed back to the API. + * + * Unlike `deepseek-v4-flash-free`, the model id `big-pickle` gives no signal to + * the replay detector, and `requiresReasoningReplay` (with allowLegacyFallback:false, + * as the translator calls it) only triggers on: + * - interleavedField === "reasoning_content" (models.dev signal), or + * - isDeepSeekReasoningModel() pattern match. + * + * Note: `supportsReasoning` is NOT consumed by `requiresReasoningReplay`, so + * marking the model `supportsReasoning: true` alone does NOT enable replay. + * The real trigger is an explicit `interleavedField: "reasoning_content"` on the + * registry entry, surfaced by getResolvedModelCapabilities. + * + * This test asserts the end-to-end wiring for both OpenCode registrations + * (`opencode`/alias `oc` and `opencode-zen`). + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); +const { getResolvedModelCapabilities } = await import("../../src/lib/modelCapabilities.ts"); +const { requiresReasoningReplay } = await import("../../open-sse/services/reasoningCache.ts"); + +type ModelEntry = { + id: string; + supportsReasoning?: boolean; + interleavedField?: string; + [key: string]: unknown; +}; + +function getModel(providerId: string, modelId: string): ModelEntry | undefined { + const provider = (REGISTRY as Record)[providerId]; + return provider?.models?.find((m) => m.id === modelId); +} + +for (const providerId of ["opencode", "opencode-zen"]) { + test(`#2900 ${providerId}/big-pickle registry declares interleavedField reasoning_content`, () => { + const model = getModel(providerId, "big-pickle"); + assert.ok(model, `big-pickle must be registered in ${providerId}`); + assert.strictEqual( + model.interleavedField, + "reasoning_content", + `${providerId}/big-pickle must declare interleavedField:"reasoning_content" to trigger replay` + ); + }); + + test(`#2900 ${providerId}/big-pickle resolves interleavedField via capabilities`, () => { + const caps = getResolvedModelCapabilities({ provider: providerId, model: "big-pickle" }); + assert.strictEqual( + caps.interleavedField, + "reasoning_content", + `getResolvedModelCapabilities must surface the registry interleavedField for ${providerId}/big-pickle` + ); + }); + + test(`#2900 ${providerId}/big-pickle triggers reasoning replay`, () => { + const caps = getResolvedModelCapabilities({ provider: providerId, model: "big-pickle" }); + const isReasoner = requiresReasoningReplay({ + provider: providerId, + model: "big-pickle", + thinkingEnabled: false, + supportsReasoning: caps.reasoning, + interleavedField: caps.interleavedField, + allowLegacyFallback: false, + }); + assert.strictEqual( + isReasoner, + true, + `${providerId}/big-pickle must require reasoning replay (matching deepseek-v4-flash-free behavior)` + ); + }); +}