-
Notifications
You must be signed in to change notification settings - Fork 670
fix(catalog): synthesize incomplete combo members with context fallback (#1163) #1305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -620,6 +620,104 @@ export function applyConfigHintsToCachedModels(name: string, prov: OcxProviderCo | |
| return models.map(model => applyProviderConfigHints(name, prov, model, contextCap)); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Last-resort context window for combo member synthesis when discovery and | ||
| * provider config both omit one. Matches the catalog entry default in | ||
| * `normalizeRoutedCatalogEntry` so incomplete live rows still catalog. | ||
| */ | ||
| const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000; | ||
|
|
||
| /** | ||
| * Resolve a combo target to a catalog member for derivation. | ||
| * Prefer discovery metadata; when the target is missing from the gather map or | ||
| * lacks a positive contextWindow, synthesize from the (registry-enriched) | ||
| * provider config so combos remain catalogued when targets are configured but | ||
| * discovery metadata is incomplete. Disabled providers stay unresolved. | ||
| * When hints still omit contextWindow, prefer known maxInputTokens, else | ||
| * COMBO_MEMBER_CONTEXT_FALLBACK so a live row without ctx does not drop the | ||
| * whole combo from the public catalog. | ||
| */ | ||
| export function resolveComboCatalogMember( | ||
| target: { provider: string; model: string }, | ||
| memberByKey: ReadonlyMap<string, CatalogModel>, | ||
| providers: ReadonlyMap<string, OcxProviderConfig>, | ||
| contextCap?: number, | ||
| ): CatalogModel | undefined { | ||
| const existing = memberByKey.get(targetKey(target)); | ||
| const prov = providers.get(target.provider); | ||
| // Disabled providers never contribute members — even a complete discovery row | ||
| // is unusable for catalog derivation while the provider is off. | ||
| if (prov?.disabled === true) return undefined; | ||
|
|
||
| // Complete live/configured rows still honor providerContextCaps so a high | ||
| // discovery window cannot outrun an operator-configured cap. | ||
| if ( | ||
| existing | ||
| && typeof existing.contextWindow === "number" | ||
| && existing.contextWindow > 0 | ||
| ) { | ||
| const capped = applyProviderContextCap(existing.contextWindow, contextCap); | ||
| if (capped === undefined || capped === existing.contextWindow) return existing; | ||
| const maxInput = typeof existing.maxInputTokens === "number" && existing.maxInputTokens > 0 | ||
| ? Math.min(existing.maxInputTokens, capped) | ||
| : capped; | ||
| return { | ||
| ...existing, | ||
| contextWindow: capped, | ||
| maxInputTokens: maxInput, | ||
| contextCap, | ||
| contextCapped: true as const, | ||
| }; | ||
| } | ||
|
|
||
| const base: CatalogModel = existing ?? { | ||
| id: target.model, | ||
| provider: target.provider, | ||
| }; | ||
| const hinted = prov | ||
| ? applyProviderConfigHints(target.provider, prov, base, contextCap) | ||
| : base; | ||
| const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0 | ||
| ? hinted.contextWindow | ||
| : undefined; | ||
| // Prefer a known positive maxInputTokens over inventing 128k when discovery | ||
| // advertised an input limit but no context window (common thin /models rows). | ||
| const knownMaxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0 | ||
| ? hinted.maxInputTokens | ||
| : (typeof base.maxInputTokens === "number" && base.maxInputTokens > 0 | ||
| ? base.maxInputTokens | ||
| : undefined); | ||
| const uncappedContext = hintedContext | ||
| ?? knownMaxInput | ||
| ?? (existing || prov ? COMBO_MEMBER_CONTEXT_FALLBACK : undefined); | ||
| if (uncappedContext === undefined) return undefined; | ||
| const usedFallback = hintedContext === undefined; | ||
| const cappedContext = applyProviderContextCap(uncappedContext, contextCap); | ||
| const contextWindow = cappedContext ?? uncappedContext; | ||
| const fallbackCapped = usedFallback | ||
| && contextCap !== undefined | ||
| && cappedContext !== undefined | ||
| && cappedContext !== uncappedContext; | ||
|
|
||
| const inputModalities = hinted.inputModalities ?? base.inputModalities ?? ["text"]; | ||
| const reasoningEfforts = hinted.reasoningEfforts | ||
| ?? (prov ? configuredReasoningEfforts(prov, target.model) : undefined) | ||
| ?? base.reasoningEfforts; | ||
| const maxInputTokens = knownMaxInput !== undefined | ||
| ? Math.min(knownMaxInput, contextWindow) | ||
| : contextWindow; | ||
|
|
||
| return { | ||
| ...hinted, | ||
| inputModalities, | ||
| ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), | ||
| contextWindow, | ||
| maxInputTokens, | ||
| ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}), | ||
| }; | ||
| } | ||
|
|
||
| export function isDatedVariantId(liveId: string, configuredId: string): boolean { | ||
| if (!liveId.startsWith(`${configuredId}-`)) return false; | ||
| return /^\d{8}$/.test(liveId.slice(configuredId.length + 1)); | ||
|
|
@@ -1319,21 +1417,26 @@ async function gatherRoutedModelsUncached( | |
| if (!memberByKey.has(key)) memberByKey.set(key, synthetic); | ||
| } | ||
| } | ||
| // Enriched (registry-hydrated) provider clones — shared by combo member synthesis and | ||
| // custom-model vision-sidecar inheritance so both see the same merged registry view. | ||
| const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider])); | ||
| for (const id of listComboIds(config)) { | ||
| const combo = getCombo(config, id); | ||
| if (!combo) continue; | ||
| const members = combo.targets | ||
| .map(target => memberByKey.get(targetKey(target))) | ||
| .map(target => resolveComboCatalogMember( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a combo target was deliberately removed from Useful? React with 👍 / 👎. |
||
| target, | ||
| memberByKey, | ||
| enrichedByName, | ||
| providerContextCap(config, target.provider), | ||
| )) | ||
| .filter((member): member is CatalogModel => member !== undefined); | ||
| const derived = deriveComboCatalogModel(id, combo, members); | ||
| if (derived) all.push(derived); | ||
| else warnUncataloguedComboOnce(id, combo, members, localOmissions); | ||
| } | ||
| replaceLastComboCatalogOmissions(localOmissions); | ||
| all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider))); | ||
| // Enriched (registry-hydrated) provider clones, keyed by name — the same view used above so | ||
| // custom rows get the same noVisionModels / inputModalities treatment as discovered rows. | ||
| const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider])); | ||
| // Provider-derived rows keyed by their Codex-facing slug: a custom override replaces the row | ||
| // with the same slug below, so that row's provider capability metadata is the inheritance source. | ||
| const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model])); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Consider naming
usedFallbackfor what it actually measures.Line 695 sets
usedFallbackwheneverhintedContextis undefined. That includes theknownMaxInputbranch at line 692, which is not the 128,000-token fallback. The behavior is still correct, becausefallbackCappedat lines 698-701 additionally requires that the cap changed the value. Only the name is misleading for the next reader who touches this block.♻️ Suggested rename
📝 Committable suggestion
🧰 Tools
🪛 ast-grep (0.45.0)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 Prompt for AI Agents