mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-24 16:12:23 +03:00
fix(models): expose Ollama Cloud native effort tiers (#11307)
Validated on a 17-PR combined board: models-catalog-combo-metadata + ollama-cloud-reasoning-effort-tiers-10788 within the board's 287/287, typecheck:core clean, check:open-sse-typecheck clean, vitest 405/405. Publishes Ollama Cloud's native none/low/medium/high/max effort vocabulary for reasoning-capable passthrough/tagged models with no exact registry declaration, adds none to DeepSeek V4/GLM 5.x, and preserves narrower exact-model vocabularies (GPT-OSS) via intersection. Refs #10788. Thank you @ekinnee!
This commit is contained in:
@@ -181,6 +181,29 @@ export function getRegistryEntry(provider: string): RegistryEntry | null {
|
||||
return REGISTRY[provider] || _byAlias.get(provider) || null;
|
||||
}
|
||||
|
||||
/** Resolve only a model's explicit reasoning vocabulary. */
|
||||
export function getRegistryModelThinkingEfforts(
|
||||
provider: string,
|
||||
modelId: string
|
||||
): readonly string[] | undefined {
|
||||
const entry = getRegistryEntry(provider);
|
||||
if (!entry) return undefined;
|
||||
const model = entry.models.find((candidate) => candidate.id === modelId);
|
||||
return model?.supportedThinkingEfforts;
|
||||
}
|
||||
|
||||
/** Resolve a model's explicit reasoning vocabulary before its provider fallback. */
|
||||
export function getRegistryThinkingEfforts(
|
||||
provider: string,
|
||||
modelId: string
|
||||
): readonly string[] | undefined {
|
||||
const entry = getRegistryEntry(provider);
|
||||
if (!entry) return undefined;
|
||||
const modelEfforts = getRegistryModelThinkingEfforts(provider, modelId);
|
||||
if (modelEfforts !== undefined) return modelEfforts;
|
||||
return entry.defaultSupportedThinkingEfforts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether a non-empty live catalog may exclude omitted static models
|
||||
* during request routing and wildcard expansion.
|
||||
|
||||
@@ -9,6 +9,7 @@ export const ollama_cloudProvider: RegistryEntry = {
|
||||
modelsUrl: "https://ollama.com/api/tags",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
defaultSupportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
// Note: rate limits vary by plan (free = "Light usage", Pro = more, Max = 5x Pro).
|
||||
// Users can generate API keys at https://ollama.com/settings/keys
|
||||
models: [
|
||||
@@ -24,23 +25,20 @@ export const ollama_cloudProvider: RegistryEntry = {
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high"],
|
||||
},
|
||||
// #10788: Ollama Cloud accepts low|medium|high|max|none uniformly across
|
||||
// its reasoning-capable models (see supportsMaxEffortForProvider's
|
||||
// isOllamaCloud comment in open-sse/executors/base/reasoningEffort.ts) —
|
||||
// declare supportedThinkingEfforts so appendSyncedEffortVariants() (which
|
||||
// runs before static-model capability enrichment) can synthesize the
|
||||
// catalog's selectable -low/-high/-max variant ids for these models.
|
||||
// #10788: these models accept none|low|medium|high|max. Keep their explicit
|
||||
// declarations aligned with the provider fallback so the static and synced
|
||||
// catalog paths expose the same native vocabulary.
|
||||
{
|
||||
id: "deepseek-v4-pro",
|
||||
name: "DeepSeek V4 Pro",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
id: "deepseek-v4-flash",
|
||||
name: "DeepSeek V4 Flash",
|
||||
supportsReasoning: true,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6" },
|
||||
// Ollama Cloud accepts low|medium|high|max|none and rejects xhigh, so the
|
||||
@@ -50,14 +48,14 @@ export const ollama_cloudProvider: RegistryEntry = {
|
||||
name: "GLM 5.1",
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: false,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
{
|
||||
id: "glm-5.2",
|
||||
name: "GLM 5.2",
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: false,
|
||||
supportedThinkingEfforts: ["low", "medium", "high", "max"],
|
||||
supportedThinkingEfforts: ["none", "low", "medium", "high", "max"],
|
||||
},
|
||||
// #3110: MiniMax M3 via Ollama
|
||||
{ id: "minimax-m3", name: "MiniMax M3", contextLength: 1048576, supportsVision: true },
|
||||
|
||||
@@ -139,6 +139,9 @@ export interface RegistryEntry {
|
||||
requestDefaults?: ProviderRequestDefaults;
|
||||
oauth?: RegistryOAuth;
|
||||
models: RegistryModel[];
|
||||
/** Provider-native reasoning vocabulary for reasoning-capable passthrough models
|
||||
* that do not have an explicit per-model declaration. */
|
||||
defaultSupportedThinkingEfforts?: readonly string[];
|
||||
modelsUrl?: string;
|
||||
/** Prefix to prepend to model IDs before upstream API calls (e.g. "accounts/fireworks/models/") */
|
||||
modelIdPrefix?: string;
|
||||
|
||||
@@ -28,7 +28,11 @@ import { getAllAudioModels } from "@omniroute/open-sse/config/audioRegistry";
|
||||
import { getAllModerationModels } from "@omniroute/open-sse/config/moderationRegistry";
|
||||
import { getAllVideoModels } from "@omniroute/open-sse/config/videoRegistry";
|
||||
import { getAllMusicModels } from "@omniroute/open-sse/config/musicRegistry";
|
||||
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry";
|
||||
import {
|
||||
getRegistryModelThinkingEfforts,
|
||||
getRegistryThinkingEfforts,
|
||||
REGISTRY,
|
||||
} from "@omniroute/open-sse/config/providerRegistry";
|
||||
import { CODEX_NATIVE_UNPREFIXED_MODELS } from "@omniroute/open-sse/services/model";
|
||||
import { isModelSelectable } from "@omniroute/open-sse/services/modelLifecycle";
|
||||
import { resolveNestedComboTargets } from "@omniroute/open-sse/services/combo";
|
||||
@@ -585,7 +589,9 @@ async function buildUnifiedModelsResponseCore(
|
||||
modelId,
|
||||
target,
|
||||
eligibleConnectionIds,
|
||||
connectionCatalog || {}
|
||||
connectionCatalog || {},
|
||||
getRegistryModelThinkingEfforts(providerId, modelId),
|
||||
getRegistryThinkingEfforts(providerId, modelId)
|
||||
);
|
||||
if (
|
||||
connectionEfforts === undefined &&
|
||||
@@ -669,7 +675,7 @@ async function buildUnifiedModelsResponseCore(
|
||||
providerId,
|
||||
modelId,
|
||||
canonical.capabilities.supportsThinking,
|
||||
registryModel?.supportedThinkingEfforts,
|
||||
getRegistryThinkingEfforts(providerId, modelId),
|
||||
true
|
||||
)
|
||||
: getThinkingCapabilityFields(
|
||||
|
||||
@@ -36,6 +36,7 @@ export type ComboCatalogTarget = {
|
||||
|
||||
type ConnectionScopedReasoningModel = {
|
||||
id: string;
|
||||
supportsThinking?: boolean;
|
||||
supportedThinkingEfforts?: string[];
|
||||
};
|
||||
|
||||
@@ -106,7 +107,9 @@ export function getConnectionScopedEffortTiers(
|
||||
modelId: string,
|
||||
target: Pick<ComboCatalogTarget, "connectionId" | "allowedConnectionIds">,
|
||||
eligibleConnectionIds: readonly string[] | undefined,
|
||||
modelsByConnection: ConnectionScopedReasoningCatalog
|
||||
modelsByConnection: ConnectionScopedReasoningCatalog,
|
||||
explicitThinkingEfforts?: readonly string[],
|
||||
fallbackThinkingEfforts?: readonly string[]
|
||||
): string[] | undefined {
|
||||
const eligible = eligibleConnectionIds ? new Set(eligibleConnectionIds) : undefined;
|
||||
if (target.connectionId && eligible && !eligible.has(target.connectionId)) return [];
|
||||
@@ -139,7 +142,16 @@ export function getConnectionScopedEffortTiers(
|
||||
);
|
||||
if (matching.some((model) => model === undefined)) return [];
|
||||
|
||||
const efforts = matching.map((model) => model?.supportedThinkingEfforts || []);
|
||||
const efforts = matching.map((model) => {
|
||||
const resolved = model?.supportedThinkingEfforts?.length
|
||||
? model.supportedThinkingEfforts
|
||||
: model?.supportsThinking === true && fallbackThinkingEfforts
|
||||
? [...fallbackThinkingEfforts]
|
||||
: [];
|
||||
return explicitThinkingEfforts
|
||||
? explicitThinkingEfforts.filter((effort) => resolved.includes(effort))
|
||||
: resolved;
|
||||
});
|
||||
return intersectStringArrays(efforts);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,14 @@
|
||||
// refactors. (Confirmed convention: grep "from \"@omniroute/open-sse" src/app/api/v1/models/)
|
||||
import { getLearnedReasoningEffortForModel } from "@omniroute/open-sse/services/learnedReasoningEffortCaps.ts";
|
||||
import { isSkippedEffortProvider } from "@omniroute/open-sse/utils/syncedEffortVariants.ts";
|
||||
import {
|
||||
getRegistryModelThinkingEfforts,
|
||||
getRegistryThinkingEfforts,
|
||||
} from "@omniroute/open-sse/config/providerRegistry.ts";
|
||||
|
||||
interface SyncedCapabilityFlags {
|
||||
id?: string;
|
||||
supportsThinking?: boolean;
|
||||
supportsVision?: boolean;
|
||||
supportedThinkingEfforts?: string[];
|
||||
}
|
||||
@@ -37,10 +42,23 @@ interface SyncedCapabilityFlags {
|
||||
function effectiveEffortTiers(sm: SyncedCapabilityFlags, ownedBy: string): string[] | undefined {
|
||||
if (isSkippedEffortProvider(ownedBy)) return undefined;
|
||||
const learned = sm.id ? getLearnedReasoningEffortForModel(sm.id) : null;
|
||||
const synced =
|
||||
Array.isArray(sm.supportedThinkingEfforts) && sm.supportedThinkingEfforts.length > 0
|
||||
? sm.supportedThinkingEfforts
|
||||
: null;
|
||||
const explicit = sm.id ? getRegistryModelThinkingEfforts(ownedBy, sm.id) : undefined;
|
||||
if (explicit) {
|
||||
const observed = learned ? [...learned] : synced;
|
||||
const narrowed = observed
|
||||
? explicit.filter((effort) => observed.includes(effort))
|
||||
: [...explicit];
|
||||
return narrowed.length > 0 ? narrowed : undefined;
|
||||
}
|
||||
if (learned) return [...learned];
|
||||
return Array.isArray(sm.supportedThinkingEfforts) && sm.supportedThinkingEfforts.length > 0
|
||||
? sm.supportedThinkingEfforts
|
||||
: undefined;
|
||||
if (synced) return synced;
|
||||
if (!sm.supportsThinking || !sm.id) return undefined;
|
||||
const registryEfforts = getRegistryThinkingEfforts(ownedBy, sm.id);
|
||||
return registryEfforts && registryEfforts.length > 0 ? [...registryEfforts] : undefined;
|
||||
}
|
||||
|
||||
/** Build the `capabilities` object for a fresh synced-model catalog entry, or `undefined` when neither flag applies. */
|
||||
|
||||
@@ -567,3 +567,64 @@ test("mixed DeepSeek combos advertise the efforts accepted by every V4 target",
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("Ollama Cloud projects native efforts for base, tagged, and combo models", async () => {
|
||||
const provider = "ollama-cloud";
|
||||
const baseModel = "deepseek-v4-flash";
|
||||
const taggedModel = "deepseek-v4-flash:0731";
|
||||
const narrowModel = "gpt-oss:20b";
|
||||
const nativeEfforts = ["none", "low", "medium", "high", "max"];
|
||||
const narrowEfforts = ["low", "medium", "high"];
|
||||
const connection = await providersDb.createProviderConnection({
|
||||
provider,
|
||||
authType: "apikey",
|
||||
name: "ollama-cloud-native-efforts",
|
||||
apiKey: "ollama-cloud-test-key",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
});
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection(provider, connection.id, [
|
||||
{ id: baseModel, name: "DeepSeek V4 Flash", supportsThinking: true },
|
||||
{ id: taggedModel, name: "DeepSeek V4 Flash 0731", supportsThinking: true },
|
||||
{
|
||||
id: narrowModel,
|
||||
name: "GPT-OSS 20B",
|
||||
supportsThinking: true,
|
||||
supportedThinkingEfforts: nativeEfforts,
|
||||
},
|
||||
]);
|
||||
await combosDb.createCombo({
|
||||
name: "ollama-cloud-native-efforts-combo",
|
||||
strategy: "auto",
|
||||
models: [`${provider}/${baseModel}`, `${provider}/${taggedModel}`],
|
||||
});
|
||||
await combosDb.createCombo({
|
||||
name: "ollama-cloud-narrow-efforts-combo",
|
||||
strategy: "auto",
|
||||
models: [`${provider}/${narrowModel}`],
|
||||
});
|
||||
|
||||
const response = await catalog.getUnifiedModelsResponse(
|
||||
new Request("http://localhost/api/v1/models")
|
||||
);
|
||||
const body = (await response.json()) as { data: Array<Record<string, unknown>> };
|
||||
const capabilitiesFor = (modelId: string) => {
|
||||
const model = body.data.find((item) => item.id === modelId);
|
||||
assert.ok(model, modelId);
|
||||
return model.capabilities as Record<string, unknown>;
|
||||
};
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
for (const modelId of [
|
||||
`ollamacloud/${baseModel}`,
|
||||
`ollamacloud/${taggedModel}`,
|
||||
"ollama-cloud-native-efforts-combo",
|
||||
]) {
|
||||
const effortTiers = capabilitiesFor(modelId).effort_tiers;
|
||||
assert.deepEqual(effortTiers, nativeEfforts, modelId);
|
||||
assert.equal((effortTiers as string[]).includes("xhigh"), false, modelId);
|
||||
}
|
||||
for (const modelId of [`ollamacloud/${narrowModel}`, "ollama-cloud-narrow-efforts-combo"]) {
|
||||
assert.deepEqual(capabilitiesFor(modelId).effort_tiers, narrowEfforts, modelId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ollama_cloudProvider } from "../../open-sse/config/providers/registry/ollama-cloud/index.ts";
|
||||
import { getRegistryThinkingEfforts } from "../../open-sse/config/providerRegistry.ts";
|
||||
|
||||
// #10788: ollama-cloud declared supportsReasoning:true on several models
|
||||
// (glm-5.1/5.2, deepseek-v4-pro/flash) but never declared
|
||||
@@ -19,6 +20,7 @@ test("#10788: ollama-cloud reasoning-capable models declare supportedThinkingEff
|
||||
Array.isArray(control?.supportedThinkingEfforts) && control.supportedThinkingEfforts.length > 0,
|
||||
"control: gpt-oss:20b should already declare supportedThinkingEfforts"
|
||||
);
|
||||
assert.deepEqual(control.supportedThinkingEfforts, ["low", "medium", "high"]);
|
||||
|
||||
const reasoningModelIds = ["glm-5.1", "glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash"];
|
||||
for (const id of reasoningModelIds) {
|
||||
@@ -33,8 +35,29 @@ test("#10788: ollama-cloud reasoning-capable models declare supportedThinkingEff
|
||||
// low|medium|high|max|none — xhigh is rejected and mapped to max.
|
||||
assert.deepEqual(
|
||||
[...(model?.supportedThinkingEfforts ?? [])],
|
||||
["low", "medium", "high", "max"],
|
||||
`${id} should declare Ollama Cloud's documented low/medium/high/max vocabulary`
|
||||
["none", "low", "medium", "high", "max"],
|
||||
`${id} should declare Ollama Cloud's documented none/low/medium/high/max vocabulary`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("#10788: provider fallback preserves explicit and unrelated vocabularies", () => {
|
||||
assert.deepEqual(getRegistryThinkingEfforts("ollama-cloud", "deepseek-v4-flash:0731"), [
|
||||
"none",
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
assert.deepEqual(getRegistryThinkingEfforts("ollama-cloud", "gpt-oss:20b"), [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
]);
|
||||
assert.deepEqual(getRegistryThinkingEfforts("deepseek", "deepseek-v4-flash"), [
|
||||
"none",
|
||||
"low",
|
||||
"high",
|
||||
"max",
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user