From 5676a803d3a6f991a0db4f370463d870a04ec85d Mon Sep 17 00:00:00 2001 From: Benedictus Reynaldo Hartanto Date: Sat, 5 Sep 2026 06:16:38 +0900 Subject: [PATCH] feat(catalog,providers): compact google-antigravity to agy across display and routing --- src/codex/catalog/parsing.ts | 2 + src/codex/catalog/provider-fetch.ts | 36 +- src/codex/catalog/sync.ts | 22 +- src/providers/default-aliases.ts | 39 ++ src/providers/derive.ts | 1 + src/providers/registry.ts | 3 +- src/router.ts | 37 +- tests/codex-integration/codex-catalog.test.ts | 23 ++ .../providers/provider-model-aliases.test.ts | 334 ++++++++++++++++++ 9 files changed, 473 insertions(+), 24 deletions(-) diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index d8d2479032..ceb86de551 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -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. */ diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index a759126e3c..147440f15e 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -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"; @@ -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 @@ -414,6 +416,7 @@ function captureProviderGather( configured: OcxProviderConfig, authResolver: ModelsAuthResolver, retainConfiguredModelIds?: ReadonlySet, + config?: Pick, ): CapturedProviderGather { const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured)); enrichProviderFromRegistry(name, enriched); @@ -455,6 +458,7 @@ function captureProviderGather( maxModels: discovery.maxModels, trustedOpenAiApi, }); + const effectiveAlias = effectiveProviderAliasDecision(name, configured, config); return Object.freeze({ name, provider, @@ -463,6 +467,7 @@ function captureProviderGather( request, fastPolicyAuthority, metadataModelIdCaseFold, + effectiveAlias, ...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}), ...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0 ? { retainConfiguredModelIds } @@ -504,6 +509,7 @@ function captureGatherFlight( provider, authResolver, comboTargetsByProvider.get(name), + config, )); const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy)); return Object.freeze({ @@ -729,8 +735,10 @@ export function applyProviderConfigHints( model: CatalogModel, providerCap?: number, metadataModelIdCaseFold?: boolean, + effectiveAlias?: string | null, ): CatalogModel { const displayName = configuredModelDisplayName(prov, model.id); + const providerAlias = typeof effectiveAlias === "string" || effectiveAlias === null ? effectiveAlias : effectiveProviderAliasDecision(name, prov); const configuredCap = configuredContextWindow(prov, model.id); const configuredMaxInput = configuredMaxInputTokens(prov, model.id); const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold); @@ -756,6 +764,7 @@ export function applyProviderConfigHints( const { supportsServiceTier: _staleServiceTier, fastTierDescription: _staleFastTierDescription, + providerAlias: _staleProviderAlias, ...modelWithoutServiceTier } = model; // 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。 @@ -768,6 +777,7 @@ export function applyProviderConfigHints( const hinted = { ...modelWithoutServiceTier, ...(displayName !== undefined ? { displayName } : {}), + ...(providerAlias !== undefined ? { providerAlias } : {}), ...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}), ...(inputModalities ? { inputModalities } : {}), ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), @@ -827,8 +837,9 @@ export function catalogHintsFromProviderConfig( id: string, contextCap?: number, metadataModelIdCaseFold?: boolean, + effectiveAlias?: string | null, ): Partial { - 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; } @@ -839,8 +850,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)); } @@ -1468,7 +1480,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[], @@ -1522,7 +1534,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[] => ( @@ -1539,7 +1551,7 @@ 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", ); } @@ -1547,7 +1559,7 @@ async function fetchProviderModelsWithAuth( 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", ); @@ -1588,7 +1600,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", ); @@ -1606,7 +1618,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 @@ -1618,7 +1630,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", @@ -1658,7 +1670,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", @@ -1735,7 +1747,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"); @@ -1798,7 +1810,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 diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index b9527f3a50..df1dbdba76 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -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"; @@ -269,15 +270,22 @@ function isExactComboCatalogEntry( * The model-id portion also carries a redundant `-` prefix (`deepseek-deepseek-v4-flash`) * that is dropped for display. All other providers keep the raw slug exactly as before. */ -function routedDisplayName(slug: string): string { +function routedDisplayName(slug: string, model?: CatalogModel, config?: Pick): 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; } @@ -306,7 +314,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"; @@ -375,7 +383,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 diff --git a/src/providers/default-aliases.ts b/src/providers/default-aliases.ts index ae7914c177..b11078bedc 100644 --- a/src/providers/default-aliases.ts +++ b/src/providers/default-aliases.ts @@ -1,3 +1,42 @@ + +import { PROVIDER_REGISTRY } from "./registry"; + +export function effectiveProviderAlias( + providerName: string, + provider?: Pick, + config?: Pick, +): 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 + ); + if (claimedByOther) return undefined; + } + return regAlias; +} + +export function effectiveProviderAliasDecision( + providerName: string, + provider?: Pick, + config?: Pick, +): 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._-]*$/; diff --git a/src/providers/derive.ts b/src/providers/derive.ts index f17ff4d690..02852fce39 100644 --- a/src/providers/derive.ts +++ b/src/providers/derive.ts @@ -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 } : {}), // 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, diff --git a/src/providers/registry.ts b/src/providers/registry.ts index e22d502379..f78e82b965 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -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. */ @@ -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" }, diff --git a/src/router.ts b/src/router.ts index 874af4633c..1dcd78481e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -679,10 +679,39 @@ function routeModelInternal( // no such provider exists. if (slash > 0) { const requestedProvider = modelId.slice(0, slash); - const provName = hasOwnProvider(config.providers, requestedProvider) - ? requestedProvider - : Object.entries(config.providers).find(([, provider]) => - typeof provider.alias === "string" && provider.alias.toLowerCase() === requestedProvider.toLowerCase())?.[0]; + const requestedLower = requestedProvider.toLowerCase(); + let provName: string | undefined; + + if (hasOwnProvider(config.providers, requestedProvider)) { + provName = requestedProvider; + } else { + // Pass 1: explicit configured provider aliases (operator override always wins) + const configuredMatches = Object.entries(config.providers).filter(([, provider]) => + typeof provider.alias === "string" && provider.alias.trim().toLowerCase() === requestedLower, + ); + if (configuredMatches.length === 1) { + provName = configuredMatches[0]![0]; + } else if (configuredMatches.length > 1) { + throw new Error("provider alias '" + requestedProvider + "' is ambiguous: " + configuredMatches.map(([n]) => n).sort().join(", ")); + } else { + // Pass 2: built-in registry aliases, only for providers that do NOT have an explicit alias override + // and whose registry alias has not been claimed by another configured provider (#3531 review) + const registryMatches = Object.entries(config.providers).filter(([name, provider]) => { + if (provider.alias !== undefined) return false; + const regAlias = PROVIDER_REGISTRY.find(e => e.id === name)?.alias; + if (!regAlias || regAlias.toLowerCase() !== requestedLower) return false; + const claimedByOther = Object.entries(config.providers).some(([otherName, p]) => + otherName !== name && typeof p.alias === "string" && p.alias.trim().toLowerCase() === requestedLower + ); + return !claimedByOther; + }); + if (registryMatches.length === 1) { + provName = registryMatches[0]![0]; + } else if (registryMatches.length > 1) { + throw new Error("provider alias '" + requestedProvider + "' is ambiguous across registry fallbacks: " + registryMatches.map(([n]) => n).sort().join(", ")); + } + } + } if (!provName) { // A genuine slash-containing native model id still falls through unchanged. } else { diff --git a/tests/codex-integration/codex-catalog.test.ts b/tests/codex-integration/codex-catalog.test.ts index 8bad4599ce..febaf976cb 100644 --- a/tests/codex-integration/codex-catalog.test.ts +++ b/tests/codex-integration/codex-catalog.test.ts @@ -2176,6 +2176,29 @@ describe("configured CatalogModel displayName -> catalog display_name", () => { expect(api?.slug).toBe("commandcode/deepseek-deepseek-v4-pro"); }); + test("Google Antigravity routed models relabel the picker row with compact agy prefix", () => { + const entries = buildCatalogEntries(nativeTemplate(), [], [ + { provider: "google-antigravity", id: "gemini-3.8-flash", owned_by: "google-antigravity" }, + { provider: "google-antigravity", id: "claude-sonnet-4-6", owned_by: "google-antigravity" }, + ]); + const gemini = entries.find(e => e.slug === "google-antigravity/gemini-3.8-flash"); + const claude = entries.find(e => e.slug === "google-antigravity/claude-sonnet-4-6"); + + // Display-only relabel: routing slugs stay untouched. + expect(gemini?.display_name).toBe("agy/gemini-3.8-flash"); + expect(gemini?.slug).toBe("google-antigravity/gemini-3.8-flash"); + expect(claude?.display_name).toBe("agy/claude-sonnet-4-6"); + expect(claude?.slug).toBe("google-antigravity/claude-sonnet-4-6"); + }); + + test("Google Antigravity respects custom providerAlias on catalog display", () => { + const entries = buildCatalogEntries(nativeTemplate(), [], [ + { provider: "google-antigravity", id: "gemini-3.8-flash", providerAlias: "antigrav", owned_by: "google-antigravity" }, + ]); + const gemini = entries.find(e => e.slug === "google-antigravity/gemini-3.8-flash"); + expect(gemini?.display_name).toBe("antigrav/gemini-3.8-flash"); + }); + test("empty/whitespace displayName is ignored and falls back to the slug", () => { const entries = buildCatalogEntries(nativeTemplate(), [], [ { provider: "deepseek", id: "deepseek-v4", displayName: " ", owned_by: "deepseek" }, diff --git a/tests/providers/provider-model-aliases.test.ts b/tests/providers/provider-model-aliases.test.ts index 4ecc4198e1..d326a8562e 100644 --- a/tests/providers/provider-model-aliases.test.ts +++ b/tests/providers/provider-model-aliases.test.ts @@ -1,3 +1,7 @@ +import { clearModelCache } from "../../src/codex/model-cache"; +import { gatherRoutedModels } from "../../src/codex/catalog/provider-fetch"; +import { applyProviderConfigHints } from "../../src/codex/catalog/provider-fetch"; +import { buildCatalogEntries } from "../../src/codex/catalog/sync"; import { describe, expect, test } from "bun:test"; import { effectiveModelAliases } from "../../src/providers/default-aliases"; import { routeModel } from "../../src/router"; @@ -58,4 +62,334 @@ describe("provider and model aliases", () => { ["anthropic/claude-opus-5-a", { alias: "opus", source: "builtin" }], ]); }); + test("google-antigravity provider compact alias agy resolves to native google-antigravity", () => { + const c = { + port: 10100, + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + models: ["gemini-3.8-flash", "claude-sonnet-4-6"], + }, + }, + } as unknown as OcxConfig; + + // Resolves with agy prefix + expect(routeModel(c, "agy/gemini-3.8-flash")).toMatchObject({ + providerName: "google-antigravity", + modelId: "gemini-3.8-flash", + routeReason: "explicit-provider-namespace", + }); + + // Case-insensitive alias + expect(routeModel(c, "AGY/gemini-3.8-flash")).toMatchObject({ + providerName: "google-antigravity", + modelId: "gemini-3.8-flash", + }); + + // Canonical full name remains valid and unaffected + expect(routeModel(c, "google-antigravity/gemini-3.8-flash")).toMatchObject({ + providerName: "google-antigravity", + modelId: "gemini-3.8-flash", + }); + }); + + test("custom provider alias overrides and disables the built-in registry alias", () => { + const custom = { + port: 10100, + defaultProvider: "openai", + providers: { + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + alias: "antigrav", + models: ["gemini-3.8-flash"], + }, + }, + } as unknown as OcxConfig; + + expect(routeModel(custom, "antigrav/gemini-3.8-flash")).toMatchObject({ + providerName: "google-antigravity", + modelId: "gemini-3.8-flash", + routeReason: "explicit-provider-namespace", + }); + // Negative assertion: explicit user alias disables the built-in registry alias + expect(() => routeModel(custom, "agy/gemini-3.8-flash")).toThrow("No provider configured for model: agy/gemini-3.8-flash"); + }); + + test("explicit configured alias wins over registry fallback independent of insertion order", () => { + const order1 = { + port: 10100, + defaultProvider: "google-antigravity", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://other.test/v1", + alias: "agy", + models: ["model-x"], + }, + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + models: ["gemini-3.8-flash"], + }, + }, + } as unknown as OcxConfig; + + const order2 = { + port: 10100, + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + models: ["gemini-3.8-flash"], + }, + other: { + adapter: "openai-chat", + baseUrl: "https://other.test/v1", + alias: "agy", + models: ["model-x"], + }, + }, + } as unknown as OcxConfig; + + expect(routeModel(order1, "agy/model-x")).toMatchObject({ + providerName: "other", + modelId: "model-x", + }); + expect(routeModel(order2, "agy/model-x")).toMatchObject({ + providerName: "other", + modelId: "model-x", + }); + }); + test("cross-provider alias ownership: when other explicitly claims agy, Google row suppresses agy and advertised namespace routes back to google-antigravity", async () => { + const c = { + port: 10100, + defaultProvider: "openai", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://other.test/v1", + alias: "agy", + models: ["model-x"], + }, + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + models: ["gemini-3.8-flash"], + }, + }, + } as unknown as OcxConfig; + + c.providers.other.liveModels = false; + c.providers["google-antigravity"].liveModels = false; + + // Real gather exercises captureGatherFlight and threads immutable effectiveAlias + const models = await gatherRoutedModels(c); + const googleModel = models.find(m => m.provider === "google-antigravity" && m.id === "gemini-3.8-flash")!; + const otherModel = models.find(m => m.provider === "other" && m.id === "model-x")!; + + const entries = buildCatalogEntries(null, [], [googleModel, otherModel]); + const googleEntry = entries.find(e => e.slug === "google-antigravity/gemini-3.8-flash")!; + const otherEntry = entries.find(e => e.slug === "other/model-x")!; + + // 1. Ownership collision: 'other' explicitly claimed 'agy', so Google row suppresses 'agy' + // and falls back to the canonical slug 'google-antigravity/gemini-3.8-flash'. + expect(googleEntry.display_name).toBe("google-antigravity/gemini-3.8-flash"); + expect(otherEntry.display_name).toBe("other/model-x"); + + // 2. Routing fidelity: Every advertised Google namespace routes back to google-antigravity! + expect(routeModel(c, googleEntry.display_name)).toMatchObject({ + providerName: "google-antigravity", + modelId: "gemini-3.8-flash", + routeReason: "explicit-provider-namespace", + }); + + // 3. 'agy/model-x' routes to 'other' (explicit configured alias ownership) + expect(routeModel(c, "agy/model-x")).toMatchObject({ + providerName: "other", + modelId: "model-x", + routeReason: "explicit-provider-namespace", + }); + }); + test("static gather (liveModels: false) suppresses agy when other provider explicitly owns it", async () => { + const c = { + port: 10100, + defaultProvider: "openai", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://other.test/v1", + alias: "agy", + models: ["model-x"], + liveModels: false, + }, + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + models: ["gemini-3.8-flash"], + liveModels: false, + }, + }, + } as unknown as OcxConfig; + + const models = await gatherRoutedModels(c); + const googleModel = models.find(m => m.provider === "google-antigravity" && m.id === "gemini-3.8-flash")!; + expect(googleModel).toBeDefined(); + // Static gather applies captured effectiveAlias (null due to cross-provider collision) + expect(googleModel.providerAlias).toBeNull(); + + const entries = buildCatalogEntries(null, [], [googleModel]); + expect(entries[0]!.display_name).toBe("google-antigravity/gemini-3.8-flash"); + }); + test("real cache-boundary regression: live discovery primes cache and warm cache re-hints in both directions", async () => { + clearModelCache("google-antigravity"); + clearModelCache("other"); + + let fetchCalls = 0; + const stubFetch = (async () => { + fetchCalls++; + return new Response(JSON.stringify({ + data: [{ id: "gemini-3.8-flash" }], + }), { status: 200, headers: { "Content-Type": "application/json" } }); + }) as unknown as typeof fetch; + + try { + const cAlone = { + port: 10100, + defaultProvider: "google-antigravity", + modelCacheTtlMs: 60000, + providers: { + "google-antigravity": { + adapter: "openai-chat", + baseUrl: "https://mock.google.test/v1", + authMode: "key", + apiKey: "test-key", + liveModels: true, + fetch: stubFetch, + }, + }, + } as unknown as OcxConfig; + + // 1. Initial live gather primes the cache under default alias ownership + const models1 = await gatherRoutedModels(cAlone); + expect(fetchCalls).toBe(1); // Exactly one outbound fetch + const entries1 = buildCatalogEntries(null, [], models1); + const e1 = entries1.find(e => e.slug === "google-antigravity/gemini-3.8-flash")!; + expect(e1.display_name).toBe("agy/gemini-3.8-flash"); + + // 2. Second gather inside TTL with conflicting ownership: cached row is re-hinted to canonical Google display + const cConflicting = { + port: 10100, + defaultProvider: "google-antigravity", + modelCacheTtlMs: 60000, + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://other.test/v1", + alias: "agy", + models: ["model-x"], + liveModels: false, + }, + "google-antigravity": { + adapter: "openai-chat", + baseUrl: "https://mock.google.test/v1", + authMode: "key", + apiKey: "test-key", + liveModels: true, + fetch: stubFetch, + }, + }, + } as unknown as OcxConfig; + + const models2 = await gatherRoutedModels(cConflicting); + expect(fetchCalls).toBe(1); // Cache hit, zero additional fetches + const entries2 = buildCatalogEntries(null, [], models2); + const e2 = entries2.find(e => e.slug === "google-antigravity/gemini-3.8-flash")!; + expect(e2.display_name).toBe("google-antigravity/gemini-3.8-flash"); + + // 3. Third gather inside TTL (inverse direction): conflict removed, cached row re-hints back to agy + const models3 = await gatherRoutedModels(cAlone); + expect(fetchCalls).toBe(1); // Cache hit, zero additional fetches + const entries3 = buildCatalogEntries(null, [], models3); + const e3 = entries3.find(e => e.slug === "google-antigravity/gemini-3.8-flash")!; + expect(e3.display_name).toBe("agy/gemini-3.8-flash"); + } finally { + clearModelCache("google-antigravity"); + clearModelCache("other"); + } + }); + + test("empty or cleared custom alias disables both custom and built-in registry alias", () => { + const c = { + port: 10100, + defaultProvider: "openai", + providers: { + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + alias: "", // explicit empty/cleared alias + models: ["gemini-3.8-flash"], + }, + }, + } as unknown as OcxConfig; + + // Setting empty alias disables built-in agy fallback + expect(() => routeModel(c, "agy/gemini-3.8-flash")).toThrow("No provider configured for model: agy/gemini-3.8-flash"); + // Canonical name routes cleanly + expect(routeModel(c, "google-antigravity/gemini-3.8-flash")).toMatchObject({ + providerName: "google-antigravity", + modelId: "gemini-3.8-flash", + }); + }); + test("boundary regression: collision-suppressed Google stays canonical while unaliased provider retains original model shape", async () => { + const c = { + port: 10100, + defaultProvider: "openai", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://other.test/v1", + alias: "agy", + models: ["model-x"], + liveModels: false, + }, + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + models: ["gemini-3.8-flash"], + liveModels: false, + }, + xai: { + adapter: "openai-chat", + baseUrl: "https://api.x.ai/v1", + models: ["grok-4.6"], + liveModels: false, + }, + }, + } as unknown as OcxConfig; + + const models = await gatherRoutedModels(c); + const googleModel = models.find(m => m.provider === "google-antigravity" && m.id === "gemini-3.8-flash")!; + const xaiModel = models.find(m => m.provider === "xai" && m.id === "grok-4.6")!; + + // 1. Collision-suppressed Google has providerAlias: null and stays canonical in display + expect(googleModel).toBeDefined(); + expect(googleModel.providerAlias).toBeNull(); + const entries = buildCatalogEntries(null, [], [googleModel]); + expect(entries[0]!.display_name).toBe("google-antigravity/gemini-3.8-flash"); + + // 2. Provider with no built-in/configured alias retains its original model shape (no providerAlias property) + expect(xaiModel).toBeDefined(); + expect("providerAlias" in xaiModel).toBe(false); + expect(Object.prototype.hasOwnProperty.call(xaiModel, "providerAlias")).toBe(false); + }); });