feat(api): sync upstream reasoning.supported_efforts into synced-model catalog (#7694) (#7767)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-19 20:52:54 -03:00
committed by GitHub
parent e8a6123169
commit 7b85e1f6f7
10 changed files with 733 additions and 35 deletions

View File

@@ -0,0 +1 @@
- Generic OpenAI-compatible model sync now captures a provider's upstream `reasoning.supported_efforts` (in addition to the pre-existing flat `supportedThinkingEfforts` shape), surfaces it in the model catalog as `capabilities.effort_tiers`, and advertises `<prefix>/<model>-{effort}` request-time alias ids for catalog-only clients — mirroring the existing per-model reasoning-effort plumbing (#6879). Codex and Kimi are excluded since they already own their own effort-suffix mechanism.

View File

@@ -2189,9 +2189,16 @@ export async function handleChatCore({
// carries no reasoning field of any shape — an explicit client/combo-leg value
// always wins. Scoped to the OpenAI Chat Completions dispatch shape (the shape
// `reasoning_effort` is native to); unset ModelSpec.defaultReasoningEffort is a
// no-op. See open-sse/services/defaultReasoningEffort.ts.
// no-op. #7694: `modelInfo.resolvedThinkingEffort` — set when the request's model
// id carried a `<prefix>/<model>-{effort}` synced-model alias suffix
// (`src/sse/services/model.ts`) — takes priority over the static per-model default.
// See open-sse/services/defaultReasoningEffort.ts.
if (targetFormat === FORMATS.OPENAI) {
translatedBody = applyDefaultReasoningEffort(translatedBody, finalModelToUpstream);
translatedBody = applyDefaultReasoningEffort(
translatedBody,
finalModelToUpstream,
(modelInfo as { resolvedThinkingEffort?: string })?.resolvedThinkingEffort
);
}
}

View File

@@ -24,18 +24,24 @@ function hasExplicitReasoningField(body: Record<string, unknown>): boolean {
}
/**
* Inject the resolved model's `defaultReasoningEffort` as `reasoning_effort` when the
* request has no reasoning field. Returns `body` unchanged (same reference) when there
* is nothing to inject, so callers can chain it without extra guards.
* Inject a resolved reasoning effort as `reasoning_effort` when the request has no
* reasoning field. Returns `body` unchanged (same reference) when there is nothing to
* inject, so callers can chain it without extra guards.
*
* `suffixEffort` (#7694) is the tier a `<prefix>/<model>-{effort}` synced-model alias
* resolved to (`src/sse/services/model.ts`'s `resolveSyncedModelIdAndEffort`) — an
* explicit, request-time model selection, so it takes priority over the static
* `ModelSpec.defaultReasoningEffort` fleet-wide default (#6879) when both are present.
*/
export function applyDefaultReasoningEffort<T extends Record<string, unknown>>(
body: T,
modelId: string
modelId: string,
suffixEffort?: string | null
): T {
if (!body || typeof body !== "object") return body;
if (hasExplicitReasoningField(body)) return body;
const defaultEffort = getModelSpec(modelId)?.defaultReasoningEffort;
const defaultEffort = suffixEffort || getModelSpec(modelId)?.defaultReasoningEffort;
if (!defaultEffort) return body;
return { ...body, reasoning_effort: defaultEffort };

View File

@@ -425,6 +425,34 @@ export function parseModel(modelStr: string | null | undefined): ParsedModel {
return { provider: null, model: cleanStr, isAlias: true, providerAlias: null, extendedContext };
}
/**
* Generic `-{effort}` suffix split for a synced model (#7694), sibling to Codex's own
* `splitCodexReasoningSuffix` (`open-sse/executors/codex.ts`) but not tied to any single
* provider. `knownEfforts` must be the CANDIDATE base model's own declared
* `supportedThinkingEfforts` — the caller is responsible for only invoking this once it
* already has a specific synced-model candidate in hand (e.g. by trying each synced model
* for the provider), so a suffix is only ever stripped when it is an EXACT, known tier for
* THAT model — never a blind string match. This avoids colliding with a model id that
* legitimately ends in an effort-like token, and keeps parsing pure/synchronous (no DB
* access here — the DB-backed candidate lookup lives in `src/sse/services/model.ts`).
*/
export function splitSyncedEffortSuffix(
modelId: string,
knownEfforts: readonly string[] | null | undefined
): { baseModel: string; effort: string | null } {
if (typeof modelId !== "string" || !modelId || !Array.isArray(knownEfforts)) {
return { baseModel: modelId, effort: null };
}
for (const effort of knownEfforts) {
if (typeof effort !== "string" || !effort) continue;
const suffix = `-${effort}`;
if (modelId.length > suffix.length && modelId.endsWith(suffix)) {
return { baseModel: modelId.slice(0, -suffix.length), effort };
}
}
return { baseModel: modelId, effort: null };
}
/**
* Resolve model alias from aliases object
* Format: { "alias": "provider/model" }

View File

@@ -0,0 +1,109 @@
/**
* Synced-model reasoning-effort catalog variants (#7694).
*
* Generic OpenAI-compatible model discovery (`src/lib/providerModels/modelDiscovery.ts`)
* captures upstream `reasoning.supported_efforts` (or OmniRoute's own flat
* `supportedThinkingEfforts` import field) into `SyncedAvailableModel.supportedThinkingEfforts`,
* and the catalog builder (`src/app/api/v1/models/catalog.ts`) surfaces it as
* `capabilities.effort_tiers`. Catalog-only clients (OpenCode, plain OpenAI-SDK model
* pickers) can only choose a model by its `id`, so — mirroring `claudeEffortVariants.ts`'s
* `<provider>/<model>-<level>` pattern for Claude — this module synthesizes one catalog
* entry per declared tier:
*
* <provider>/<model>-<tier> e.g. myprovider/my-model-high
*
* The gateway already resolves these ids at request time
* (`splitSyncedEffortSuffix` in `open-sse/services/model.ts`, wired through
* `src/sse/services/model.ts`'s synced-metadata lookup): it strips the `-<tier>` suffix
* back to the real base model and surfaces the tier as `reasoning_effort` before dispatch,
* only when the base model's own `supportedThinkingEfforts` actually declares that tier —
* never a blind string match.
*
* Skipped entirely for `codex` and `kimi`-owned models: both already own a conflicting
* native `-{effort}` suffix mechanism (`splitCodexReasoningSuffix` /
* `getKimiCodeStaticThinkingPolicy`), so double-registering here would collide with their
* own alias resolution. Also skipped for any model whose id already ends in a token that
* matches a canonical effort value, to avoid colliding with a model that legitimately ends
* in an effort-like token (e.g. a model literally named "...-high").
*/
import { CANONICAL_EFFORT_VALUES } from "@/shared/reasoning/effortStandardization.ts";
/** Provider ids that already own a native `-{effort}` suffix mechanism — never double-register. */
export const SYNCED_EFFORT_SKIP_PROVIDERS = new Set(["codex"]);
/** Provider-id prefixes covering that mechanism's multiple connection variants (kimi-coding, kimi-coding-apikey). */
const SYNCED_EFFORT_SKIP_PROVIDER_PREFIXES = ["kimi"];
function isSkippedEffortProvider(ownedBy: string): boolean {
return (
SYNCED_EFFORT_SKIP_PROVIDERS.has(ownedBy) ||
SYNCED_EFFORT_SKIP_PROVIDER_PREFIXES.some((prefix) => ownedBy.startsWith(prefix))
);
}
interface CatalogModelEntry {
id?: unknown;
owned_by?: unknown;
root?: unknown;
name?: unknown;
capabilities?: { effort_tiers?: unknown; [key: string]: unknown };
[key: string]: unknown;
}
function endsWithKnownEffortToken(id: string): boolean {
return CANONICAL_EFFORT_VALUES.some((value) => id.endsWith(`-${value}`));
}
function extractEffortTiers(model: CatalogModelEntry): string[] {
const tiers = model.capabilities?.effort_tiers;
if (!Array.isArray(tiers)) return [];
return tiers.filter((tier): tier is string => typeof tier === "string" && tier.length > 0);
}
/**
* Whether the catalog should advertise reasoning-effort variants for this entry.
*
* Rule: a synced model that declares `capabilities.effort_tiers`, is not owned by a
* provider that already owns its own suffix mechanism, is not a virtual combo entry, and
* whose id does not already end in a token that collides with a canonical effort value.
*/
export function shouldExposeSyncedEffortVariants(
model: CatalogModelEntry
): model is CatalogModelEntry & { id: string } {
if (!model || typeof model !== "object") return false;
const id = model.id;
if (typeof id !== "string" || id.length === 0) return false;
if (model.owned_by === "combo") return false;
if (typeof model.owned_by === "string" && isSkippedEffortProvider(model.owned_by)) {
return false;
}
if (endsWithKnownEffortToken(id)) return false;
return extractEffortTiers(model).length > 0;
}
/**
* Append reasoning-effort variants for every eligible synced model. Returns the original
* array reference unchanged when nothing is eligible (no allocation in the common case).
* Derived from the already key-filtered/hidden-filtered catalog list, so a variant's base
* fields (visibility, capabilities, pricing) are inherited by spreading the base entry
* rather than re-running any filter — an alias never bypasses a filter the base model was
* already subject to.
*/
export function appendSyncedEffortVariants<T extends CatalogModelEntry>(models: T[]): T[] {
if (!Array.isArray(models)) return models;
const variants: T[] = [];
const existingIds = new Set(models.map((model) => model.id));
for (const model of models) {
if (!shouldExposeSyncedEffortVariants(model)) continue;
const baseRoot = typeof model.root === "string" && model.root ? model.root : model.id;
for (const tier of extractEffortTiers(model)) {
const variantId = `${model.id}-${tier}`;
if (existingIds.has(variantId)) continue;
existingIds.add(variantId);
variants.push({ ...model, id: variantId, root: `${baseRoot}-${tier}` });
}
}
return variants.length > 0 ? [...models, ...variants] : models;
}

View File

@@ -12,6 +12,8 @@ import {
import { extractAliasBackedModels } from "./aliasBackedModels";
import { appendNoThinkingVariants } from "@omniroute/open-sse/utils/noThinkingAlias";
import { appendClaudeEffortVariants } from "@omniroute/open-sse/utils/claudeEffortVariants";
import { appendSyncedEffortVariants } from "@omniroute/open-sse/utils/syncedEffortVariants";
import { buildSyncedCapabilities, mergeSyncedCapabilities } from "./syncedCapabilities";
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry";
import {
getAllImageModels,
@@ -865,22 +867,14 @@ async function buildUnifiedModelsResponseCore(
...(endpoints.length > 1 || !endpoints.includes("chat")
? { supported_endpoints: endpoints }
: {}),
// #4264: surface the vision flag captured at sync time so imported
// image-capable models (e.g. OpenRouter) aren't shown as text-only.
...(sm.supportsVision ? { capabilities: { vision: true } } : {}),
// #4264/#7694: vision + reasoning-effort-tier flags captured at sync time,
// merged into a single capabilities object (see ./syncedCapabilities.ts).
...(buildSyncedCapabilities(sm) ? { capabilities: buildSyncedCapabilities(sm) } : {}),
};
const existingAliasModel = models.find((model) => model.id === aliasId);
if (existingAliasModel) {
// Merge (not clobber) capabilities so syncing a vision flag onto a
// registry/combo model that already declares other capabilities keeps both.
const mergedCapabilities =
sm.supportsVision || existingAliasModel.capabilities
? {
...(existingAliasModel.capabilities || {}),
...(sm.supportsVision ? { vision: true } : {}),
}
: undefined;
const mergedCapabilities = mergeSyncedCapabilities(existingAliasModel.capabilities, sm);
Object.assign(existingAliasModel, syncedFields);
if (mergedCapabilities) existingAliasModel.capabilities = mergedCapabilities;
continue;
@@ -1489,6 +1483,11 @@ async function buildUnifiedModelsResponseCore(
prefixMode === "canonical" ? aliasToProviderId : undefined
);
// #7694: advertise `<provider>/<model>-<tier>` variants for synced models that
// captured `reasoning.supported_efforts` at sync time (capabilities.effort_tiers).
// Derived from the already key-filtered list; skips codex/kimi (own suffix mechanism).
finalModels = appendSyncedEffortVariants(finalModels);
// #4424 follow-up — drop exact-duplicate ids that slip through the per-source push
// guards (e.g. `codex/gpt-5.5`, `veo-free/seedance` listed twice). Keyed by listing
// identity (id, type, subtype) so the intentional same-id audio transcription/speech

View File

@@ -0,0 +1,47 @@
/**
* Synced-model catalog `capabilities` builder (#7694).
*
* Extracted from catalog.ts (frozen file-size baseline — `config/quality/file-size-baseline.json`)
* to keep the vision (#4264) and reasoning-effort-tier (#7694) flags merged into a SINGLE
* `capabilities` object rather than two separate spreads that would silently overwrite one
* another via object-spread order. A model can be both vision- and reasoning-capable.
*/
interface SyncedCapabilityFlags {
supportsVision?: boolean;
supportedThinkingEfforts?: string[];
}
function hasEffortTiers(sm: SyncedCapabilityFlags): boolean {
return Array.isArray(sm.supportedThinkingEfforts) && sm.supportedThinkingEfforts.length > 0;
}
/** Build the `capabilities` object for a fresh synced-model catalog entry, or `undefined` when neither flag applies. */
export function buildSyncedCapabilities(
sm: SyncedCapabilityFlags
): Record<string, boolean | string[]> | undefined {
const effortTiers = hasEffortTiers(sm);
if (!sm.supportsVision && !effortTiers) return undefined;
return {
...(sm.supportsVision ? { vision: true } : {}),
...(effortTiers ? { effort_tiers: sm.supportedThinkingEfforts! } : {}),
};
}
/**
* Merge (not clobber) capabilities onto an already-catalogued entry so syncing a
* vision/effort-tier flag onto a registry/combo model that already declares other
* capabilities keeps both. Returns `undefined` when there is nothing to merge.
*/
export function mergeSyncedCapabilities(
existing: Record<string, unknown> | undefined,
sm: SyncedCapabilityFlags
): Record<string, unknown> | undefined {
const effortTiers = hasEffortTiers(sm);
if (!sm.supportsVision && !effortTiers && !existing) return undefined;
return {
...(existing || {}),
...(sm.supportsVision ? { vision: true } : {}),
...(effortTiers ? { effort_tiers: sm.supportedThinkingEfforts! } : {}),
};
}

View File

@@ -1,8 +1,10 @@
import { z } from "zod";
import {
getSyncedAvailableModelsForConnection,
replaceSyncedAvailableModelsForConnection,
type SyncedAvailableModel,
} from "@/lib/db/models";
import { CANONICAL_EFFORT_VALUES } from "@/shared/reasoning/effortStandardization";
type JsonRecord = Record<string, unknown>;
@@ -58,6 +60,52 @@ export function detectVisionInput(record: JsonRecord): boolean {
return false;
}
// #7694: nested `reasoning.supported_efforts` shape some OpenAI-compatible upstreams
// expose (as opposed to the flat `supportedThinkingEfforts` field OmniRoute's own
// import format already emits). Hard Rule #7 — validate the untrusted upstream
// payload with Zod before it is trusted/stored; a malformed shape degrades to
// `undefined` instead of throwing, so one bad record never fails the whole sync.
const reasoningSupportedEffortsSchema = z
.object({ supported_efforts: z.array(z.string()).optional() })
.partial()
.nullable()
.optional();
// Maps common upstream synonyms onto OmniRoute's canonical effort vocabulary
// (`src/shared/reasoning/effortStandardization.ts`). Values already in
// `CANONICAL_EFFORT_VALUES`, and any unrecognized provider-native tier (e.g.
// Codex's own "ultra"), pass through unchanged — only known synonyms are mapped.
const EFFORT_SYNONYMS: Record<string, string> = { max: "xhigh" };
function normalizeSupportedEffort(effort: string): string {
if ((CANONICAL_EFFORT_VALUES as readonly string[]).includes(effort)) return effort;
return EFFORT_SYNONYMS[effort.toLowerCase()] || effort;
}
/**
* #7694: read the nested `record.reasoning.supported_efforts` shape and normalize each
* tier onto the canonical vocabulary. Returns `undefined` (never throws) when the field
* is absent or malformed, so it can be used as a fallback alongside the pre-existing flat
* `record.supportedThinkingEfforts` field without disturbing that field's current
* pass-through behavior.
*/
export function detectSupportedThinkingEfforts(record: JsonRecord): string[] | undefined {
const parsed = reasoningSupportedEffortsSchema.safeParse(record.reasoning);
if (!parsed.success || !parsed.data) return undefined;
const rawEfforts = parsed.data.supported_efforts;
if (!Array.isArray(rawEfforts)) return undefined;
const efforts = Array.from(
new Set(
rawEfforts
.filter((effort): effort is string => typeof effort === "string" && effort.length > 0)
.map(normalizeSupportedEffort)
)
);
return efforts.length > 0 ? efforts : undefined;
}
export function isAutoFetchModelsEnabled(providerSpecificData: unknown): boolean {
return asRecord(providerSpecificData).autoFetchModels !== false;
}
@@ -128,13 +176,21 @@ export function normalizeDiscoveredModels(models: unknown): SyncedAvailableModel
? { upstreamProtocol: toNonEmptyString(record.upstreamProtocol)! }
: {}),
...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}),
...(Array.isArray(record.supportedThinkingEfforts)
? {
...(() => {
// #7694: the flat field (OmniRoute's own import format) wins verbatim when
// present, unchanged from its current pass-through behavior; only fall back to
// the nested `reasoning.supported_efforts` shape (normalized onto the canonical
// vocabulary) when the flat field is absent.
if (Array.isArray(record.supportedThinkingEfforts)) {
return {
supportedThinkingEfforts: record.supportedThinkingEfforts.filter(
(effort): effort is string => typeof effort === "string" && effort.length > 0
),
}
: {}),
};
}
const nested = detectSupportedThinkingEfforts(record);
return nested ? { supportedThinkingEfforts: nested } : {};
})(),
...(toNonEmptyString(record.defaultThinkingEffort)
? { defaultThinkingEffort: toNonEmptyString(record.defaultThinkingEffort)! }
: {}),

View File

@@ -9,7 +9,11 @@ import {
} from "@/lib/localDb";
import { getCachedSettings } from "@/lib/localDb";
import { getSyncedAvailableModels } from "@/lib/db/models";
import { parseModel, getModelInfoCore } from "@omniroute/open-sse/services/model.ts";
import {
parseModel,
getModelInfoCore,
splitSyncedEffortSuffix,
} from "@omniroute/open-sse/services/model.ts";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
export { parseModel };
@@ -101,8 +105,56 @@ type RuntimeModelMeta = {
alwaysThinking?: boolean;
supportedThinkingEfforts?: string[];
defaultThinkingEffort?: string;
// #7694: set when `modelId` carried a `-{effort}` suffix resolved against a synced
// model's own `supportedThinkingEfforts` (see `resolveSyncedModelIdAndEffort` below).
// Threaded through to `handleChatCore` -> `applyDefaultReasoningEffort` so the suffix
// becomes `reasoning_effort` only when the request itself carries no reasoning field.
resolvedThinkingEffort?: string;
};
// Providers that already own a native `-{effort}` suffix mechanism — never
// double-resolve the generic synced suffix on top of theirs (#7694, mirrors the
// catalog-side skip list in `open-sse/utils/syncedEffortVariants.ts`).
const SYNCED_EFFORT_SKIP_PROVIDER_PREFIXES = ["codex", "kimi"];
function isSyncedEffortSkippedProvider(providerId: string): boolean {
return SYNCED_EFFORT_SKIP_PROVIDER_PREFIXES.some((prefix) => providerId.startsWith(prefix));
}
/**
* #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
* `supportedThinkingEfforts` (`splitSyncedEffortSuffix`) — never a blind/global effort
* list, so a model id that legitimately ends in an effort-like token is left untouched
* unless some synced model's real tier list says otherwise. Returns the original
* `modelId` with `effort: null` when nothing matches or the provider already owns its
* own suffix mechanism.
*/
function resolveSyncedModelIdAndEffort(
providerId: string,
modelId: string,
syncedModels: unknown
): { modelId: string; effort: string | null } {
if (isSyncedEffortSkippedProvider(providerId) || !Array.isArray(syncedModels)) {
return { modelId, effort: null };
}
if (findSyncedModelMeta(syncedModels, modelId)) return { modelId, effort: null };
for (const candidate of syncedModels as Array<{ id?: unknown; supportedThinkingEfforts?: unknown }>) {
if (typeof candidate?.id !== "string" || !Array.isArray(candidate.supportedThinkingEfforts)) {
continue;
}
const attempt = splitSyncedEffortSuffix(
modelId,
candidate.supportedThinkingEfforts as string[]
);
if (attempt.effort && attempt.baseModel === candidate.id) {
return { modelId: attempt.baseModel, effort: attempt.effort };
}
}
return { modelId, effort: null };
}
function findCustomModelMeta(models: unknown, modelId: string): any {
if (!Array.isArray(models)) return undefined;
return (
@@ -151,19 +203,32 @@ function buildRuntimeModelMeta(customMatch: any, syncedMatch: any): RuntimeModel
return metadata;
}
async function lookupModelMeta(providerId: string, modelId: string): Promise<RuntimeModelMeta> {
async function lookupModelMeta(
providerId: string,
modelId: string
): Promise<{ modelId: string; metadata: RuntimeModelMeta }> {
try {
const [customModels, syncedModels] = await Promise.all([
getCustomModels(providerId),
getSyncedAvailableModels(providerId),
]);
// #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(
providerId,
modelId,
syncedModels
);
// #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, modelId);
const syncedMatch = findSyncedModelMeta(syncedModels, modelId);
return buildRuntimeModelMeta(customMatch, syncedMatch);
const customMatch = findCustomModelMeta(customModels, resolvedModelId);
const syncedMatch = findSyncedModelMeta(syncedModels, resolvedModelId);
const metadata = buildRuntimeModelMeta(customMatch, syncedMatch);
if (effort) metadata.resolvedThinkingEffort = effort;
return { modelId: resolvedModelId, metadata };
} catch {
return {};
return { modelId, metadata: {} };
}
}
@@ -192,8 +257,9 @@ export async function getModelInfo(modelStr) {
const attachRuntimeModelMeta = async (info: any) => {
if (!info?.provider || !info?.model) return info;
const metadata = await lookupModelMeta(String(info.provider), String(info.model));
return Object.keys(metadata).length > 0 ? { ...info, ...metadata } : info;
const { modelId, metadata } = await lookupModelMeta(String(info.provider), String(info.model));
const resolvedInfo = modelId !== info.model ? { ...info, model: modelId } : info;
return Object.keys(metadata).length > 0 ? { ...resolvedInfo, ...metadata } : resolvedInfo;
};
// Check custom provider nodes first (for both alias and non-alias formats)
@@ -225,13 +291,13 @@ export async function getModelInfo(modelStr) {
parsed.model as string,
matchedOpenAI.prefix
);
const metadata = await lookupModelMeta(
const { modelId, metadata } = await lookupModelMeta(
matchedOpenAI.id as string,
normalizedModel
);
return {
provider: matchedOpenAI.id,
model: normalizedModel,
model: modelId,
extendedContext,
...metadata,
};
@@ -247,13 +313,13 @@ export async function getModelInfo(modelStr) {
parsed.model as string,
matchedAnthropic.prefix
);
const metadata = await lookupModelMeta(
const { modelId, metadata } = await lookupModelMeta(
matchedAnthropic.id as string,
normalizedModel
);
return {
provider: matchedAnthropic.id,
model: normalizedModel,
model: modelId,
extendedContext,
...metadata,
};

View File

@@ -0,0 +1,379 @@
/**
* #7694 — capture upstream `reasoning.supported_efforts` at sync, surface it as
* `capabilities.effort_tiers` in the catalog, and support `<prefix>/<model>-{effort}`
* request-time alias resolution.
*
* Mirrors the #6879 per-model reasoning-effort precedent
* (open-sse/services/defaultReasoningEffort.ts): captures/exposes what the provider
* already declared and threads it through the EXISTING `supportedThinkingEfforts`
* plumbing (`SyncedAvailableModel`, `src/sse/services/model.ts` `RuntimeModelMeta`) —
* no parallel storage mechanism.
*/
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-7694-effort-sync-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "catalog-test-secret-7694";
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 v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
const { getModelInfo } = await import("../../src/sse/services/model.ts");
const { normalizeDiscoveredModels, detectSupportedThinkingEfforts } = await import(
"../../src/lib/providerModels/modelDiscovery.ts"
);
const { splitSyncedEffortSuffix } = await import("../../open-sse/services/model.ts");
const {
appendSyncedEffortVariants,
shouldExposeSyncedEffortVariants,
SYNCED_EFFORT_SKIP_PROVIDERS,
} = await import("../../open-sse/utils/syncedEffortVariants.ts");
const { applyDefaultReasoningEffort } = await import(
"../../open-sse/services/defaultReasoningEffort.ts"
);
async function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
v1ModelsCatalog.__resetCatalogBuilderRunsForTest();
await resetStorage();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
async function seedProviderConnection(provider: string) {
return providersDb.createProviderConnection({
provider,
authType: "apikey",
name: `${provider}-${Math.random().toString(16).slice(2, 8)}`,
apiKey: `${provider}-key`,
isActive: true,
testStatus: "active",
});
}
// ---------------------------------------------------------------------------
// Step 1+2: sync-time capture of the nested `reasoning.supported_efforts` shape,
// normalized onto the canonical vocabulary. Hard Rule #7 — Zod-validated.
// ---------------------------------------------------------------------------
test("normalizeDiscoveredModels: captures nested reasoning.supported_efforts (no flat field) and normalizes 'max' -> 'xhigh'", () => {
const [model] = normalizeDiscoveredModels([
{
id: "some/model-7694",
reasoning: { supported_efforts: ["low", "medium", "max"] },
},
]);
assert.deepEqual(model.supportedThinkingEfforts, ["low", "medium", "xhigh"]);
});
test("normalizeDiscoveredModels: pre-existing flat supportedThinkingEfforts field wins verbatim over nested (regression)", () => {
const [model] = normalizeDiscoveredModels([
{
id: "some/model-flat",
supportedThinkingEfforts: ["low", "max"], // byte-identical pass-through, no normalization
reasoning: { supported_efforts: ["high"] }, // must be ignored — flat field wins
},
]);
assert.deepEqual(model.supportedThinkingEfforts, ["low", "max"]);
});
test("normalizeDiscoveredModels: an unrecognized provider-native tier passes through unchanged (e.g. codex-style 'ultra')", () => {
const [model] = normalizeDiscoveredModels([
{ id: "some/model-ultra", reasoning: { supported_efforts: ["ultra"] } },
]);
assert.deepEqual(model.supportedThinkingEfforts, ["ultra"]);
});
test("normalizeDiscoveredModels: graceful degradation — reasoning:{} (no supported_efforts key) produces no supportedThinkingEfforts field, does not throw", () => {
const [model] = normalizeDiscoveredModels([{ id: "some/model-empty-reasoning", reasoning: {} }]);
assert.equal("supportedThinkingEfforts" in model, false);
});
test("normalizeDiscoveredModels: graceful degradation — reasoning entirely absent produces no supportedThinkingEfforts field, does not throw", () => {
const [model] = normalizeDiscoveredModels([{ id: "some/model-no-reasoning" }]);
assert.equal("supportedThinkingEfforts" in model, false);
});
test("detectSupportedThinkingEfforts: Hard Rule #7 — malformed upstream payload (supported_efforts is a string, not an array) is rejected by Zod and degrades to undefined without throwing", () => {
assert.doesNotThrow(() => {
const result = detectSupportedThinkingEfforts({
reasoning: { supported_efforts: "high" as unknown },
});
assert.equal(result, undefined);
});
});
test("detectSupportedThinkingEfforts: malformed upstream payload (reasoning is a string, not an object) is rejected by Zod and degrades to undefined without throwing", () => {
assert.doesNotThrow(() => {
const result = detectSupportedThinkingEfforts({ reasoning: "not-an-object" as unknown });
assert.equal(result, undefined);
});
});
// ---------------------------------------------------------------------------
// Step 3: catalog surfaces capabilities.effort_tiers for synced models.
// ---------------------------------------------------------------------------
test("#7694 catalog: GET /api/v1/models surfaces capabilities.effort_tiers for a synced model with nested reasoning.supported_efforts", async () => {
const connection = await seedProviderConnection("huggingface");
await modelsDb.replaceSyncedAvailableModelsForConnection("huggingface", connection.id, [
{
id: "some-org/reasoning-model-7694",
name: "Reasoning Model 7694",
supportedThinkingEfforts: ["low", "medium", "high"],
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
assert.equal(response.status, 200);
const body = (await response.json()) as {
data: Array<{ id: string; capabilities?: { effort_tiers?: string[] } }>;
};
const entry = body.data.find((m) => m.id.endsWith("some-org/reasoning-model-7694"));
assert.ok(entry, "synced reasoning model must appear in the catalog");
assert.deepEqual(entry!.capabilities?.effort_tiers, ["low", "medium", "high"]);
});
test("#7694 catalog: a synced model with no supportedThinkingEfforts gets no capabilities.effort_tiers (graceful degradation)", async () => {
const connection = await seedProviderConnection("huggingface");
await modelsDb.replaceSyncedAvailableModelsForConnection("huggingface", connection.id, [
{ id: "some-org/no-reasoning-model-7694", name: "No Reasoning Model 7694" },
]);
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 entry = body.data.find((m) => m.id.endsWith("some-org/no-reasoning-model-7694"));
assert.ok(entry);
assert.equal(entry!.capabilities?.effort_tiers, undefined);
});
// ---------------------------------------------------------------------------
// Step 4: catalog generates `-{effort}` alias ids per declared tier.
// ---------------------------------------------------------------------------
test("#7694 catalog: GET /api/v1/models advertises a <model>-<tier> alias id per declared effort tier", async () => {
const connection = await seedProviderConnection("huggingface");
await modelsDb.replaceSyncedAvailableModelsForConnection("huggingface", connection.id, [
{
id: "some-org/reasoning-model-alias-7694",
name: "Reasoning Model Alias 7694",
supportedThinkingEfforts: ["low", "high"],
},
]);
const response = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models")
);
const body = (await response.json()) as { data: Array<{ id: string }> };
const baseEntry = body.data.find((m) => m.id.endsWith("some-org/reasoning-model-alias-7694"));
assert.ok(baseEntry, "base model must appear in the catalog");
const highVariant = body.data.find((m) => m.id === `${baseEntry!.id}-high`);
const lowVariant = body.data.find((m) => m.id === `${baseEntry!.id}-low`);
assert.ok(highVariant, `expected a "${baseEntry!.id}-high" alias entry`);
assert.ok(lowVariant, `expected a "${baseEntry!.id}-low" alias entry`);
});
// ---------------------------------------------------------------------------
// shouldExposeSyncedEffortVariants / appendSyncedEffortVariants — pure unit tests.
// ---------------------------------------------------------------------------
test("shouldExposeSyncedEffortVariants: skips codex/kimi-owned models (own suffix mechanism) — Kimi/codex regression guard", () => {
assert.equal(
shouldExposeSyncedEffortVariants({
id: "codex/gpt-5.5",
owned_by: "codex",
capabilities: { effort_tiers: ["low", "high"] },
}),
false
);
assert.equal(
shouldExposeSyncedEffortVariants({
id: "kimi/kimi-k3",
owned_by: "kimi-coding",
capabilities: { effort_tiers: ["low", "high"] },
}),
false
);
assert.equal(
shouldExposeSyncedEffortVariants({
id: "kimi/kimi-k3",
owned_by: "kimi-coding-apikey",
capabilities: { effort_tiers: ["low", "high"] },
}),
false
);
assert.ok(SYNCED_EFFORT_SKIP_PROVIDERS.has("codex"));
});
test("shouldExposeSyncedEffortVariants: skips a model id that already ends in a token matching a canonical effort value (collision guard)", () => {
assert.equal(
shouldExposeSyncedEffortVariants({
id: "someprovider/my-model-high", // legitimately ends in "-high"
owned_by: "someprovider",
capabilities: { effort_tiers: ["low", "high"] },
}),
false
);
});
test("shouldExposeSyncedEffortVariants: exposes a plain synced model carrying effort_tiers", () => {
assert.equal(
shouldExposeSyncedEffortVariants({
id: "someprovider/my-model",
owned_by: "someprovider",
capabilities: { effort_tiers: ["low", "high"] },
}),
true
);
});
test("appendSyncedEffortVariants: generates one variant per tier, inheriting the base model's fields", () => {
const base = {
id: "someprovider/my-model",
owned_by: "someprovider",
root: "my-model",
capabilities: { effort_tiers: ["low", "high"] },
hidden: false,
};
const result = appendSyncedEffortVariants([base]);
assert.equal(result.length, 3);
const high = result.find((m) => m.id === "someprovider/my-model-high");
assert.ok(high);
assert.equal(high!.root, "my-model-high");
assert.equal(high!.hidden, false); // inherited, not re-derived
});
// ---------------------------------------------------------------------------
// Step 5: request-time `<prefix>/<model>-{effort}` alias resolution.
// ---------------------------------------------------------------------------
test("splitSyncedEffortSuffix: strips a known trailing effort token for a candidate's own declared tiers", () => {
const result = splitSyncedEffortSuffix("my-model-high", ["low", "medium", "high"]);
assert.deepEqual(result, { baseModel: "my-model", effort: "high" });
});
test("splitSyncedEffortSuffix: leaves an unsupported effort token untouched (not a known tier for this model)", () => {
const result = splitSyncedEffortSuffix("my-model-ultrahigh", ["low", "medium", "high"]);
assert.deepEqual(result, { baseModel: "my-model-ultrahigh", effort: null });
});
test("splitSyncedEffortSuffix: no known efforts list -> untouched", () => {
const result = splitSyncedEffortSuffix("my-model-high", undefined);
assert.deepEqual(result, { baseModel: "my-model-high", effort: null });
});
const EFFORT_CONN_ID = "openai-compatible-chat-7694-effort-probe";
const EFFORT_PREFIX = "pfx7694";
const EFFORT_BASE_MODEL = "reasoning-model-7694";
async function seedEffortSuffixNode() {
await providersDb.createProviderNode({
id: EFFORT_CONN_ID,
type: "openai-compatible",
name: "7694 effort-suffix probe",
prefix: EFFORT_PREFIX,
baseUrl: "https://proxy.example.com",
chatPath: "/v1/chat/completions",
modelsPath: "/v1/models",
});
await modelsDb.replaceSyncedAvailableModelsForConnection(EFFORT_CONN_ID, EFFORT_CONN_ID, [
{
id: EFFORT_BASE_MODEL,
name: "Reasoning Model 7694",
supportedThinkingEfforts: ["low", "medium", "high"],
},
]);
}
test("#7694 RED->GREEN: <prefix>/<model>-high resolves to the base model id and derives 'high' as the effort", async () => {
await seedEffortSuffixNode();
const info = (await getModelInfo(`${EFFORT_PREFIX}/${EFFORT_BASE_MODEL}-high`)) as {
provider?: string;
model?: string;
resolvedThinkingEffort?: string;
};
assert.equal(info.provider, EFFORT_CONN_ID);
assert.equal(info.model, EFFORT_BASE_MODEL, "suffix must be stripped back to the base model id");
assert.equal(info.resolvedThinkingEffort, "high");
});
test("#7694: an unsupported effort suffix is NOT stripped (treated as a literal/unknown model id — collision-risk guard holds)", async () => {
await seedEffortSuffixNode();
const info = (await getModelInfo(`${EFFORT_PREFIX}/${EFFORT_BASE_MODEL}-ultrahigh`)) as {
provider?: string;
model?: string;
resolvedThinkingEffort?: string;
};
assert.equal(info.provider, EFFORT_CONN_ID);
assert.equal(
info.model,
`${EFFORT_BASE_MODEL}-ultrahigh`,
"unknown suffix must pass through literally, not be silently stripped"
);
assert.equal(info.resolvedThinkingEffort, undefined);
});
test("#7694: the base model id (no suffix) still resolves normally and carries its full tier list", async () => {
await seedEffortSuffixNode();
const info = (await getModelInfo(`${EFFORT_PREFIX}/${EFFORT_BASE_MODEL}`)) as {
provider?: string;
model?: string;
supportedThinkingEfforts?: string[];
resolvedThinkingEffort?: string;
};
assert.equal(info.provider, EFFORT_CONN_ID);
assert.equal(info.model, EFFORT_BASE_MODEL);
assert.deepEqual(info.supportedThinkingEfforts, ["low", "medium", "high"]);
assert.equal(info.resolvedThinkingEffort, undefined);
});
// ---------------------------------------------------------------------------
// applyDefaultReasoningEffort: the suffix-resolved effort (#7694) takes priority over
// the static per-model ModelSpec default (#6879), and an explicit client value still
// always wins over both.
// ---------------------------------------------------------------------------
test("applyDefaultReasoningEffort: a suffix-resolved effort (#7694) is injected as reasoning_effort", () => {
const body = { model: "some-model", messages: [] };
const result = applyDefaultReasoningEffort(body, "some-model", "high");
assert.equal(result.reasoning_effort, "high");
});
test("applyDefaultReasoningEffort: an explicit client reasoning_effort still wins over the suffix-resolved effort", () => {
const body = { model: "some-model", messages: [], reasoning_effort: "low" };
const result = applyDefaultReasoningEffort(body, "some-model", "high");
assert.equal(result.reasoning_effort, "low");
});
test("applyDefaultReasoningEffort: no suffix effort and no ModelSpec default -> no injection (regression, same reference)", () => {
const body = { model: "some-model-untouched", messages: [] };
const result = applyDefaultReasoningEffort(body, "some-model-untouched", null);
assert.equal(result, body);
});