Skip to content
Merged
507 changes: 507 additions & 0 deletions src/codex/catalog/auto-review.ts

Large diffs are not rendered by default.

981 changes: 981 additions & 0 deletions src/codex/catalog/build-entries.ts

Large diffs are not rendered by default.

229 changes: 229 additions & 0 deletions src/codex/catalog/derive-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import type { OcxConfig } from "../../types";
import { effectiveProviderAlias } from "../../providers/default-aliases";
import { identifyRoutedModel } from "../../adapters/identity";
import { COMBO_NAMESPACE } from "../../combos";
import {
CODEX_CUSTOM_MODEL_CATALOG_KIND,
applyCatalogMetadata,
applyNativeOpenAiContextOverride,
applyRoutedCodexToolMode,
catalogModelSlug,
ensureStrictCatalogFields,
normalizeRoutedCatalogEntry,
normalizeServiceTiers,
} from "./parsing";
import type { CatalogModel, RawEntry } from "./parsing";
import {
hasNativeOpenAiCapabilityMetadata,
upstreamNativeEntry,
type NativeContextLimitsInput,
} from "./metadata";
import {
applyCatalogModelMetadata,
applyReasoningLevels,
ensureGpt56ReasoningLevels,
ensureUltraReasoningLevel,
isGpt56NativeSlug,
} from "./effort";
import { CATALOG_INACTIVE_REASON_FIELD, SPAWN_PRIORITY_FIELD } from "./subagent-roster";

export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
if (priority !== 9) clone.priority = priority;
applyNativeOpenAiContextOverride(clone, contextCap);
// GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
// Older natives (gpt-5.5) get mock max + ultra
// (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone);
return ensureStrictCatalogFields(normalizeServiceTiers(clone));
}

export function isExactComboCatalogModel(
model: CatalogModel | undefined,
exactComboSlugs: ReadonlySet<string>,
): boolean {
return model?.provider === COMBO_NAMESPACE && exactComboSlugs.has(catalogModelSlug(model));
}

export function isExactComboCatalogEntry(
entry: RawEntry,
exactComboSlugs: ReadonlySet<string>,
): boolean {
return entry.owned_by === COMBO_NAMESPACE
&& typeof entry.slug === "string"
&& exactComboSlugs.has(entry.slug);
}

/**
* Friendly Codex-picker label for a routed `provider/model` slug. Command Code's two config
* ids differ by a single dash (`command-code` vs `commandcode`), so relabel them to the
* lowercase-dash style the opencode presets use: `commandcode-auth/x` and `commandcode-api/x`.
* The model-id portion also carries a redundant `<vendor>-` prefix (`deepseek-deepseek-v4-flash`)
* that is dropped for display. Google Antigravity is relabeled to the compact `agy/` prefix for
* the same reason: `google-antigravity/` alone consumes most of the picker row. That prefix comes
* from the row's own `providerAlias`, decided once per gather flight; `null` means a cross-provider
* collision suppressed it and the canonical slug stands. This is the raw-slug path only -- a
* configured `modelAliases` entry is labeled by the effective-alias path in
* catalog/provider-fetch.ts (#2960) and keeps the canonical provider name. All other providers
* keep the raw slug exactly as before.
*/
function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick<OcxConfig, "providers">): string {
const slash = slug.indexOf("/");
if (slash <= 0) return slug;
const provider = slug.slice(0, slash);
let modelId = slug.slice(slash + 1);
if (provider === "google-antigravity") {
if (model?.providerAlias === null) return slug;
const alias = (typeof model?.providerAlias === "string" && model.providerAlias.trim().length > 0)
? model.providerAlias.trim()
: effectiveProviderAlias(provider, undefined, config);
return alias ? `${alias}/${modelId}` : slug;
}
if (provider === "command-code" || provider === "commandcode") {
const m = modelId.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
if (m && modelId.startsWith(`${m[1]}-${m[1]}-`)) modelId = modelId.slice(m[1]!.length + 1);
return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${modelId}`;
}
return slug;
}

function preservePinnedNativeCustomReasoning(model?: CatalogModel): boolean {
return model !== undefined
&& model.catalogKind === CODEX_CUSTOM_MODEL_CATALOG_KIND
&& hasNativeOpenAiCapabilityMetadata(model.id)
&& Array.isArray(model.reasoningEfforts);
}

/**
* Cria uma entrada nativa ou roteada a partir do snapshot upstream, de um clone
* do template ou de campos mínimos. Aplica os metadados e limites pertinentes
* sem alterar o template nem herdar sua marca de nome ou histórico de prioridade.
*/
export function deriveEntry(
template: RawEntry | null,
slug: string,
desc: string,
priority: number,
model?: CatalogModel,
exactComboSlugs: ReadonlySet<string> = new Set(),
contextCap?: NativeContextLimitsInput,
): RawEntry {
const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
// Go exposes model-specific upstream enums; synthetic tiers mislead subagent overrides.
const preserveExactReasoning = preserveExact || model?.provider === "opencode-go";
const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
? upstreamNativeEntry(model.id)
: null;
const isRouted = model !== undefined;
if (!isRouted && !slug.includes("/")) {
// Supported native slug covered by the upstream snapshot: use the REAL entry (exact
// reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
// instead of cloning an older template.
const upstream = upstreamNativeEntry(slug);
if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap);
}
if (template || codexForwardNativeCapabilityAlias) {
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
delete e.opencodex_native_display_name;
// A cached template may carry display-order history; each new row owns its natural rank.
delete e[SPAWN_PRIORITY_FIELD];
e.slug = slug;
e.display_name = routedDisplayName(slug, model);
e.description = desc;
e.priority = priority;
e.visibility = "list";
if ("upgrade" in e) e.upgrade = null;
delete e.availability_nux; // don't replay another model's "now available" NUX
// Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
// and advertise the reasoning ladder Codex accepts.
if (isRouted) {
// A routed model is NOT the native template: never inherit its context
// window when /models omits context metadata (#992). Known metadata
// restores exact values below; an enabled Context cap fills the gap;
// otherwise the strict-fields fallback supplies the 128k triple.
if (!codexForwardNativeCapabilityAlias) {
delete e.context_window;
delete e.max_context_window;
delete e.auto_compact_token_limit;
}
// Native id for identity text + metadata lookups — the slug may be an encoded
// alias (`provider/vendor-model`); the model object carries the native id.
const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
if (typeof e.base_instructions === "string") {
// Proxy-neutral: keep the GPT-5/OpenAI disclaimer but never advertise the opencodex proxy
// (leaking that into base_instructions is a non-first-party signature → ToS risk).
e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
}
applyReasoningLevels(
e,
model?.reasoningEfforts,
model?.defaultReasoningEffort,
preserveExactReasoning
|| codexForwardNativeCapabilityAlias !== null
|| preservePinnedNativeCustomReasoning(model),
);
// This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
// native tool/search/responses-lite contract while preserving the routed slug and wire id.
if (!codexForwardNativeCapabilityAlias) {
normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true, model?.codexToolMode);
} else if (model?.codexToolMode !== undefined) {
applyRoutedCodexToolMode(e, model.codexToolMode);
}
if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
applyCatalogModelMetadata(e, model);
if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
// Additive only. `visibility` is untouched: an inactive row must still be OFFERED, which is
// the whole point of #1711 — operator disable is what removes rows, and it stays a separate
// path from this one.
if (model?.quotaInactiveReason) e[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
} else {
applyNativeOpenAiContextOverride(e, contextCap);
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
else ensureUltraReasoningLevel(e);
// Older natives do not support Responses Lite. A newer template must not enable
// reasoning.context or WebSockets on those models.
if (!isGpt56NativeSlug(slug)) {
delete e.use_responses_lite;
delete e.supports_websockets;
}
}
return ensureStrictCatalogFields(normalizeServiceTiers(e), {
preserveExactInputModalities: preserveExact,
isRouted,
});
}
// Fallback when no template is available (best-effort; strict parser may need more).
// Routed fallbacks default to code-mode tool exposure (or shell mode when codexToolMode === "shell");
// otherwise the nested catalog expands into `exec.description` and can exceed Cursor's 120 KB serialized tool limit (#1830).
// Cursor still omits hosted web-search metadata because runTurn bypasses that separate sidecar.
const isCursorFallback = isRouted && model?.provider === "cursor";
const entry: RawEntry = {
slug, display_name: routedDisplayName(slug, model), description: desc,
shell_type: "unified_exec", visibility: "list", supported_in_api: true,
priority, base_instructions: "You are a helpful coding assistant.",
...(isRouted
? isCursorFallback
? { supports_search_tool: true }
: { web_search_tool_type: "text_and_image", supports_search_tool: true }
: {}),
};
if (isRouted) {
applyRoutedCodexToolMode(entry, model?.codexToolMode);
applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning || preservePinnedNativeCustomReasoning(model));
}
else {
applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
}
if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
applyCatalogModelMetadata(entry, model);
if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
// Same additive stamp as the templated path above. A routed row that reaches the no-template
// fallback is still a served row, so omitting it here would make the field depend on whether a
// template happened to be cached — which is exactly what the regression test caught.
if (model?.quotaInactiveReason) entry[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap);
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
preserveExactInputModalities: preserveExact,
isRouted,
});
}
1 change: 0 additions & 1 deletion src/codex/catalog/effort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import { nativeOpenAiCapabilitySourceSlug, SELF_DESCRIBED_NATIVE_OPENAI_MODELS,
import { isReserveCatalogProjection } from "./reserve";
import { loadBundledCodexCatalog } from "./bundled";
import type { BundledCatalogDeps, ReadonlyRawCatalog } from "./bundled";
import { deriveEntry } from "./sync";
import {
formatClampLogLines,
formatRuntimeLogLine,
Expand Down
63 changes: 63 additions & 0 deletions src/codex/catalog/gated-native-warn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { OcxConfig } from "../../types";
import { codexModelEntitlementStateForAccount, type CodexModelEntitlementSnapshot } from "../model-entitlements";
import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "../account-label";
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";

export function gatedNativeReauthSuppressionReason(args: {
snapshot: CodexModelEntitlementSnapshot;
slug: string;
eligibleAccountIds?: ReadonlySet<string>;
needsReauth: (accountId: string) => boolean;
label: (accountId: string) => string;
}): string | undefined {
const observed = [...args.snapshot.modelsByAccount.keys()]
.filter(accountId => !args.eligibleAccountIds || args.eligibleAccountIds.has(accountId))
// Only accounts that could actually have served THIS model. An account upstream positively
// denied is not why the model is missing, and blaming it would send the operator to repair a
// credential that was never going to help. `unknown` has to stay in: an account whose roster
// could not be confirmed reports `unknown` rather than `granted`, and a credential stuck on
// a failed refresh is exactly that account.
.filter(accountId => (
codexModelEntitlementStateForAccount(args.snapshot, accountId, args.slug) !== "denied"
));
const stuck = observed.filter(accountId => args.needsReauth(accountId));
if (stuck.length === 0) return undefined;
const names = stuck.map(accountId => args.label(accountId)).sort().join(", ");
return stuck.length === observed.length
? `every Codex account that could serve it needs reauthentication (${names})`
: `${stuck.length} of ${observed.length} Codex accounts that could serve it need reauthentication (${names})`;
}

/** Durable, operator-facing label for a pool account id; never the raw id or the email. */
export function gatedNativeAccountLabel(config: OcxConfig, accountId: string): string {
// Direct mode narrows eligibility to the native main credential, so this is the account most
// likely to be named here. `codexAuthContextLogLabel` calls it "main" everywhere else; hashing
// it into a `p`-prefixed digest would name the one account the operator cannot look up.
if (accountId === MAIN_CODEX_ACCOUNT_ID) return "main";
const account = (config.codexAccounts ?? []).find(candidate => candidate.id === accountId);
return account ? codexAccountLogLabel(account) : fallbackCodexAccountLogLabel(accountId);
}

const warnedGatedNativeSuppression = new Set<string>();

/** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
export function resetGatedNativeSuppressionWarningsForTests(): void {
warnedGatedNativeSuppression.clear();
}

export function warnGatedNativeSuppressedOnce(slug: string, reason: string): void {
const signature = `${slug}\u0000${reason}`;
if (warnedGatedNativeSuppression.has(signature)) return;
warnedGatedNativeSuppression.add(signature);
console.warn(
`[opencodex] catalog sync: ${slug} is not being offered because ${reason}. `
+ "Sign in again to restore it.",
);
}

/**
* Mescla o catálogo retido com os modelos visíveis e as configurações atuais,
* incluindo os nomes nativos. Tenta preservar o backup original e usa a permissão
* de escrita para publicar o resultado apenas se os bytes mudarem, retornando
* a contagem de entradas roteadas e por conta, o caminho e o estado da gravação.
*/
Loading
Loading