mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
79
tests/unit/reasoning-replay-big-pickle.test.ts
Normal file
79
tests/unit/reasoning-replay-big-pickle.test.ts
Normal 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)`
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user