Files
OmniRoute/src/app/api/v1/models/syncedCapabilities.ts
Dizzle 00c80fd14a feat(models): surface learned reasoning_effort sets in catalog, variants, and dispatch (#11252)
Validated on the combined 12-PR batch board + the resolved merge against the post-#11232 tip: focused suites 73/73 (learned-reasoning-effort-caps, synced-capabilities-learned-effort-override, synced-effort-suffix-learned-validation, effort-tiers-loop-catalog-e2e, reasoning-effort-clamp-and-retry, reasoning-effort-learned-capability) + opencode-plugin effort-tier-variants 4/4, typecheck:core clean, gates within baseline. The stacked-branch conflict after #11232 squash-landed was resolved by hand (the learned-caps module keeps both the Set API and the new model-scoped lookup). The effort_tiers loop is closed end-to-end: catalog advertises exactly what the upstream accepts, and -<tier> suffix variants resolve against the learned set. Thank you @maxmad64bis!
2026-08-23 14:38:57 -03:00

77 lines
3.6 KiB
TypeScript

/**
* 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.
*
* effort_tiers loop (2026-08-23): a runtime-learned accepted set (#11232,
* learnedReasoningEffortCaps) REPLACES the synced `supportedThinkingEfforts`
* when one exists — the proven contract beats the advertised one. Lookup is
* model-scoped: executors record under connection ids while this module sees
* provider ids, so an exact provider:model key would always miss.
*
* Exclusion gate: `ownedBy` is REQUIRED and checked against
* `isSkippedEffortProvider` (codex/glm/kimi — providers that already own a
* conflicting `-{effort}` suffix mechanism, see syncedEffortVariants.ts, #7694).
* Without this, the blind opencode-plugin mapping (`capabilities.effort_tiers`
* -> ModelV2 `variants`) would double-handle those providers' native suffix
* ids. `shouldExposeSyncedEffortVariants` gates only the *synthetic*
* `<id>-<tier>` catalog entries (open-sse/utils/syncedEffortVariants.ts) — it
* never runs over the base entry's `capabilities`, so it cannot substitute
* for this check. Required (not optional) so no call site can silently skip it.
*/
// Use the same canonical alias as catalogModelPolicy.ts (l.1) — a relative path from
// src/app/api/v1/models/ to open-sse/ would need 5 `../` and silently breaks under
// 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";
interface SyncedCapabilityFlags {
id?: string;
supportsVision?: boolean;
supportedThinkingEfforts?: string[];
}
function effectiveEffortTiers(sm: SyncedCapabilityFlags, ownedBy: string): string[] | undefined {
if (isSkippedEffortProvider(ownedBy)) return undefined;
const learned = sm.id ? getLearnedReasoningEffortForModel(sm.id) : null;
if (learned) return [...learned];
return Array.isArray(sm.supportedThinkingEfforts) && sm.supportedThinkingEfforts.length > 0
? sm.supportedThinkingEfforts
: undefined;
}
/** Build the `capabilities` object for a fresh synced-model catalog entry, or `undefined` when neither flag applies. */
export function buildSyncedCapabilities(
sm: SyncedCapabilityFlags,
ownedBy: string
): Record<string, boolean | string[]> | undefined {
const tiers = effectiveEffortTiers(sm, ownedBy);
if (!sm.supportsVision && !tiers) return undefined;
return {
...(sm.supportsVision ? { vision: true } : {}),
...(tiers ? { effort_tiers: tiers } : {}),
};
}
/**
* 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,
ownedBy: string
): Record<string, unknown> | undefined {
const tiers = effectiveEffortTiers(sm, ownedBy);
if (!sm.supportsVision && !tiers && !existing) return undefined;
return {
...(existing || {}),
...(sm.supportsVision ? { vision: true } : {}),
...(tiers ? { effort_tiers: tiers } : {}),
};
}