feat(providers): add DeepSeek V4 thinking effort aliases (#9485)

* feat(providers): add DeepSeek V4 thinking effort aliases

* docs(changelog): add DeepSeek effort alias entry

* 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

* fix(combos): expose static registry effort tiers in Combo Builder (#9485)

Static provider registry models (e.g. DeepSeek V4 Flash/Pro) declare
supportedThinkingEfforts, but buildModelOptions() only ran
appendSyncedEffortVariants() over DB-synced rows. Synced metadata for a
DeepSeek connection can omit supportedThinkingEfforts, so the catalog/
Playground surfaced the declared aliases while the Combo Builder picker
showed only the bare base ids.

Feed builtInModels with declared effort tiers through the same
appendSyncedEffortVariants() utility used for synced rows, inheriting the
base entry's contextLength/outputTokenLimit/supportedEndpoints/
supportsThinking and preserving its source. DeepSeek is not skipped by
shouldExposeSyncedEffortVariants(), so Flash (none/low/high/max) and Pro
(none/high/max) aliases now appear in the Combo Builder for any connection
whose synced rows omit effort metadata.

Regression test seeds a DeepSeek connection with effort-less synced rows
and asserts the exact alias sets, source preservation, and metadata
inheritance.
This commit is contained in:
Jonathan Bailey
2026-08-11 05:50:55 -07:00
committed by GitHub
parent 45b997765c
commit 84e83e2f19
9 changed files with 448 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, including Combo Builder exposure ([#9485](https://github.com/diegosouzapw/OmniRoute/pull/9485)).

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

@@ -294,17 +294,25 @@ export function sanitizeReasoningEffortForProvider(
return writeEffortValue(b, "max", 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

@@ -763,9 +763,13 @@ async function buildUnifiedModelsResponseCore(
staticModelId: model.id,
syncedModelIds: syncedForProvider ? [...syncedForProvider] : [],
});
const hasDeclaredEffortTiers =
Array.isArray(model.supportedThinkingEfforts) &&
model.supportedThinkingEfforts.length > 0;
if (
coveredBySynced &&
(exclusiveListing || !isRegisteredEffortVariant(providerModels, model.id))
(exclusiveListing ||
(!isRegisteredEffortVariant(providerModels, model.id) && !hasDeclaredEffortTiers))
)
continue;
if (!providerSupportsModel(canonicalProviderId, model.id)) continue;
@@ -776,6 +780,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,
@@ -786,6 +802,8 @@ async function buildUnifiedModelsResponseCore(
root: model.id,
parent: null,
...(visionFields || {}),
...thinkingFields,
...thinkingCapabilities,
});
}
if (
@@ -806,6 +824,8 @@ async function buildUnifiedModelsResponseCore(
root: model.id,
parent: includeAlias ? aliasId : null,
...(providerVisionFields || {}),
...thinkingFields,
...thinkingCapabilities,
});
}
}

View File

@@ -85,19 +85,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

@@ -457,6 +457,55 @@ function buildModelOptions(
});
}
// #9485: static registry models can declare provider-specific effort tiers even
// when a connection's synced row does not include supportedThinkingEfforts.
// Feed those declarations through the same catalog variant utility, while
// copying the merged base option so aliases retain its metadata and source.
const staticCatalogShaped = builtInModels
.filter(
(m): m is RegistryModel & { supportedThinkingEfforts: readonly string[] } =>
typeof m.id === "string" &&
Array.isArray(m.supportedThinkingEfforts) &&
m.supportedThinkingEfforts.length > 0
)
.map((m) => ({
id: `${providerId}/${m.id}`,
owned_by: providerId,
root: m.id,
name: m.name,
capabilities: { effort_tiers: m.supportedThinkingEfforts },
}));
if (staticCatalogShaped.length > 0) {
const baseRawIdByVariantId = new Map<string, string>();
for (const shaped of staticCatalogShaped) {
for (const tier of shaped.capabilities.effort_tiers) {
if (typeof tier === "string" && tier.length > 0) {
baseRawIdByVariantId.set(`${shaped.id}-${tier}`, shaped.root);
}
}
}
const withVariants = appendSyncedEffortVariants(staticCatalogShaped);
for (const variant of withVariants) {
if (typeof variant.id !== "string") continue;
const rawId = variant.id.startsWith(`${providerId}/`)
? variant.id.slice(providerId.length + 1)
: variant.id;
if (modelMap.has(rawId)) continue;
const baseId = baseRawIdByVariantId.get(variant.id) ?? rawId;
const base = modelMap.get(baseId);
addModelOption(modelMap, providerId, {
id: rawId,
name: base ? `${base.name} (${rawId.slice(baseId.length + 1)})` : rawId,
source: base?.source ?? "system",
supportedEndpoints: base?.supportedEndpoints,
contextLength: base?.contextLength ?? null,
outputTokenLimit: base?.outputTokenLimit ?? null,
supportsThinking: base?.supportsThinking,
});
}
}
for (const model of customModels) {
if (model.isHidden === true) continue;
const source = ["api-sync", "auto-sync", "imported"].includes(

View File

@@ -123,6 +123,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
@@ -194,7 +225,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") {
@@ -202,8 +237,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;
}
@@ -226,16 +275,33 @@ 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;
}
// Custom models remain explicit operator overrides even when live discovery
// is authoritative for the provider.
const customMatch = findCustomModelMeta(customModels, resolvedModelId);
const syncedMatch = findSyncedModelMeta(syncedModels, resolvedModelId);
const registryMatch = findRegistryModel(providerId, resolvedModelId);
const effortBaseModelId = getRegisteredProviderEffortBaseModelId(providerId, modelId);
const liveBackedEffortVariant =
@@ -244,7 +310,7 @@ async function lookupModelMeta(
const available =
!liveCatalog.authoritative || Boolean(customMatch || syncedMatch || liveBackedEffortVariant);
const metadata = buildRuntimeModelMeta(customMatch, syncedMatch);
const metadata = buildRuntimeModelMeta(customMatch, syncedMatch, registryMatch);
if (effort) metadata.resolvedThinkingEffort = effort;
return { modelId: resolvedModelId, metadata, available };

View File

@@ -97,3 +97,76 @@ test("#8072 buildModelOptions: synced <model>-<tier> effort variants appear in t
);
}
});
test("#9485 static DeepSeek effort aliases appear when synced rows omit supportedThinkingEfforts", async () => {
const connection = await providersDb.createProviderConnection({
provider: "deepseek",
authType: "apikey",
name: "deepseek-9485-effort",
apiKey: "deepseek-key-9485",
isActive: true,
testStatus: "active",
});
const flashId = "deepseek-v4-flash";
const proId = "deepseek-v4-pro";
const syncedMetadata = {
supportedEndpoints: ["chat"],
inputTokenLimit: 65536,
outputTokenLimit: 16384,
supportsThinking: true,
};
await modelsDb.replaceSyncedAvailableModelsForConnection("deepseek", connection.id, [
{ id: flashId, name: "Synced DeepSeek V4 Flash", ...syncedMetadata },
{ id: proId, name: "Synced DeepSeek V4 Pro", ...syncedMetadata },
]);
const payload = await getComboBuilderOptions();
const provider = payload.providers.find((p) => p.providerId === "deepseek");
assert.ok(provider, "deepseek provider must appear in the combo builder output");
const baseModels = new Map(
[flashId, proId].map((id) => {
const base = provider!.models.find((model) => model.id === id);
assert.ok(base, `${id} base model must appear in the provider's models list`);
return [id, base!];
})
);
const expectedAliases = new Set([
`${flashId}-none`,
`${flashId}-low`,
`${flashId}-high`,
`${flashId}-max`,
`${proId}-none`,
`${proId}-high`,
`${proId}-max`,
]);
const deepSeekAliases = new Set(
provider!.models
.map((model) => model.id)
.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
);
for (const aliasId of expectedAliases) {
const baseId = aliasId.startsWith(`${flashId}-`) ? flashId : proId;
const base = baseModels.get(baseId)!;
const alias = provider!.models.find((model) => model.id === aliasId);
assert.ok(alias, `${aliasId} effort alias must appear in the model picker`);
assert.equal(alias!.source, base.source, `${aliasId} must preserve the base source`);
assert.equal(alias!.contextLength, base.contextLength);
assert.equal(alias!.outputTokenLimit, base.outputTokenLimit);
assert.deepEqual(alias!.supportedEndpoints, base.supportedEndpoints);
assert.equal(alias!.supportsThinking, base.supportsThinking);
}
});

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");
});