Compare commits

...

7 Commits

Author SHA1 Message Date
diegosouzapw
a6e5d47e97 chore(quality): rebaseline catalog.ts file-size cap for #9485 DeepSeek V4 effort aliases
check:file-size failed because the DeepSeek V4 thinking-effort-tier fix grows
src/app/api/v1/models/catalog.ts by 6 lines at the existing static-model
emission chokepoint (a hasDeclaredEffortTiers gate + a call into the existing
getThinkingCapabilityFields helper). All field-computation logic stays in the
non-frozen catalogHelpers.ts leaf; catalog.ts only wires the new
skipCanonicalEffortFallback flag through. Bumps the frozen cap 1549->1555
with a justification entry, mirroring prior thinking/vision-field
rebaselines at the same chokepoint.

Co-authored-by: excessivechaos <excessivechaos@users.noreply.github.com>
2026-08-05 19:56:17 -03:00
Diego Rodrigues de Sa e Souza
d4d2cd28c6 Merge branch 'release/v3.8.50' into fix/deepseek-thinking-efforts 2026-08-05 16:23:31 -03:00
Diego Rodrigues de Sa e Souza
52603fcffb Merge branch 'release/v3.8.50' into fix/deepseek-thinking-efforts 2026-08-05 13:20:56 -03:00
excessivechaos
dd44abf28a fix(catalog): scope effort-tier fallback to declared models and harden resolver
Addresses reviewer findings on #9485:

- CRITICAL #1: catalog no longer synthesizes unresolvable effort aliases for
  static reasoning models without declared tiers (cheaperinference, cline, etc.)
- CRITICAL #2: tiered static models survive synced-coverage suppression so
  normal installs with synced DeepSeek base models still expose aliases
- WARNING #3: registry suffix resolution short-circuits when the raw id matches
  a direct custom or synced model, preserving custom apiFormat/targetFormat
- WARNING #4: empty synced effort array no longer erases the registry fallback
- WARNING #5: isFlash check is robust to suffixed/prefixed model ids
- Added regression tests for blast radius, custom-model shadowing, none-path,
  and suffixed isFlash
2026-08-05 09:18:32 -07:00
Diego Rodrigues de Sa e Souza
0334695bff Merge branch 'release/v3.8.50' into fix/deepseek-thinking-efforts 2026-08-05 12:00:04 -03:00
excessivechaos
4da2dbe019 docs(changelog): add DeepSeek effort alias entry 2026-08-04 20:45:10 -07:00
excessivechaos
e9c0d561da feat(providers): add DeepSeek V4 thinking effort aliases 2026-08-04 20:43:54 -07:00
8 changed files with 328 additions and 23 deletions

View File

@@ -0,0 +1 @@
- **feat(providers):** add native DeepSeek V4 Flash and Pro thinking-effort aliases for their documented per-model tiers ([#9485](https://github.com/diegosouzapw/OmniRoute/pull/9485)).

View File

@@ -1,4 +1,5 @@
{
"_rebaseline_2026_08_05_9485_deepseek_v4_effort_aliases": "PR #9485 (excessivechaos, fix/deepseek-thinking-efforts) own growth: src/app/api/v1/models/catalog.ts 1549->1555 (+6, check-file-size.mjs counts via split(\"\\n\").length) at the existing static-model emission chokepoint in buildUnifiedModelsResponseCore. Adds a `hasDeclaredEffortTiers` gate (skip the synced-coverage suppression when the static model declares its own supportedThinkingEfforts list) and calls the existing getThinkingCapabilityFields(...) helper — now with a new skipCanonicalEffortFallback flag — to spread thinking/effort_tiers fields onto both the alias and provider-prefixed model entries. DeepSeek V4's thinking-effort tiers (minimal/low/medium/high) were being silently suppressed by the synced-model-coverage guard added for #7786, and any effort variant not explicitly declared fell back to synthesizing unresolvable `<prefix>/<model>-{tier}` ids for every static reasoning model. All the actual field-computation logic (hasDeclaredTiers, effort_tiers selection, extendCodexGpt56EffortValues fallback) lives in the non-frozen leaf src/app/api/v1/models/catalogHelpers.ts::getThinkingCapabilityFields; catalog.ts only adds the 5-line gate + destructure + spread at its single static-model push site. Cohesive wiring at the existing chokepoint, mirroring prior thinking/vision-field rebaselines (#4264 supportsVision, #6218-era supportsThinking) at the same site; not extractable further without hiding the per-model field-merge boundary. Covered by tests/unit/deepseek-thinking-efforts.test.ts.",
"_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent's conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR's own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
@@ -388,7 +389,7 @@
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
"src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1109,
"src/app/api/providers/[id]/models/route.ts": 2250,
"src/app/api/v1/models/catalog.ts": 1549,
"src/app/api/v1/models/catalog.ts": 1555,
"src/lib/tokenHealthCheck.ts": 1021,
"src/lib/db/apiKeys.ts": 1529,
"src/lib/db/core.ts": 1637,

View File

@@ -9,7 +9,17 @@ export const deepseekProvider: RegistryEntry = {
authType: "apikey",
authHeader: "bearer",
models: [
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", supportsReasoning: true },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", supportsReasoning: true },
{
id: "deepseek-v4-pro",
name: "DeepSeek V4 Pro",
supportsReasoning: true,
supportedThinkingEfforts: ["none", "high", "max"],
},
{
id: "deepseek-v4-flash",
name: "DeepSeek V4 Flash",
supportsReasoning: true,
supportedThinkingEfforts: ["none", "low", "high", "max"],
},
],
};

View File

@@ -265,17 +265,25 @@ export function sanitizeReasoningEffortForProvider(
return stripEffortValue(b, c);
}
// Native DeepSeek (api.deepseek.com) — V4 thinking mode accepts reasoning_effort
// ONLY as {high, max} (its own top tier is literally "max"). OmniRoute's internal
// scale is low|medium|high|xhigh where xhigh is the top, so map onto DeepSeek's
// vocabulary: xhigh → max (top→top), low|medium → high (below the enum floor).
// high/max pass through unchanged. Without this, the claude→openai translator's
// xhigh (and max-normalized-to-xhigh below) reaches DeepSeek as an unknown value,
// silently dropping the client's requested effort. This is the INVERSE of the
// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max (pi#4055).
// 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.
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 mapped =
effortStr === "xhigh" ? "max" : effortStr === "low" || effortStr === "medium" ? "high" : null;
effortStr === "xhigh"
? "max"
: effortStr === "medium" || (effortStr === "low" && !isFlash)
? "high"
: null;
if (mapped && mapped !== effortStr) {
log?.info?.(
"REASONING_SANITIZE",

View File

@@ -728,13 +728,17 @@ async function buildUnifiedModelsResponseCore(
// the fix, a provider with any synced model silently dropped ALL its
// static models.
const syncedForProvider = syncedModelIdsByCanonicalProvider.get(canonicalProviderId);
const hasDeclaredEffortTiers =
Array.isArray(model.supportedThinkingEfforts) &&
model.supportedThinkingEfforts.length > 0;
if (
shouldSuppressStaticModelBySyncedCoverage({
providerHasSynced: syncedForProvider !== undefined && syncedForProvider.size > 0,
staticModelId: model.id,
syncedModelIds: syncedForProvider ? [...syncedForProvider] : [],
}) &&
!isRegisteredEffortVariant(providerModels, model.id)
!isRegisteredEffortVariant(providerModels, model.id) &&
!hasDeclaredEffortTiers
)
continue;
if (!providerSupportsModel(canonicalProviderId, model.id)) continue;
@@ -745,6 +749,18 @@ async function buildUnifiedModelsResponseCore(
const visionFields =
getVisionCapabilityFields(aliasId) || getVisionCapabilityFields(model.id);
const thinkingFields = getThinkingCapabilityFields(
canonicalProviderId,
model.id,
model.supportsReasoning,
model.supportedThinkingEfforts,
// Skip the canonical fallback for static models without declared tiers —
// otherwise the catalog synthesizes unresolvable `<prefix>/<model>-{tier}`
// ids for every static reasoning model across all providers (#9485 review).
!hasDeclaredEffortTiers
);
const thinkingCapabilities =
Object.keys(thinkingFields).length > 0 ? { capabilities: thinkingFields } : {};
if (includeAlias) {
models.push({
id: aliasId,
@@ -755,6 +771,8 @@ async function buildUnifiedModelsResponseCore(
root: model.id,
parent: null,
...(visionFields || {}),
...thinkingFields,
...thinkingCapabilities,
});
}
if (
@@ -775,6 +793,8 @@ async function buildUnifiedModelsResponseCore(
root: model.id,
parent: includeAlias ? aliasId : null,
...(providerVisionFields || {}),
...thinkingFields,
...thinkingCapabilities,
});
}
}

View File

@@ -84,19 +84,24 @@ export function getThinkingCapabilityFields(
providerId: string,
modelId: string,
resolvedThinking?: boolean | null,
supportedThinkingEfforts?: readonly string[]
supportedThinkingEfforts?: readonly string[],
/** When true, skip the canonical effort-tier fallback — used for static registry
* models that declare `supportsReasoning` but no explicit tier list, so the
* catalog does not synthesize unresolvable `<prefix>/<model>-{tier}` ids. */
skipCanonicalEffortFallback = false
): Record<string, boolean | string[]> {
const supportsThinking = resolvedThinking;
if (typeof supportsThinking !== "boolean") return {};
const hasDeclaredTiers =
supportedThinkingEfforts && supportedThinkingEfforts.length > 0;
return {
thinking: supportsThinking,
supportsThinking,
...(supportsThinking
...(supportsThinking && (hasDeclaredTiers || !skipCanonicalEffortFallback)
? {
effort_tiers:
supportedThinkingEfforts && supportedThinkingEfforts.length > 0
? [...supportedThinkingEfforts]
: extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES),
effort_tiers: hasDeclaredTiers
? [...supportedThinkingEfforts!]
: extendCodexGpt56EffortValues(providerId, modelId, CANONICAL_EFFORT_VALUES),
}
: {}),
};

View File

@@ -121,6 +121,37 @@ function isSyncedEffortSkippedProvider(providerId: string): boolean {
return SYNCED_EFFORT_SKIP_PROVIDER_PREFIXES.some((prefix) => providerId.startsWith(prefix));
}
/** Resolve a suffix against an explicitly tiered static registry model. */
function resolveRegistryModelIdAndEffort(
providerId: string,
modelId: string
): { modelId: string; effort: string | null } {
if (isSyncedEffortSkippedProvider(providerId)) return { modelId, effort: null };
const registryModels = REGISTRY[providerId]?.models;
if (!Array.isArray(registryModels)) return { modelId, effort: null };
if (registryModels.some((candidate) => candidate?.id === modelId)) {
return { modelId, effort: null };
}
for (const candidate of registryModels) {
if (!Array.isArray(candidate?.supportedThinkingEfforts)) continue;
const attempt = splitSyncedEffortSuffix(modelId, candidate.supportedThinkingEfforts);
if (attempt.effort && attempt.baseModel === candidate.id) {
return { modelId: attempt.baseModel, effort: attempt.effort };
}
}
return { modelId, effort: null };
}
function findRegistryModel(providerId: string, modelId: string): any {
const registryModels = REGISTRY[providerId]?.models;
return Array.isArray(registryModels)
? registryModels.find((candidate) => candidate?.id === modelId)
: undefined;
}
/**
* #7694: when `modelId` has no direct synced-model match, try stripping a trailing
* `-{effort}` token by testing it against each candidate synced model's OWN declared
@@ -189,7 +220,11 @@ function copySyncedThinkingMetadata(metadata: RuntimeModelMeta, syncedMatch: any
metadata.supportsThinking = syncedMatch.supportsThinking;
}
if (syncedMatch?.alwaysThinking === true) metadata.alwaysThinking = true;
if (Array.isArray(syncedMatch?.supportedThinkingEfforts)) {
// Only let a non-empty synced effort list override the static registry fallback;
// an empty array from an incomplete synced discovery must not erase registry-declared
// tiers (#9485 review).
if (Array.isArray(syncedMatch?.supportedThinkingEfforts) &&
syncedMatch.supportedThinkingEfforts.length > 0) {
metadata.supportedThinkingEfforts = syncedMatch.supportedThinkingEfforts;
}
if (typeof syncedMatch?.defaultThinkingEffort === "string") {
@@ -197,8 +232,22 @@ function copySyncedThinkingMetadata(metadata: RuntimeModelMeta, syncedMatch: any
}
}
function buildRuntimeModelMeta(customMatch: any, syncedMatch: any): RuntimeModelMeta {
function copyRegistryThinkingMetadata(metadata: RuntimeModelMeta, registryMatch: any): void {
if (typeof registryMatch?.supportsReasoning === "boolean") {
metadata.supportsThinking = registryMatch.supportsReasoning;
}
if (Array.isArray(registryMatch?.supportedThinkingEfforts)) {
metadata.supportedThinkingEfforts = [...registryMatch.supportedThinkingEfforts];
}
}
function buildRuntimeModelMeta(
customMatch: any,
syncedMatch: any,
registryMatch: any
): RuntimeModelMeta {
const metadata = resolveRuntimeFormats(customMatch, syncedMatch);
copyRegistryThinkingMetadata(metadata, registryMatch);
copySyncedThinkingMetadata(metadata, syncedMatch);
return metadata;
}
@@ -215,16 +264,34 @@ async function lookupModelMeta(
// #7694: no direct match on the raw modelId? try a synced-declared `-{effort}`
// suffix before falling back to the literal id, so `<prefix>/<model>-<tier>`
// resolves to the real base model + a resolved effort.
const { modelId: resolvedModelId, effort } = resolveSyncedModelIdAndEffort(
// #7694: no direct match on the raw modelId? try a synced-declared `-{effort}`
// suffix before falling back to the literal id, so `<prefix>/<model>-<tier>`
// resolves to the real base model + a resolved effort.
let { modelId: resolvedModelId, effort } = resolveSyncedModelIdAndEffort(
providerId,
modelId,
syncedModels
);
// Short-circuit registry suffix resolution when the raw id is already a direct
// custom or synced model — otherwise a model literally named
// `deepseek-v4-flash-low` gets rewritten to `deepseek-v4-flash` + effort `low`
// and its custom/synced metadata (apiFormat/targetFormat) is dropped (#9485 review).
if (
!effort &&
resolvedModelId === modelId &&
!findCustomModelMeta(customModels, modelId) &&
!findSyncedModelMeta(syncedModels, modelId)
) {
const registryResolution = resolveRegistryModelIdAndEffort(providerId, modelId);
resolvedModelId = registryResolution.modelId;
effort = registryResolution.effort;
}
// #7364: exact match first; retain the case-insensitive custom-model fallback
// while also consulting the API-synced catalog for Kimi runtime metadata.
const customMatch = findCustomModelMeta(customModels, resolvedModelId);
const syncedMatch = findSyncedModelMeta(syncedModels, resolvedModelId);
const metadata = buildRuntimeModelMeta(customMatch, syncedMatch);
const registryMatch = findRegistryModel(providerId, resolvedModelId);
const metadata = buildRuntimeModelMeta(customMatch, syncedMatch, registryMatch);
if (effort) metadata.resolvedThinkingEffort = effort;
return { modelId: resolvedModelId, metadata };
} catch {

View File

@@ -0,0 +1,193 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-deepseek-efforts-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "deepseek-efforts-test-secret";
const core = await import("../../src/lib/db/core.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const modelsDb = await import("../../src/lib/db/models.ts");
const { getModelInfo } = await import("../../src/sse/services/model.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
const { sanitizeReasoningEffortForProvider } = await import("../../open-sse/executors/base.ts");
test.beforeEach(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
});
test.after(() => {
core.resetDbInstance();
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 catalog exposes only the declared effort aliases", async () => {
await providersDb.createProviderConnection({
provider: "deepseek",
authType: "apikey",
name: "deepseek-efforts",
apiKey: "deepseek-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 }> };
const ids = new Set(body.data.map((model) => model.id));
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-none")));
assert.ok([...ids].some((id) => id.endsWith("deepseek-v4-flash-low")));
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-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("hardcoded DeepSeek effort suffixes resolve through the static registry", async () => {
const flashLow = await getModelInfo("ds/deepseek-v4-flash-low");
assert.equal(flashLow.provider, "deepseek");
assert.equal(flashLow.model, "deepseek-v4-flash");
assert.equal(flashLow.resolvedThinkingEffort, "low");
const flashNone = await getModelInfo("deepseek/deepseek-v4-flash-none");
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);
});
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");
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("non-DeepSeek static reasoning models do not advertise unresolvable effort aliases", async () => {
// cheaperinference declares deepseek-v4-flash/pro with supportsReasoning: true
// but no supportedThinkingEfforts — the catalog must NOT synthesize
// cheaperinference/deepseek-v4-flash-{low,high,...} ids for them (#9485 review #1).
await providersDb.createProviderConnection({
provider: "cheaperinference",
authType: "apikey",
name: "cheaperinference-blast-radius",
apiKey: "cheaperinference-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 }> };
const ids = body.data.map((model) => model.id);
// Static base models for cheaperinference should still be present
assert.ok(
ids.some((id) => id.endsWith("cheaperinference/deepseek-v4-flash")),
"cheaperinference/deepseek-v4-flash base entry should still be present"
);
// But NO effort-suffixed aliases should be synthesized
assert.equal(
ids.some((id) => /cheaperinference\/deepseek-v4-flash-(none|low|medium|high|max|xhigh)$/.test(id)),
false,
"cheaperinference static reasoning models must not advertise unresolvable effort aliases"
);
assert.equal(
ids.some((id) => /cheaperinference\/deepseek-v4-pro-(none|low|medium|high|max|xhigh)$/.test(id)),
false,
"cheaperinference static reasoning models must not advertise unresolvable effort aliases"
);
});
test("custom model named deepseek-v4-flash-low is not rewritten by registry suffix resolution", async () => {
// A custom (DB) model literally named deepseek-v4-flash-low on the deepseek
// provider must not be silently rewritten to deepseek-v4-flash + effort low,
// which would drop its custom apiFormat/targetFormat metadata (#9485 review #3).
await modelsDb.addCustomModel(
"deepseek",
"deepseek-v4-flash-low",
"deepseek-v4-flash-low",
"manual",
"responses",
["chat"],
"responses"
);
const info = await getModelInfo("ds/deepseek-v4-flash-low");
// The model id should be preserved as the literal custom id, not rewritten
assert.equal(info.model, "deepseek-v4-flash-low");
// The custom apiFormat must survive (not dropped by registry rewriting)
assert.equal(info.apiFormat, "responses");
// No resolved effort should be injected — this is a distinct custom model
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).
const flashNone = await getModelInfo("ds/deepseek-v4-flash-none");
assert.equal(flashNone.model, "deepseek-v4-flash");
assert.equal(flashNone.resolvedThinkingEffort, "none");
const sanitized = sanitizeReasoningEffortForProvider(
{ model: "deepseek-v4-flash", reasoning_effort: "none" },
"deepseek",
"deepseek-v4-flash"
) as Record<string, unknown>;
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).
const sanitizedSuffixed = sanitizeReasoningEffortForProvider(
{ model: "deepseek-v4-flash-low", reasoning_effort: "low" },
"deepseek",
"deepseek-v4-flash-low"
) as Record<string, unknown>;
assert.equal(sanitizedSuffixed.reasoning_effort, "low");
});