feat(providers): add DeepSeek V4 thinking effort aliases

This commit is contained in:
excessivechaos
2026-08-04 20:43:54 -07:00
parent 6b531fbacd
commit e9c0d561da
5 changed files with 196 additions and 14 deletions

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,21 @@ 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") {
const isFlash = modelStr.toLowerCase() === "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

@@ -737,6 +737,14 @@ async function buildUnifiedModelsResponseCore(
const visionFields =
getVisionCapabilityFields(aliasId) || getVisionCapabilityFields(model.id);
const thinkingFields = getThinkingCapabilityFields(
canonicalProviderId,
model.id,
model.supportsReasoning,
model.supportedThinkingEfforts
);
const thinkingCapabilities =
Object.keys(thinkingFields).length > 0 ? { capabilities: thinkingFields } : {};
if (includeAlias) {
models.push({
id: aliasId,
@@ -747,6 +755,8 @@ async function buildUnifiedModelsResponseCore(
root: model.id,
parent: null,
...(visionFields || {}),
...thinkingFields,
...thinkingCapabilities,
});
}
if (
@@ -767,6 +777,8 @@ async function buildUnifiedModelsResponseCore(
root: model.id,
parent: includeAlias ? aliasId : null,
...(providerVisionFields || {}),
...thinkingFields,
...thinkingCapabilities,
});
}
}

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
@@ -197,8 +228,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 +260,22 @@ 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(
let { modelId: resolvedModelId, effort } = resolveSyncedModelIdAndEffort(
providerId,
modelId,
syncedModels
);
if (!effort && resolvedModelId === 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,105 @@
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 { 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");
});