mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
This commit is contained in:
committed by
GitHub
parent
e8a6123169
commit
7b85e1f6f7
1
changelog.d/features/7694-reasoning-supported-efforts.md
Normal file
1
changelog.d/features/7694-reasoning-supported-efforts.md
Normal 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.
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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" }
|
||||
|
||||
109
open-sse/utils/syncedEffortVariants.ts
Normal file
109
open-sse/utils/syncedEffortVariants.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
47
src/app/api/v1/models/syncedCapabilities.ts
Normal file
47
src/app/api/v1/models/syncedCapabilities.ts
Normal 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! } : {}),
|
||||
};
|
||||
}
|
||||
@@ -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)! }
|
||||
: {}),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
379
tests/unit/sync-reasoning-supported-efforts-7694.test.ts
Normal file
379
tests/unit/sync-reasoning-supported-efforts-7694.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user