Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/codex/catalog/effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { redactSecretString, redactUserPath } from "../../lib/redact";
import upstreamModelsSnapshot from "../data/upstream-models.json";


import { readCatalog, readCodexCatalogPath } from "./parsing";
import { COMBO_CATALOG_KIND, readCatalog, readCodexCatalogPath } from "./parsing";
import type { CatalogModel, RawEntry } from "./parsing";
import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
import { loadBundledCodexCatalog } from "./bundled";
Expand Down Expand Up @@ -111,9 +111,11 @@ export const ROUTED_REASONING_LEVELS = [...CODEX_REASONING_LEVELS];

export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void {
if (!model) return;
// This marker survives strict catalog normalization and lets sync distinguish a stale
// bare combo alias from a genuine native model row.
if (model.provider === COMBO_NAMESPACE) entry.owned_by = model.owned_by ?? COMBO_NAMESPACE;
if (model.provider === COMBO_NAMESPACE) entry.opencodex_catalog_kind = COMBO_CATALOG_KIND;
// Preserve upstream/provider ownership as semantic catalog metadata. Generated lifecycle
// markers must use opencodex_catalog_kind instead of overloading this field.
if (model.owned_by) entry.owned_by = model.owned_by;
Comment thread
giulioleone097 marked this conversation as resolved.
else if (model.provider === COMBO_NAMESPACE) entry.owned_by = COMBO_NAMESPACE;
// displayName is DISPLAY-ONLY: it relabels the picker row but never touches the routing
// slug, alias, or provider. deriveEntry already stamped the slug as display_name; a
// configured displayName overrides just the label. The `/` separator is rejected at every
Expand Down
19 changes: 19 additions & 0 deletions src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export function isDefaultCatalogPath(path: string): boolean {
export interface CatalogModel {
id: string;
provider: string;
/** Final provider adapter identity used to derive adapter-specific catalog rows. */
adapter?: OcxProviderConfig["adapter"];
/** Public Codex-facing slug override (used by combo aliases). */
alias?: string;
/**
Expand Down Expand Up @@ -117,6 +119,23 @@ export interface CatalogModel {

export type RawEntry = Record<string, unknown>;

export const COMBO_CATALOG_KIND = "combo-v1";
export const ROUTED_CONTEXT_COMPAT_CATALOG_KIND = "routed-context-compat-v1";

export function isComboCatalogEntry(entry: RawEntry): boolean {
return entry.opencodex_catalog_kind === COMBO_CATALOG_KIND;
}

export function isRoutedContextCompatEntry(entry: RawEntry): boolean {
return entry.opencodex_catalog_kind === ROUTED_CONTEXT_COMPAT_CATALOG_KIND;
}

export function routedContextCompatTarget(entry: RawEntry): string | undefined {
return isRoutedContextCompatEntry(entry) && typeof entry.opencodex_routed_slug === "string"
? entry.opencodex_routed_slug
: undefined;
}

export type RawCatalog = { models?: RawEntry[]; [k: string]: unknown };

export const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go"]);
Expand Down
99 changes: 80 additions & 19 deletions src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,18 @@ import {
type OAuthActiveTokenObservation,
} from "../../oauth";
import type { OcxConfig, OcxProviderConfig } from "../../types";
import { modelInList } from "../../types";
import { MODEL_ADAPTER_OVERRIDE_ALLOWED, modelInList } from "../../types";
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
import { getProviderRegistryEntry, providerMatchesRegistryTransport, providerModelWireDefault } from "../../providers/registry";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
import { resolveWireProtocolOverride } from "../../server/adapter-resolve";
import {
COMBO_NAMESPACE,
comboModelId,
Expand Down Expand Up @@ -131,9 +132,33 @@ interface CapturedProviderGather {
readonly discovery: ResolvedProviderModelDiscovery;
readonly policy: CatalogProviderDiscoveryPolicySnapshot;
readonly request: CapturedModelsRequest;
readonly registryWireDefaults: Readonly<Record<string, string>>;
readonly observedAuth?: ModelsAuthResolution;
}

function captureRegistryWireDefaults(
name: string,
provider: OcxProviderConfig,
): Readonly<Record<string, string>> {
const declared = getProviderRegistryEntry(name)?.modelWireDefaults ?? {};
const captured: Record<string, string> = {};
for (const modelId of Object.keys(declared)) {
const wire = providerModelWireDefault(
name,
provider,
modelId,
MODEL_ADAPTER_OVERRIDE_ALLOWED,
"responses",
);
if (wire) captured[modelId.trim().toLowerCase()] = wire;
}
return Object.freeze(captured);
}

function capturedRegistryWireDefault(captured: CapturedProviderGather, modelId: string): string | null {
return captured.registryWireDefaults[modelId.trim().toLowerCase()] ?? null;
}

interface GatherFlightCapture {
readonly discoveryPolicyIdentity: string;
readonly authIdentity: string;
Expand Down Expand Up @@ -378,6 +403,7 @@ function captureProviderGather(
maxModels: resolved.maxModels,
});
const registryTransportMatch = providerMatchesRegistryTransport(name, provider);
const registryWireDefaults = captureRegistryWireDefaults(name, provider);
const trustedOpenAiApi = captureTrustedOpenAiApiPolicy(name, registryTransportMatch);
const policy = detachedFrozen({
provider: name,
Expand All @@ -401,6 +427,7 @@ function captureProviderGather(
discovery,
policy,
request,
registryWireDefaults,
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
});
}
Expand Down Expand Up @@ -440,6 +467,7 @@ function captureGatherFlight(
// It is the one member of a provider row that is legitimately a function,
// so it is dropped here rather than allowed to break every encode.
provider: omitProviderTransportExecutor(provider.provider),
registryWireDefaults: provider.registryWireDefaults,
}))),
discoveryPolicySnapshots,
providers: Object.freeze(providers),
Expand Down Expand Up @@ -547,8 +575,13 @@ function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined,
return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
}

export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
void name;
export function applyProviderConfigHints(
name: string,
prov: OcxProviderConfig,
model: CatalogModel,
providerCap?: number,
capturedWireDefault?: string | null,
): CatalogModel {
const configuredCap = configuredContextWindow(prov, model.id);
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
let inputModalities = configuredInputModalities(prov, model.id);
Expand All @@ -565,6 +598,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
const hinted = {
...model,
adapter: resolveWireProtocolOverride(name, model.id, prov, "responses", capturedWireDefault).adapter,
...(configuredCap !== undefined
? {
contextWindow: typeof model.contextWindow === "number" && model.contextWindow > 0
Expand Down Expand Up @@ -597,14 +631,32 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
return providerCap !== undefined ? { ...hinted, contextCap: providerCap, contextCapped: false } : hinted;
}

export function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial<CatalogModel> {
const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap);
export function catalogHintsFromProviderConfig(
name: string,
prov: OcxProviderConfig,
id: string,
contextCap?: number,
capturedWireDefault?: string | null,
): Partial<CatalogModel> {
const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, capturedWireDefault);
const { provider: _provider, id: _id, ...hints } = hinted;
return hints;
}

export function applyConfigHintsToCachedModels(name: string, prov: OcxProviderConfig, models: CatalogModel[], contextCap?: number): CatalogModel[] {
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap));
export function applyConfigHintsToCachedModels(
name: string,
prov: OcxProviderConfig,
models: CatalogModel[],
contextCap?: number,
registryWireDefaults?: Readonly<Record<string, string>>,
): CatalogModel[] {
return models.map(model => applyProviderConfigHints(
name,
prov,
model,
contextCap,
registryWireDefaults ? registryWireDefaults[model.id.trim().toLowerCase()] ?? null : undefined,
));
}

export function isDatedVariantId(liveId: string, configuredId: string): boolean {
Expand Down Expand Up @@ -838,7 +890,7 @@ async function fetchProviderModelsWithAuth(
const configured: CatalogModel[] = configuredIds.map(id => ({
id,
provider: name,
...catalogHintsFromProviderConfig(name, prov, id, contextCap),
...catalogHintsFromProviderConfig(name, prov, id, contextCap, capturedRegistryWireDefault(captured, id)),
}));
// Static catalogs never need an OAuth refresh or an upstream model request. Clear any
// discovery failure left by an older live configuration even when the account is logged out.
Expand All @@ -859,7 +911,7 @@ async function fetchProviderModelsWithAuth(
: [{
id: prov.defaultModel,
provider: name,
...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap),
...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, capturedRegistryWireDefault(captured, prov.defaultModel)),
}];
const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined;
const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => (
Expand All @@ -874,10 +926,10 @@ async function fetchProviderModelsWithAuth(
// suffix) but filter the static seed to the bases the account actually has — so models not on the
// plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
const cachedCursor = getFreshCached(name, ttlMs);
if (cachedCursor) return applyConfigHintsToCachedModels(name, prov, cachedCursor);
if (cachedCursor) return applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, captured.registryWireDefaults);
if (isModelsFetchCoolingDown(name)) {
const cooling = getStaleCached(name);
return cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured;
return cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, captured.registryWireDefaults) : configured;
}
const liveResult = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
if (liveResult.ok) {
Expand All @@ -894,7 +946,7 @@ async function fetchProviderModelsWithAuth(
`[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
);
const staleCursor = getStaleCached(name);
return staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured;
return staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, captured.registryWireDefaults) : configured;
}
if (prov.authMode === "oauth" && !apiKey) {
// No usable token (logged out, or account marked needsReauth). Still surface the
Expand All @@ -903,12 +955,12 @@ async function fetchProviderModelsWithAuth(
return configured;
}
const fresh = getFreshCached(name, ttlMs);
if (fresh) return withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)); // dedups Codex's frequent /v1/models polling within the TTL
if (fresh) return withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, captured.registryWireDefaults)); // dedups Codex's frequent /v1/models polling within the TTL
if (isModelsFetchCoolingDown(name)) {
// A recently-failed provider (unreachable API, missing proxy, bad key) must not re-pay the
// fetch timeout on every catalog poll — the dashboard polls this path per page load.
const stale = getStaleCached(name);
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : failedDiscoveryConfigured;
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, captured.registryWireDefaults)) : failedDiscoveryConfigured;
}
const url = request.url;
const headers = materializeCapturedHeaders(request, apiKey);
Expand All @@ -927,7 +979,7 @@ async function fetchProviderModelsWithAuth(
const stale = getStaleCached(name);
return {
models: stale
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap))
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, captured.registryWireDefaults))
: failedDiscoveryConfigured,
fallback: stale ? "stale" : "configured",
shouldLog,
Expand Down Expand Up @@ -1000,7 +1052,7 @@ async function fetchProviderModelsWithAuth(
provider: name,
...(ownedBy ? { owned_by: ownedBy } : {}),
...catalogHintsFromModelsApiItem(name, m),
}, contextCap);
}, contextCap, capturedRegistryWireDefault(captured, m.id));
})
.filter(m => shouldExposeProviderModel(name, m.id));
// Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
Expand All @@ -1018,7 +1070,13 @@ async function fetchProviderModelsWithAuth(
const dated = live.find(l => isDatedVariantId(l.id, m.id));
if (dated) {
// Reapply config hints so alias-keyed overrides (modelContextWindows etc.) win.
live.push(applyProviderConfigHints(name, prov, { ...dated, id: m.id }, contextCap));
live.push(applyProviderConfigHints(
name,
prov,
{ ...dated, id: m.id },
contextCap,
capturedRegistryWireDefault(captured, m.id),
));
} else if (seedVertexDefault || shouldRetainConfiguredProviderModel(name, m.id)) {
live.push(m);
} else {
Expand Down Expand Up @@ -1293,10 +1351,14 @@ async function gatherRoutedModelsUncached(
const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
const customModels = (config.customModels ?? []).map(cm => {
const rawProvider = config.providers[cm.provider];
const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
const base: CatalogModel = {
id: cm.modelId,
provider: cm.provider,
...(enrichedProvider?.adapter
? { adapter: resolveWireProtocolOverride(cm.provider, cm.modelId, enrichedProvider).adapter }
: {}),
// Display-only label: never feeds routing (customModels are keyed by routedSlug below).
...(cm.displayName ? { displayName: cm.displayName } : {}),
...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
Expand Down Expand Up @@ -1327,7 +1389,6 @@ async function gatherRoutedModelsUncached(
// (#349/#344). Deliberately NOT the full applyProviderConfigHints pass — custom rows are a
// user override, so their explicit contextWindow / inputModalities / reasoning fields must be
// preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, merged.id)) {
const current = merged.inputModalities ?? ["text"];
if (!current.includes("image")) {
Expand Down
Loading
Loading