mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
fix(deepseek): align V4 reasoning efforts across DeepSeek and OpenCode Go (#10540)
* fix(deepseek): align V4 reasoning efforts * docs(changelog): note DeepSeek effort fix * fix(deepseek): align OpenCode V4 effort aliases * test(deepseek): align effort alias expectation * fix(deepseek): scope low effort to v4 * fix(opencode-go): route DeepSeek V4 through Responses
This commit is contained in:
1
changelog.d/fixes/10540-deepseek-v4-efforts.md
Normal file
1
changelog.d/fixes/10540-deepseek-v4-efforts.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(deepseek):** Advertise `none`, `low`, `high`, and `max` for V4 Pro and Flash, derive OpenCode Go effort aliases from base-model metadata, and route those models through native Responses ([#10540](https://github.com/diegosouzapw/OmniRoute/pull/10540)) — thanks @jackjinke
|
||||
@@ -24,7 +24,7 @@ export const deepseekProvider: RegistryEntry = {
|
||||
contextLength: 1_000_000,
|
||||
maxOutputTokens: 384_000,
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["none", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "high", "max"],
|
||||
toolCalling: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -131,27 +131,19 @@ export const opencode_goProvider: RegistryEntry = {
|
||||
{ id: "grok-4.5-low", name: "Grok 4.5 (low effort)", supportsReasoning: true },
|
||||
{ id: "grok-4.5-medium", name: "Grok 4.5 (medium effort)", supportsReasoning: true },
|
||||
{ id: "grok-4.5-high", name: "Grok 4.5 (high effort)", supportsReasoning: true },
|
||||
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
|
||||
// OpencodeExecutor rewrites these aliases to the canonical upstream id and injects reasoning_effort.
|
||||
{ id: "deepseek-v4-pro-low", name: "DeepSeek V4 Pro (low effort)", supportsReasoning: true },
|
||||
{
|
||||
id: "deepseek-v4-pro-medium",
|
||||
name: "DeepSeek V4 Pro (medium effort)",
|
||||
supportsReasoning: true,
|
||||
},
|
||||
{ id: "deepseek-v4-pro-high", name: "DeepSeek V4 Pro (high effort)", supportsReasoning: true },
|
||||
{ id: "deepseek-v4-pro-max", name: "DeepSeek V4 Pro (max effort)", supportsReasoning: true },
|
||||
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
|
||||
// #8353: DeepSeek V4 Flash effort tiers from the OpenCode Go registry.
|
||||
{
|
||||
id: "deepseek-v4-flash-high",
|
||||
name: "DeepSeek V4 Flash (high effort)",
|
||||
id: "deepseek-v4-pro",
|
||||
name: "DeepSeek V4 Pro",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["none", "low", "high", "max"],
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
{
|
||||
id: "deepseek-v4-flash-max",
|
||||
name: "DeepSeek V4 Flash (max effort)",
|
||||
id: "deepseek-v4-flash",
|
||||
name: "DeepSeek V4 Flash",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["none", "low", "high", "max"],
|
||||
targetFormat: "openai-responses",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -297,23 +297,17 @@ export function sanitizeReasoningEffortForProvider(
|
||||
return writeEffortValue(b, "max", c);
|
||||
}
|
||||
|
||||
// Native DeepSeek (api.deepseek.com) — V4 thinking mode uses the native
|
||||
// {low, high, max} vocabulary on Flash and {high, max} on Pro. OmniRoute's
|
||||
// internal top tier xhigh maps to DeepSeek's literal max. Pro's unsupported
|
||||
// low/medium values still clamp to high; Flash's documented low tier passes
|
||||
// through. This is the INVERSE of the OpenRouter-DeepSeek path, whose
|
||||
// normalized API expects xhigh, not max (pi#4055). `none` is already the
|
||||
// OpenAI no-thinking carrier and passes through unchanged.
|
||||
// Native DeepSeek (api.deepseek.com) — V4 Pro and Flash use the native
|
||||
// {low, high, max} vocabulary, while other model ids retain the {high, max}
|
||||
// floor. OmniRoute's internal top tier xhigh maps to DeepSeek's literal max,
|
||||
// while compatibility-only medium maps to high. `none` is already the OpenAI
|
||||
// no-thinking carrier and passes through unchanged.
|
||||
if (provider === "deepseek") {
|
||||
// Match the Flash family even when the sanitizer sees a suffixed or prefixed
|
||||
// id — exact-match would silently clamp Flash `low → high` if a future route
|
||||
// forwards the raw catalog id (`deepseek-v4-flash-low`) before resolution
|
||||
// (#9485 review).
|
||||
const isFlash = modelStr.toLowerCase().startsWith("deepseek-v4-flash");
|
||||
const isV4 = modelStr.toLowerCase().startsWith("deepseek-v4-");
|
||||
const mapped =
|
||||
effortStr === "xhigh"
|
||||
? "max"
|
||||
: effortStr === "medium" || (effortStr === "low" && !isFlash)
|
||||
: effortStr === "medium" || (effortStr === "low" && !isV4)
|
||||
? "high"
|
||||
: null;
|
||||
if (mapped && mapped !== effortStr) {
|
||||
|
||||
@@ -31,7 +31,7 @@ interface OpencodeAccountState extends RotatableAccount {
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
const EFFORT_LEVELS = ["low", "medium", "high", "max"] as const;
|
||||
const EFFORT_LEVELS = ["none", "low", "high", "max"] as const;
|
||||
|
||||
/**
|
||||
* Models that work WITHOUT any API key on the free/noauth opencode tier.
|
||||
@@ -62,7 +62,7 @@ const OPENCODE_FREE_MODELS = new Set([
|
||||
* Models on opencode-go that support effort-tier aliases. Each entry maps the
|
||||
* canonical base id to the set of effort suffixes the upstream supports.
|
||||
*
|
||||
* - deepseek-v4-pro: all four tiers (low/medium/high/max)
|
||||
* - DeepSeek V4 Pro and Flash: none/low/high/max
|
||||
* - glm-5.2: high/max only (Z.AI maps these through the reasoning plane;
|
||||
* low/medium are not supported on the OpenAI transport)
|
||||
* - mimo-v2.5: high/max only (same reasoning; Xiaomi MiMo does not document
|
||||
@@ -70,12 +70,12 @@ const OPENCODE_FREE_MODELS = new Set([
|
||||
* - #8353 OpenCode Go registry effort variants (exact suffix sets from
|
||||
* `opencode models opencode-go --verbose`; MiniMax M3 excluded — different
|
||||
* thinking-mode mapping):
|
||||
* deepseek-v4-flash high/max; grok-4.5 low/medium/high; hy3 none/low/high;
|
||||
* kimi-k3 max; qwen3.6-plus / qwen3.7-max / qwen3.7-plus high/max
|
||||
* grok-4.5 low/medium/high; hy3 none/low/high; kimi-k3 max;
|
||||
* qwen3.6-plus / qwen3.7-max / qwen3.7-plus high/max
|
||||
*/
|
||||
const EFFORT_TIERS: Record<string, readonly string[]> = {
|
||||
"deepseek-v4-pro": EFFORT_LEVELS,
|
||||
"deepseek-v4-flash": ["high", "max"],
|
||||
"deepseek-v4-flash": EFFORT_LEVELS,
|
||||
"glm-5.2": ["high", "max"],
|
||||
"mimo-v2.5": ["high", "max"],
|
||||
"grok-4.5": ["low", "medium", "high"],
|
||||
|
||||
@@ -676,11 +676,9 @@ test("sanitizeReasoningEffortForProvider: NVIDIA GLM-5.2 mapping is narrowly sco
|
||||
});
|
||||
|
||||
// ── Native DeepSeek (api.deepseek.com) ───────────────────────────────────────
|
||||
// DeepSeek V4 thinking mode accepts reasoning_effort ONLY as {high, max}. The
|
||||
// internal OmniRoute scale (low|medium|high|xhigh, xhigh = top) must be mapped
|
||||
// onto DeepSeek's native vocabulary so the client's requested effort is honored
|
||||
// instead of silently dropped to the default. This is the INVERSE of the
|
||||
// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max.
|
||||
// DeepSeek V4 thinking mode accepts reasoning_effort as {low, high, max}.
|
||||
// The internal OmniRoute scale maps medium → high and xhigh → max so the client's
|
||||
// requested effort is honored instead of silently dropped to the default.
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek maps xhigh → max", () => {
|
||||
const log = makeLog();
|
||||
@@ -716,19 +714,25 @@ test("sanitizeReasoningEffortForProvider: native deepseek preserves max", () =>
|
||||
assert.equal(log.messages.length, 0);
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek clamps low → high", () => {
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek preserves low", () => {
|
||||
const body = {
|
||||
model: "deepseek-v4-pro",
|
||||
reasoning_effort: "low",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-v4-pro", null);
|
||||
assert.equal(result, body, "low is already valid — passes through unchanged");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native non-V4 deepseek clamps low → high", () => {
|
||||
const body = {
|
||||
model: "deepseek-chat",
|
||||
reasoning_effort: "low",
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(body, "deepseek", "deepseek-chat", null);
|
||||
assert.notEqual(result, body, "must return a new object when mutating");
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning_effort,
|
||||
"high",
|
||||
"below the {high, max} floor → high"
|
||||
);
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "high");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: native deepseek clamps medium → high", () => {
|
||||
@@ -786,11 +790,10 @@ test("sanitizeReasoningEffortForProvider: OpenRouter DeepSeek still preserves xh
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "xhigh");
|
||||
});
|
||||
|
||||
// ── opencode-go DeepSeek V4 Pro effort variants (#4647) ──────────────────────
|
||||
// opencode-go proxies DeepSeek with the native DeepSeek API contract, which
|
||||
// accepts {high, max} literally. The OpencodeExecutor's transformRequest sets
|
||||
// reasoning_effort to the variant suffix (low|medium|high|max), and the
|
||||
// sanitizer must NOT rewrite `max` → `xhigh` for this provider+model combo.
|
||||
// ── opencode-go DeepSeek V4 effort variants (#4647) ──────────────────────────
|
||||
// opencode-go proxies DeepSeek with the native DeepSeek API contract. Both V4
|
||||
// models advertise none/low/high/max, and the sanitizer must preserve those
|
||||
// literal values rather than rewriting `max` to `xhigh`.
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves max", () => {
|
||||
const body = {
|
||||
@@ -803,24 +806,26 @@ test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves
|
||||
assert.equal((result as Record<string, unknown>).reasoning_effort, "max");
|
||||
});
|
||||
|
||||
test("sanitizeReasoningEffortForProvider: opencode-go DeepSeek V4 Pro preserves variant suffix levels", () => {
|
||||
for (const level of ["low", "medium", "high", "max"]) {
|
||||
const body = {
|
||||
model: `deepseek-v4-pro-${level}`,
|
||||
reasoning_effort: level,
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(
|
||||
body,
|
||||
"opencode-go",
|
||||
`deepseek-v4-pro-${level}`,
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning_effort,
|
||||
level,
|
||||
`opencode-go deepseek-v4-pro-${level} preserves reasoning_effort=${level}`
|
||||
);
|
||||
test("sanitizeReasoningEffortForProvider: opencode-go preserves both V4 models' tiers", () => {
|
||||
for (const model of ["deepseek-v4-pro", "deepseek-v4-flash"]) {
|
||||
for (const level of ["none", "low", "high", "max"]) {
|
||||
const body = {
|
||||
model: `${model}-${level}`,
|
||||
reasoning_effort: level,
|
||||
messages: [],
|
||||
};
|
||||
const result = sanitizeReasoningEffortForProvider(
|
||||
body,
|
||||
"opencode-go",
|
||||
`${model}-${level}`,
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
(result as Record<string, unknown>).reasoning_effort,
|
||||
level,
|
||||
`opencode-go ${model}-${level} preserves reasoning_effort=${level}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ test("#9485 static DeepSeek effort aliases appear when synced rows omit supporte
|
||||
`${flashId}-high`,
|
||||
`${flashId}-max`,
|
||||
`${proId}-none`,
|
||||
`${proId}-low`,
|
||||
`${proId}-high`,
|
||||
`${proId}-max`,
|
||||
]);
|
||||
@@ -150,10 +151,6 @@ test("#9485 static DeepSeek effort aliases appear when synced rows omit supporte
|
||||
.filter((id) => id.startsWith(`${flashId}-`) || id.startsWith(`${proId}-`))
|
||||
);
|
||||
assert.deepEqual(deepSeekAliases, expectedAliases);
|
||||
assert.equal(
|
||||
provider!.models.some((model) => model.id === `${proId}-low`),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
provider!.models.some((model) => model.id === `${proId}-medium`),
|
||||
false
|
||||
|
||||
@@ -29,20 +29,21 @@ test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("DeepSeek registry declares the documented per-model thinking efforts", () => {
|
||||
const models = new Map((REGISTRY.deepseek?.models || []).map((model) => [model.id, model]));
|
||||
|
||||
assert.deepEqual(models.get("deepseek-v4-flash")?.supportedThinkingEfforts, [
|
||||
"none",
|
||||
"low",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
assert.deepEqual(models.get("deepseek-v4-pro")?.supportedThinkingEfforts, [
|
||||
"none",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
test("DeepSeek registries declare none/low/high/max on both V4 models", () => {
|
||||
const expectedEfforts = ["none", "low", "high", "max"];
|
||||
for (const providerId of ["deepseek", "opencode-go"]) {
|
||||
const models = new Map((REGISTRY[providerId]?.models || []).map((model) => [model.id, model]));
|
||||
for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) {
|
||||
assert.deepEqual(models.get(modelId)?.supportedThinkingEfforts, expectedEfforts);
|
||||
for (const effort of expectedEfforts) {
|
||||
assert.equal(
|
||||
models.has(`${modelId}-${effort}`),
|
||||
false,
|
||||
`${providerId} should derive ${modelId}-${effort} from the base model metadata`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("DeepSeek catalog exposes only the declared effort aliases", async () => {
|
||||
@@ -66,13 +67,38 @@ test("DeepSeek catalog exposes only the declared effort aliases", async () => {
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-high")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-max")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-none")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-low")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-high")));
|
||||
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-pro-max")));
|
||||
assert.equal(
|
||||
[...ids].some((id) => id.endsWith("deepseek-v4-pro-low")),
|
||||
false,
|
||||
"Pro does not advertise low"
|
||||
});
|
||||
|
||||
test("OpenCode Go catalog derives the declared V4 effort aliases from base models", async () => {
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "opencode-go",
|
||||
authType: "apikey",
|
||||
name: "opencode-go-deepseek-efforts",
|
||||
apiKey: "opencode-go-test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
|
||||
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as {
|
||||
data: Array<{ id: string; capabilities?: { effort_tiers?: string[] } }>;
|
||||
};
|
||||
const models = new Map(body.data.map((model) => [model.id, model]));
|
||||
const expectedEfforts = ["none", "low", "high", "max"];
|
||||
|
||||
for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) {
|
||||
const baseId = `opencode-go/${modelId}`;
|
||||
assert.deepEqual(models.get(baseId)?.capabilities?.effort_tiers, expectedEfforts);
|
||||
for (const effort of expectedEfforts) {
|
||||
assert.ok(models.has(`${baseId}-${effort}`), `${baseId}-${effort} must be advertised`);
|
||||
}
|
||||
assert.equal(models.has(`${baseId}-medium`), false);
|
||||
}
|
||||
});
|
||||
test("Crof synced reasoning metadata exposes exactly none/low/medium/high/max aliases", async () => {
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
@@ -181,25 +207,31 @@ test("hardcoded DeepSeek effort suffixes resolve through the static registry", a
|
||||
assert.equal(flashNone.model, "deepseek-v4-flash");
|
||||
assert.equal(flashNone.resolvedThinkingEffort, "none");
|
||||
|
||||
const unsupportedProLow = await getModelInfo("ds/deepseek-v4-pro-low");
|
||||
assert.equal(unsupportedProLow.model, "deepseek-v4-pro-low");
|
||||
assert.equal(unsupportedProLow.resolvedThinkingEffort, undefined);
|
||||
const proLow = await getModelInfo("ds/deepseek-v4-pro-low");
|
||||
assert.equal(proLow.model, "deepseek-v4-pro");
|
||||
assert.equal(proLow.resolvedThinkingEffort, "low");
|
||||
});
|
||||
|
||||
test("native DeepSeek preserves Flash low while clamping unsupported Pro low", () => {
|
||||
const flash = sanitizeReasoningEffortForProvider(
|
||||
{ model: "deepseek-v4-flash", reasoning_effort: "low" },
|
||||
"deepseek",
|
||||
"deepseek-v4-flash"
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(flash.reasoning_effort, "low");
|
||||
test("OpenCode Go V4 suffixes resolve from base-model effort metadata", async () => {
|
||||
for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) {
|
||||
for (const effort of ["none", "low", "high", "max"]) {
|
||||
const info = await getModelInfo(`opencode-go/${modelId}-${effort}`);
|
||||
assert.equal(info.provider, "opencode-go");
|
||||
assert.equal(info.model, modelId);
|
||||
assert.equal(info.resolvedThinkingEffort, effort);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const pro = sanitizeReasoningEffortForProvider(
|
||||
{ model: "deepseek-v4-pro", reasoning_effort: "low" },
|
||||
"deepseek",
|
||||
"deepseek-v4-pro"
|
||||
) as Record<string, unknown>;
|
||||
assert.equal(pro.reasoning_effort, "high");
|
||||
test("native DeepSeek preserves the documented low effort for Flash and Pro", () => {
|
||||
for (const model of ["deepseek-v4-flash", "deepseek-v4-pro"]) {
|
||||
const body = { model, reasoning_effort: "low" };
|
||||
assert.equal(
|
||||
sanitizeReasoningEffortForProvider(body, "deepseek", model),
|
||||
body,
|
||||
`${model} must pass low through unchanged`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("non-DeepSeek static reasoning models do not advertise unresolvable effort aliases", async () => {
|
||||
@@ -266,9 +298,9 @@ test("custom model named deepseek-v4-flash-low is not rewritten by registry suff
|
||||
assert.equal(info.resolvedThinkingEffort, undefined);
|
||||
});
|
||||
|
||||
test("none effort resolves and passes through the native DeepSeek sanitizer unchanged", async () => {
|
||||
// The -none suffix resolves to base + effort "none", which reaches the native
|
||||
// DeepSeek endpoint as reasoning_effort: "none" unchanged (#9485 review #8).
|
||||
test("none effort resolves and stays explicit through provider sanitation", async () => {
|
||||
// The format translator subsequently carries this as reasoning.effort:"none"
|
||||
// on DeepSeek's default Responses route, which disables thinking.
|
||||
const flashNone = await getModelInfo("ds/deepseek-v4-flash-none");
|
||||
assert.equal(flashNone.model, "deepseek-v4-flash");
|
||||
assert.equal(flashNone.resolvedThinkingEffort, "none");
|
||||
@@ -281,9 +313,9 @@ test("none effort resolves and passes through the native DeepSeek sanitizer unch
|
||||
assert.equal(sanitized.reasoning_effort, "none");
|
||||
});
|
||||
|
||||
test("isFlash check is robust to suffixed model ids", () => {
|
||||
// A suffixed id like deepseek-v4-flash-low must still be recognized as Flash
|
||||
// so its low effort is preserved, not clamped to high (#9485 review #5).
|
||||
test("suffixed Flash low remains valid before alias resolution", () => {
|
||||
// Preserve low even if a future route sanitizes the raw suffixed id before
|
||||
// resolving it to the registered base model.
|
||||
const sanitizedSuffixed = sanitizeReasoningEffortForProvider(
|
||||
{ model: "deepseek-v4-flash-low", reasoning_effort: "low" },
|
||||
"deepseek",
|
||||
|
||||
@@ -155,3 +155,46 @@ test("single-target combo reflects unblocked Antigravity Gemini reasoning", asyn
|
||||
assert.equal(capabilities.supportsThinking, true);
|
||||
assert.equal(Object.hasOwn(capabilities, "effort_tiers"), true);
|
||||
});
|
||||
|
||||
test("mixed DeepSeek combos advertise the efforts accepted by every V4 target", async () => {
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "deepseek",
|
||||
authType: "apikey",
|
||||
name: "deepseek-v4-combos",
|
||||
apiKey: "deepseek-test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
await providersDb.createProviderConnection({
|
||||
provider: "opencode-go",
|
||||
authType: "apikey",
|
||||
name: "opencode-go-deepseek-v4-combos",
|
||||
apiKey: "opencode-go-test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) {
|
||||
await combosDb.createCombo({
|
||||
name: `${modelId}-combo`,
|
||||
strategy: "auto",
|
||||
models: [`deepseek/${modelId}`, `opencode-go/${modelId}`],
|
||||
});
|
||||
}
|
||||
|
||||
const response = await catalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as { data: Array<Record<string, unknown>> };
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
for (const modelId of ["deepseek-v4-flash", "deepseek-v4-pro"]) {
|
||||
const combo = body.data.find((item) => item.id === `${modelId}-combo`);
|
||||
assert.ok(combo);
|
||||
assert.deepEqual((combo.capabilities as Record<string, unknown>).effort_tiers, [
|
||||
"none",
|
||||
"low",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,19 +78,25 @@ describe("OpencodeExecutor", () => {
|
||||
assert.equal(model.supportsReasoning, true);
|
||||
});
|
||||
|
||||
it("exposes DeepSeek V4 Pro effort variants on opencode-go only", () => {
|
||||
it("declares V4 effort tiers on OpenCode Go base models only", () => {
|
||||
const goModels = PROVIDER_MODELS["opencode-go"] || [];
|
||||
const zenModels = PROVIDER_MODELS["opencode-zen"] || [];
|
||||
const variants = ["low", "medium", "high", "max"].map((level) => `deepseek-v4-pro-${level}`);
|
||||
for (const variant of variants) {
|
||||
const model = goModels.find((m) => m.id === variant);
|
||||
assert.ok(model, `${variant} should be in opencode-go model list`);
|
||||
assert.equal(model?.supportsReasoning, true);
|
||||
assert.equal(
|
||||
zenModels.some((m) => m.id === variant),
|
||||
false,
|
||||
`${variant} should not be exposed on opencode-zen`
|
||||
);
|
||||
const efforts = ["none", "low", "high", "max"];
|
||||
|
||||
for (const modelId of ["deepseek-v4-pro", "deepseek-v4-flash"]) {
|
||||
const base = goModels.find((model) => model.id === modelId);
|
||||
assert.deepEqual(base?.supportedThinkingEfforts, efforts);
|
||||
for (const effort of efforts) {
|
||||
const variant = `${modelId}-${effort}`;
|
||||
assert.equal(
|
||||
goModels.some((model) => model.id === variant),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
zenModels.some((model) => model.id === variant),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,6 +143,16 @@ describe("OpencodeExecutor", () => {
|
||||
assert.equal(fetchCalls[0].url, "https://opencode.ai/zen/v1/responses");
|
||||
});
|
||||
|
||||
it("routes OpenCode Go DeepSeek V4 models to the responses endpoint", async () => {
|
||||
const models = ["deepseek-v4-pro", "deepseek-v4-flash"];
|
||||
|
||||
for (const [index, model] of models.entries()) {
|
||||
const result = await goExecutor.execute(createInput(model));
|
||||
assert.equal(result.url, "https://opencode.ai/zen/go/v1/responses");
|
||||
assert.equal(fetchCalls[index].url, "https://opencode.ai/zen/go/v1/responses");
|
||||
}
|
||||
});
|
||||
|
||||
it("routes gemini streaming requests to streamGenerateContent", async () => {
|
||||
registerModel("opencode-zen", {
|
||||
id: "gemini-2.5-pro",
|
||||
@@ -544,7 +560,7 @@ describe("OpencodeExecutor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("DeepSeek V4 Pro reasoning-effort variants", () => {
|
||||
describe("DeepSeek V4 reasoning-effort variants", () => {
|
||||
function baseBody(model) {
|
||||
return {
|
||||
model,
|
||||
@@ -554,17 +570,19 @@ describe("OpencodeExecutor", () => {
|
||||
};
|
||||
}
|
||||
|
||||
const levels = ["low", "medium", "high", "max"];
|
||||
for (const level of levels) {
|
||||
it(`maps deepseek-v4-pro-${level} to base id + reasoning_effort=${level}`, () => {
|
||||
const variant = `deepseek-v4-pro-${level}`;
|
||||
const out = goExecutor.transformRequest(variant, baseBody(variant), false, {
|
||||
apiKey: "test-key",
|
||||
const levels = ["none", "low", "high", "max"];
|
||||
for (const model of ["deepseek-v4-pro", "deepseek-v4-flash"]) {
|
||||
for (const level of levels) {
|
||||
it(`maps ${model}-${level} to base id + reasoning_effort=${level}`, () => {
|
||||
const variant = `${model}-${level}`;
|
||||
const out = goExecutor.transformRequest(variant, baseBody(variant), false, {
|
||||
apiKey: "test-key",
|
||||
});
|
||||
assert.equal(out.model, model);
|
||||
assert.equal(out.reasoning_effort, level);
|
||||
assert.ok(!String(out.model).endsWith(`-${level}`));
|
||||
});
|
||||
assert.equal(out.model, "deepseek-v4-pro");
|
||||
assert.equal(out.reasoning_effort, level);
|
||||
assert.ok(!String(out.model).endsWith(`-${level}`));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
it("preserves explicit reasoning_effort over the variant suffix", () => {
|
||||
|
||||
@@ -19,41 +19,29 @@ import assert from "node:assert/strict";
|
||||
// node:test process without triggering side effects (DB init, fetch,
|
||||
// etc.). We only need parseEffortLevel, which is a pure function. ───
|
||||
|
||||
const { parseEffortLevel, OpencodeExecutor } = (await import(
|
||||
"../../open-sse/executors/opencode.ts"
|
||||
)) as {
|
||||
parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null;
|
||||
OpencodeExecutor: new (provider: string) => {
|
||||
transformRequest: (
|
||||
model: string,
|
||||
body: Record<string, unknown>,
|
||||
stream: boolean,
|
||||
credentials: unknown
|
||||
) => Record<string, unknown>;
|
||||
const { parseEffortLevel, OpencodeExecutor } =
|
||||
(await import("../../open-sse/executors/opencode.ts")) as {
|
||||
parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null;
|
||||
OpencodeExecutor: new (provider: string) => {
|
||||
transformRequest: (
|
||||
model: string,
|
||||
body: Record<string, unknown>,
|
||||
stream: boolean,
|
||||
credentials: unknown
|
||||
) => Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// ─── DeepSeek v4-pro: all 4 tiers ─────────────────────────────────────────
|
||||
// ─── DeepSeek V4 Pro: none/low/high/max ───────────────────────────────────
|
||||
|
||||
test("#6922 parseEffortLevel: deepseek-v4-pro-low → low", () => {
|
||||
const result = parseEffortLevel("deepseek-v4-pro-low");
|
||||
assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "low" });
|
||||
});
|
||||
|
||||
test("#6922 parseEffortLevel: deepseek-v4-pro-medium → medium", () => {
|
||||
const result = parseEffortLevel("deepseek-v4-pro-medium");
|
||||
assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "medium" });
|
||||
});
|
||||
|
||||
test("#6922 parseEffortLevel: deepseek-v4-pro-high → high", () => {
|
||||
const result = parseEffortLevel("deepseek-v4-pro-high");
|
||||
assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "high" });
|
||||
});
|
||||
|
||||
test("#6922 parseEffortLevel: deepseek-v4-pro-max → max", () => {
|
||||
const result = parseEffortLevel("deepseek-v4-pro-max");
|
||||
assert.deepEqual(result, { baseModel: "deepseek-v4-pro", effort: "max" });
|
||||
});
|
||||
for (const effort of ["none", "low", "high", "max"]) {
|
||||
test(`#6922 parseEffortLevel: deepseek-v4-pro-${effort} → ${effort}`, () => {
|
||||
assert.deepEqual(parseEffortLevel(`deepseek-v4-pro-${effort}`), {
|
||||
baseModel: "deepseek-v4-pro",
|
||||
effort,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─── GLM-5.2: high + max only ────────────────────────────────────────────
|
||||
|
||||
@@ -81,6 +69,10 @@ test("#6922 parseEffortLevel: mimo-v2.5-max → max", () => {
|
||||
|
||||
// ─── Negative cases ────────────────────────────────────────────────────────
|
||||
|
||||
test("#6922 parseEffortLevel: deepseek-v4-pro-medium → null (unsupported tier)", () => {
|
||||
assert.strictEqual(parseEffortLevel("deepseek-v4-pro-medium"), null);
|
||||
});
|
||||
|
||||
test("#6922 parseEffortLevel: unknown model → null", () => {
|
||||
const result = parseEffortLevel("nonexistent-model-high");
|
||||
assert.strictEqual(result, null);
|
||||
@@ -160,5 +152,9 @@ test("#6922 transformRequest: unaliased model passes through unchanged", () => {
|
||||
const out = executor.transformRequest("gpt-4o-mini", body, true, CREDENTIALS);
|
||||
|
||||
assert.equal(out.model, "gpt-4o-mini", "unaliased model id is left untouched");
|
||||
assert.equal(out.reasoning_effort, undefined, "no reasoning_effort is injected for a non-tier model");
|
||||
assert.equal(
|
||||
out.reasoning_effort,
|
||||
undefined,
|
||||
"no reasoning_effort is injected for a non-tier model"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/**
|
||||
* Issue #8353 — Missing OpenCode Go reasoning variants.
|
||||
*
|
||||
* OpenCode's local Go registry exposes effort-tier aliases that OmniRoute did
|
||||
* not register or resolve. These tests cover:
|
||||
* 1. Catalog exposure on opencode-go (and absence on opencode-zen)
|
||||
* These tests cover:
|
||||
* 1. Static registry metadata for aliases that have not migrated to derived variants
|
||||
* 2. parseEffortLevel → base + effort for every listed alias
|
||||
* 3. transformRequest rewrite + reasoning_effort injection
|
||||
* 4. MiniMax M3 stays out of the effort-alias path
|
||||
@@ -12,19 +11,18 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const { parseEffortLevel, OpencodeExecutor } = (await import(
|
||||
"../../open-sse/executors/opencode.ts"
|
||||
)) as {
|
||||
parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null;
|
||||
OpencodeExecutor: new (provider: string) => {
|
||||
transformRequest: (
|
||||
model: string,
|
||||
body: Record<string, unknown>,
|
||||
stream: boolean,
|
||||
credentials: unknown
|
||||
) => Record<string, unknown>;
|
||||
const { parseEffortLevel, OpencodeExecutor } =
|
||||
(await import("../../open-sse/executors/opencode.ts")) as {
|
||||
parseEffortLevel: (model: string) => { baseModel: string; effort: string } | null;
|
||||
OpencodeExecutor: new (provider: string) => {
|
||||
transformRequest: (
|
||||
model: string,
|
||||
body: Record<string, unknown>,
|
||||
stream: boolean,
|
||||
credentials: unknown
|
||||
) => Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const { REGISTRY } = (await import("../../open-sse/config/providerRegistry.ts")) as {
|
||||
REGISTRY: Record<
|
||||
@@ -33,8 +31,10 @@ const { REGISTRY } = (await import("../../open-sse/config/providerRegistry.ts"))
|
||||
>;
|
||||
};
|
||||
|
||||
/** Exact alias set from #8353 (MiniMax M3 intentionally excluded). */
|
||||
/** Exact alias set from #8353, plus the newly declared DeepSeek tiers. */
|
||||
const ISSUE_ALIASES: ReadonlyArray<{ alias: string; base: string; effort: string }> = [
|
||||
{ alias: "deepseek-v4-flash-none", base: "deepseek-v4-flash", effort: "none" },
|
||||
{ alias: "deepseek-v4-flash-low", base: "deepseek-v4-flash", effort: "low" },
|
||||
{ alias: "deepseek-v4-flash-high", base: "deepseek-v4-flash", effort: "high" },
|
||||
{ alias: "deepseek-v4-flash-max", base: "deepseek-v4-flash", effort: "max" },
|
||||
{ alias: "grok-4.5-low", base: "grok-4.5", effort: "low" },
|
||||
@@ -68,10 +68,10 @@ function zenModelIds(): string[] {
|
||||
|
||||
// ─── Catalog exposure ──────────────────────────────────────────────────────
|
||||
|
||||
test("#8353 catalog: every listed alias is registered on opencode-go", () => {
|
||||
test("#8353 catalog: DeepSeek aliases are derived instead of registered as duplicate rows", () => {
|
||||
const ids = new Set(goModelIds());
|
||||
for (const { alias } of ISSUE_ALIASES) {
|
||||
assert.ok(ids.has(alias), `opencode-go must expose ${alias}`);
|
||||
for (const { alias, base } of ISSUE_ALIASES) {
|
||||
assert.equal(ids.has(alias), base !== "deepseek-v4-flash", alias);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -128,7 +128,7 @@ for (const { alias, base, effort } of ISSUE_ALIASES) {
|
||||
}
|
||||
|
||||
test("#8353 parseEffortLevel: unsupported tiers stay null", () => {
|
||||
assert.equal(parseEffortLevel("deepseek-v4-flash-low"), null);
|
||||
assert.equal(parseEffortLevel("deepseek-v4-flash-medium"), null);
|
||||
assert.equal(parseEffortLevel("grok-4.5-max"), null);
|
||||
assert.equal(parseEffortLevel("hy3-max"), null);
|
||||
assert.equal(parseEffortLevel("kimi-k3-high"), null);
|
||||
@@ -155,7 +155,7 @@ test("#8353 parseEffortLevel: MiniMax M3 has no effort-tier aliases", () => {
|
||||
const CREDENTIALS = { apiKey: "k" } as Record<string, unknown>;
|
||||
|
||||
const TRANSFORM_SAMPLES = [
|
||||
{ alias: "deepseek-v4-flash-high", base: "deepseek-v4-flash", effort: "high" },
|
||||
{ alias: "deepseek-v4-flash-low", base: "deepseek-v4-flash", effort: "low" },
|
||||
{ alias: "grok-4.5-medium", base: "grok-4.5", effort: "medium" },
|
||||
{ alias: "hy3-none", base: "hy3", effort: "none" },
|
||||
{ alias: "kimi-k3-max", base: "kimi-k3", effort: "max" },
|
||||
|
||||
Reference in New Issue
Block a user