Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export const CODEX_PROVIDER_MODEL_CATALOG_KIND = "provider-model-v1";
export interface CatalogModel {
id: string;
provider: string;
/** Canonical or configured short alias for the provider segment. */
providerAlias?: string | null;
/** Public Codex-facing slug override (used by combo aliases). */
alias?: string;
/** Explicit combo takeover of a bare OpenAI-native catalog id. */
Expand Down
43 changes: 31 additions & 12 deletions src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
import { execFileSync } from "node:child_process";
import { createHash, createHmac, randomBytes } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
Expand Down Expand Up @@ -164,6 +165,7 @@ interface CapturedProviderGather {
readonly request: CapturedModelsRequest;
readonly fastPolicyAuthority: FastPolicyAuthority;
readonly metadataModelIdCaseFold: boolean;
readonly effectiveAlias?: string | null;
readonly observedAuth?: ModelsAuthResolution;
/**
* Configured model ids this provider must keep even when live discovery omits
Expand Down Expand Up @@ -414,6 +416,7 @@ function captureProviderGather(
configured: OcxProviderConfig,
authResolver: ModelsAuthResolver,
retainConfiguredModelIds?: ReadonlySet<string>,
config?: Pick<OcxConfig, "providers">,
): CapturedProviderGather {
const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
enrichProviderFromRegistry(name, enriched);
Expand Down Expand Up @@ -455,6 +458,7 @@ function captureProviderGather(
maxModels: discovery.maxModels,
trustedOpenAiApi,
});
const effectiveAlias = effectiveProviderAliasDecision(name, configured, config);
return Object.freeze({
name,
provider,
Expand All @@ -463,6 +467,7 @@ function captureProviderGather(
request,
fastPolicyAuthority,
metadataModelIdCaseFold,
effectiveAlias,
...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}),
...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0
? { retainConfiguredModelIds }
Expand Down Expand Up @@ -504,6 +509,7 @@ function captureGatherFlight(
provider,
authResolver,
comboTargetsByProvider.get(name),
config,
));
const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy));
return Object.freeze({
Expand Down Expand Up @@ -729,8 +735,17 @@ export function applyProviderConfigHints(
model: CatalogModel,
providerCap?: number,
metadataModelIdCaseFold?: boolean,
effectiveAlias?: string | null,
): CatalogModel {
const displayName = configuredModelDisplayName(prov, model.id);
// The alias decision is resolved once at flight admission (captureProviderGather) and threaded
// through as `effectiveAlias`. Re-deriving it here would read PROVIDER_REGISTRY after admission,
// which is exactly the authority leak tests/codex-integration/codex-gather-authority.test.ts
// forbids: a flight must not consult the live registry once its transport has been captured.
// When no decision was threaded in, carry whatever the row already resolved to instead.
const providerAlias = typeof effectiveAlias === "string" || effectiveAlias === null
? effectiveAlias
: model.providerAlias;
const configuredCap = configuredContextWindow(prov, model.id);
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold);
Expand All @@ -756,6 +771,7 @@ export function applyProviderConfigHints(
const {
supportsServiceTier: _staleServiceTier,
fastTierDescription: _staleFastTierDescription,
providerAlias: _staleProviderAlias,
...modelWithoutServiceTier
} = model;
// 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
Expand All @@ -768,6 +784,7 @@ export function applyProviderConfigHints(
const hinted = {
...modelWithoutServiceTier,
...(displayName !== undefined ? { displayName } : {}),
...(providerAlias !== undefined ? { providerAlias } : {}),
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
...(inputModalities ? { inputModalities } : {}),
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
Expand Down Expand Up @@ -827,8 +844,9 @@ export function catalogHintsFromProviderConfig(
id: string,
contextCap?: number,
metadataModelIdCaseFold?: boolean,
effectiveAlias?: string | null,
): Partial<CatalogModel> {
const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold);
const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias);
const { provider: _provider, id: _id, ...hints } = hinted;
return hints;
}
Expand All @@ -839,8 +857,9 @@ export function applyConfigHintsToCachedModels(
models: CatalogModel[],
contextCap?: number,
metadataModelIdCaseFold?: boolean,
effectiveAlias?: string | null,
): CatalogModel[] {
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold));
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
}


Expand Down Expand Up @@ -1468,7 +1487,7 @@ async function fetchProviderModelsWithAuth(
const configured: CatalogModel[] = configuredIds.map(id => ({
id,
provider: name,
...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold),
...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
}));
const withConfiguredRetention = (
models: CatalogModel[],
Expand Down Expand Up @@ -1522,7 +1541,7 @@ async function fetchProviderModelsWithAuth(
: [{
id: prov.defaultModel,
provider: name,
...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, metadataModelIdCaseFold),
...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
}];
const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined;
const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => (
Expand All @@ -1539,15 +1558,15 @@ async function fetchProviderModelsWithAuth(
const cachedCursor = getFreshCached(name, ttlMs);
if (cachedCursor) {
return observed(
withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold)),
withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias)),
"authoritative",
);
}
if (isModelsFetchCoolingDown(name)) {
const cooling = getStaleCached(name);
return observed(
withConfiguredRetention(
cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold) : configured,
cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
),
"degraded",
);
Expand Down Expand Up @@ -1588,7 +1607,7 @@ async function fetchProviderModelsWithAuth(
const staleCursor = getStaleCached(name);
return observed(
withConfiguredRetention(
staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold) : configured,
staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
),
"degraded",
);
Expand All @@ -1606,7 +1625,7 @@ async function fetchProviderModelsWithAuth(
if (fresh) {
return observed(
withConfiguredRetention(
withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold)),
withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold, captured.effectiveAlias)),
),
"authoritative",
); // dedups Codex's frequent /v1/models polling within the TTL
Expand All @@ -1618,7 +1637,7 @@ async function fetchProviderModelsWithAuth(
return observed(
withConfiguredRetention(
stale
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold))
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias))
: failedDiscoveryConfigured,
),
"degraded",
Expand Down Expand Up @@ -1658,7 +1677,7 @@ async function fetchProviderModelsWithAuth(
return {
models: withConfiguredRetention(
stale
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold))
? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias))
: failedDiscoveryConfigured,
),
fallback: stale ? "stale" : "configured",
Expand Down Expand Up @@ -1735,7 +1754,7 @@ async function fetchProviderModelsWithAuth(
reasoningEfforts: [],
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
...(model.inputModalities ? { inputModalities: model.inputModalities } : {}),
}, contextCap, metadataModelIdCaseFold));
}, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
const forCache = withConfiguredRetention(live, { retainComboTargets: false });
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
return observed(withConfiguredRetention(configured), "degraded");
Expand Down Expand Up @@ -1798,7 +1817,7 @@ async function fetchProviderModelsWithAuth(
provider: name,
...(ownedBy ? { owned_by: ownedBy } : {}),
...discoveredHints,
}, contextCap, metadataModelIdCaseFold);
}, contextCap, metadataModelIdCaseFold, captured.effectiveAlias);
})
.filter(m => shouldExposeProviderModel(name, m.id));
// Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
Expand Down
30 changes: 22 additions & 8 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { effectiveProviderAlias } from "../../providers/default-aliases";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
Expand Down Expand Up @@ -267,17 +268,30 @@ function isExactComboCatalogEntry(
* 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. All other providers keep the raw slug exactly as before.
* 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): string {
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 model = slug.slice(slash + 1);
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 = model.match(/^([a-z0-9]+)-([a-z0-9]+(?:-[a-z0-9]+)+)$/i);
if (m && model.startsWith(`${m[1]}-${m[1]}-`)) model = model.slice(m[1]!.length + 1);
return `${provider === "command-code" ? "commandcode-auth" : "commandcode-api"}/${model}`;
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;
}
Expand Down Expand Up @@ -306,7 +320,7 @@ export function deriveEntry(
if (template || codexForwardNativeCapabilityAlias) {
const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
e.slug = slug;
e.display_name = routedDisplayName(slug);
e.display_name = routedDisplayName(slug, model);
e.description = desc;
e.priority = priority;
e.visibility = "list";
Expand Down Expand Up @@ -375,7 +389,7 @@ export function deriveEntry(
// 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), description: desc,
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
Expand Down
39 changes: 39 additions & 0 deletions src/providers/default-aliases.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@

import { PROVIDER_REGISTRY } from "./registry";

export function effectiveProviderAlias(
providerName: string,
provider?: Pick<OcxProviderConfig, "alias">,
config?: Pick<OcxConfig, "providers">,
): string | undefined {
if (provider && provider.alias !== undefined) {
const trimmed = provider.alias.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
const regAlias = PROVIDER_REGISTRY.find(e => e.id === providerName)?.alias;
if (!regAlias) return undefined;
if (config?.providers) {
const lower = regAlias.toLowerCase();
const claimedByOther = Object.entries(config.providers).some(([name, p]) =>
name !== providerName && typeof p.alias === "string" && p.alias.trim().toLowerCase() === lower
);
Comment on lines +17 to +19

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Suppress aliases claimed by canonical provider names

When a valid custom provider is named agy alongside an unaliased google-antigravity provider, this check ignores that provider key and still labels Google's catalog rows as agy/<model>. In src/router.ts, the exact hasOwnProvider(config.providers, requestedProvider) lookup runs before alias resolution, so selecting that advertised row silently routes the Google model ID through the custom agy provider. Treat other configured provider names as alias claims here, not only their alias fields.

Useful? React with 👍 / 👎.

if (claimedByOther) return undefined;
}
return regAlias;
}

export function effectiveProviderAliasDecision(
providerName: string,
provider?: Pick<OcxProviderConfig, "alias">,
config?: Pick<OcxConfig, "providers">,
): string | null | undefined {
const active = effectiveProviderAlias(providerName, provider, config);
if (active !== undefined) return active;
const hasRegistryAlias = Boolean(PROVIDER_REGISTRY.find(e => e.id === providerName)?.alias);
const hasConfiguredAlias = provider?.alias !== undefined;
if (hasRegistryAlias || hasConfiguredAlias) {
return null;
}
return undefined;
}

import type { OcxConfig, OcxProviderConfig } from "../types";

export const MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
Expand Down
1 change: 1 addition & 0 deletions src/providers/derive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
baseUrl: entry.baseUrl,
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
...(entry.alias ? { alias: entry.alias } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the registry fallback distinct from configured aliases

For the normal Google Antigravity OAuth flow, upsertOAuthProvider clones the result of providerConfigSeed, so this stores agy in provider.alias as though the operator explicitly configured it. If another provider already explicitly owns agy before Antigravity is added, the intended registry-fallback suppression is bypassed: both the router and catalog see two configured aliases, yielding an ambiguous/unusable advertised namespace rather than keeping the Google row canonical. The new tests construct the Google provider without this real seed path, so they miss the regression.

Useful? React with 👍 / 👎.

// Preserve the registry auth kind verbatim (including "local") so fail-closed gates that
// distinguish local runtimes from API-key providers keep working after the seed round-trip.
authMode: entry.authKind,
Expand Down
3 changes: 2 additions & 1 deletion src/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export interface ProviderRegistryEntry {
adapter: string;
baseUrl: string;
apiKeyTransport?: OcxProviderConfig["apiKeyTransport"];
alias?: string;
authKind: ProviderAuthKind;
codexAccountMode?: CodexAccountMode;
/** OAuth preset may explicitly honor a persisted API-key billing mode. */
Expand Down Expand Up @@ -1900,7 +1901,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
// 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
// evidence from ai.google.dev does not establish Vertex publisher availability.
{ id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
{ id: "google-antigravity", alias: "agy", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
Expand Down
Loading
Loading