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
This commit is contained in:
diegosouzapw
2026-05-31 00:41:46 -03:00
parent 5c340ea813
commit 672398e86f
4 changed files with 107 additions and 3 deletions

View File

@@ -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

View File

@@ -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<string, RegistryEntry> = {
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<string, RegistryEntry> = {
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" },

View File

@@ -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),
};
}

View File

@@ -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<string, { models?: ModelEntry[] }>)[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)`
);
});
}