{"version":3,"sources":["/Users/ami/Documents/prometheus/x12i/ai-tools/dist/chunk-2DKAWNAV.cjs","../src/cache/modelCache.ts","../src/catalog/catalogSources.ts","../src/catalog/normalizeAiProfilesCatalog.ts","../src/catalog/loadCatalogSources.ts","../src/catalog/catalogLoadCache.ts","../src/catalog/AiModelsCatalogClient.ts"],"names":[],"mappings":"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","file":"/Users/ami/Documents/prometheus/x12i/ai-tools/dist/chunk-2DKAWNAV.cjs","sourcesContent":[null,"import { createNxCache } from \"nx-cache\";\nimport type { AiModelRecord } from \"../cost/types.js\";\n\nconst cache = createNxCache<Map<string, AiModelRecord>, { cachedAt: string; count: number }>();\n\n/** Default in-memory catalog cache lifetime (24 hours). */\nexport const DEFAULT_CATALOG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;\n\n/** @alias DEFAULT_CATALOG_CACHE_TTL_MS */\nexport const DEFAULT_MODEL_CACHE_TTL_MS = DEFAULT_CATALOG_CACHE_TTL_MS;\n\nexport function resolveCatalogCacheTtlMs(override?: number): number {\n  if (override !== undefined) return override;\n  const raw = process.env.AI_TOOLS_CACHE_TTL_MS;\n  if (!raw) return DEFAULT_CATALOG_CACHE_TTL_MS;\n  const n = Number.parseInt(raw, 10);\n  return Number.isFinite(n) && n > 0 ? n : DEFAULT_CATALOG_CACHE_TTL_MS;\n}\n\nexport type ModelCacheMetadata = { cachedAt: string; count: number };\n\nexport function readCachedModels(\n  scopeKey: string,\n  ttlMs = DEFAULT_CATALOG_CACHE_TTL_MS,\n): Map<string, AiModelRecord> | null {\n  const res = cache.read(\"ai-models\", { appId: scopeKey });\n  if (!res.found) return null;\n\n  const cachedAt = res.metadata?.cachedAt;\n  if (cachedAt) {\n    const age = Date.now() - new Date(cachedAt).getTime();\n    if (age > ttlMs) return null;\n  } else if (res.meta.isExpired) {\n    return null;\n  }\n\n  return res.value;\n}\n\nexport function writeCachedModels(\n  scopeKey: string,\n  models: Map<string, AiModelRecord>,\n  ttlMs = DEFAULT_CATALOG_CACHE_TTL_MS,\n): void {\n  cache.write(\"ai-models\", models, {\n    scope: { appId: scopeKey },\n    ttlMs,\n    metadata: { cachedAt: new Date().toISOString(), count: models.size },\n  });\n}\n\nexport function invalidateModelsCache(scopeKey: string): void {\n  cache.resetKey(\"ai-models\", { appId: scopeKey });\n}\n","import { MODELS_CATALOG_REMOTE_URL } from \"@x12i/ai-profiles\";\n\n/** Canonical remote models catalog (owned by @x12i/ai-profiles). */\nexport const DEFAULT_MODELS_CATALOG_URL = MODELS_CATALOG_REMOTE_URL;\n\n/** @deprecated Use {@link DEFAULT_MODELS_CATALOG_URL}. Kept for API compatibility. */\nexport const DEFAULT_DIRECT_CATALOG_URL = DEFAULT_MODELS_CATALOG_URL;\n\n/** @deprecated Dual OpenRouter catalog removed; unified catalog is the SoT. */\nexport const DEFAULT_OPENROUTER_CATALOG_URL = DEFAULT_MODELS_CATALOG_URL;\n\nexport type CatalogSourceKind = \"direct\" | \"openrouter\";\n","import type { CatalogModelEntry, ModelsCatalogJson } from \"@x12i/ai-profiles\";\nimport { computeSupportsReasoning } from \"../models/reasoningModel.js\";\nimport type { AiModelPricing, AiModelRecord } from \"../models/types.js\";\nimport type { OpenRouterModelApi, OpenRouterPricingApi } from \"../models/openrouter.types.js\";\nimport { normalizeProvider, normalizeString } from \"../sync/modelNameResolver/normalize.js\";\nimport type { CatalogSourceKind } from \"./catalogSources.js\";\n\nconst TOKENS_PER_UNIT = 1_000_000;\n\nfunction usdPerToken(perMillion?: number | null): number {\n  if (perMillion == null || !Number.isFinite(perMillion)) return 0;\n  return perMillion / TOKENS_PER_UNIT;\n}\n\nexport function canonicalCatalogModelId(provider: string, modelId: string): string {\n  const m = normalizeString(modelId);\n  if (m.includes(\"/\")) return m;\n  const p = normalizeProvider(provider) ?? normalizeString(provider);\n  return `${p}/${m}`;\n}\n\nfunction mapStatus(status?: string): AiModelRecord[\"status\"] {\n  if (!status) return \"active\";\n  const s = status.toLowerCase();\n  if (s === \"deprecated\" || s === \"legacy\") return \"deprecated\";\n  if (s === \"active\" || s === \"preview\" || s === \"announced\") return \"active\";\n  return \"unknown\";\n}\n\nfunction mapModalities(\n  modalities?: CatalogModelEntry[\"modalities\"] | CatalogModelEntry[\"openrouter\"][\"modalities\"],\n): { input: string[]; output: string[]; modality: string } {\n  if (modalities && typeof modalities === \"object\" && !Array.isArray(modalities)) {\n    const input = modalities.input ?? [\"text\"];\n    const output = modalities.output ?? [\"text\"];\n    return { input, output, modality: `${input.join(\"+\")}->${output.join(\"+\")}` };\n  }\n  return { input: [\"text\"], output: [\"text\"], modality: \"text->text\" };\n}\n\nfunction pricingToOpenRouterApi(raw: NonNullable<\n  CatalogModelEntry[\"openrouter\"][\"pricing\"]\n>): OpenRouterPricingApi {\n  const scale = (n?: number) => (n ?? 0) / TOKENS_PER_UNIT;\n  return {\n    prompt: String(scale(raw.input)),\n    completion: String(scale(raw.output)),\n    request: \"0\",\n    image: String(scale(raw.imageInput)),\n    input_cache_read: raw.cachedInput != null ? String(scale(raw.cachedInput)) : undefined,\n    input_cache_write: raw.cacheWrite != null ? String(scale(raw.cacheWrite)) : undefined,\n    internal_reasoning:\n      raw.internalReasoning != null ? String(scale(raw.internalReasoning)) : undefined,\n    web_search:\n      raw.webSearchPerRequest != null ? String(raw.webSearchPerRequest) : undefined,\n  };\n}\n\nfunction pricingFromCatalog(\n  raw: NonNullable<CatalogModelEntry[\"openrouter\"][\"pricing\"]>,\n  pricedAt: string,\n  source: AiModelPricing[\"source\"],\n): AiModelPricing {\n  return {\n    promptUsdPerToken: usdPerToken(raw.input),\n    completionUsdPerToken: usdPerToken(raw.output),\n    imageUsdPerUnit: usdPerToken(raw.imageInput),\n    requestUsdPerRequest: 0,\n    cacheReadUsdPerToken: usdPerToken(raw.cachedInput),\n    cacheWriteUsdPerToken: usdPerToken(raw.cacheWrite),\n    reasoningUsdPerToken: usdPerToken(raw.internalReasoning),\n    webSearchUsdPerRequest:\n      typeof raw.webSearchPerRequest === \"number\" ? raw.webSearchPerRequest : undefined,\n    pricedAt,\n    source,\n  };\n}\n\nfunction stubOpenRouterApi(\n  modelId: string,\n  entry: CatalogModelEntry,\n  modalities: ReturnType<typeof mapModalities>,\n  pricing: OpenRouterPricingApi,\n  contextWindow: number,\n  maxCompletionTokens: number | null,\n): OpenRouterModelApi {\n  return {\n    id: modelId,\n    canonical_slug: entry.openrouter.canonicalSlug ?? modelId,\n    name: entry.displayName ?? modelId,\n    created: 0,\n    description: entry.displayName ?? \"\",\n    context_length: contextWindow,\n    architecture: {\n      modality: modalities.modality,\n      input_modalities: modalities.input,\n      output_modalities: modalities.output,\n      tokenizer: \"\",\n      instruct_type: null,\n    },\n    pricing,\n    top_provider: {\n      context_length: contextWindow,\n      max_completion_tokens: maxCompletionTokens,\n      is_moderated: false,\n    },\n    per_request_limits: null,\n    supported_parameters: entry.openrouter.capabilities?.toolCalling ? [\"tools\"] : [],\n    default_parameters: null,\n  };\n}\n\nfunction directProviderId(entry: CatalogModelEntry): string {\n  const direct = entry.direct;\n  if (direct.provider === \"custom\" && direct.vendorNamespace) {\n    return normalizeProvider(direct.vendorNamespace) ?? direct.vendorNamespace;\n  }\n  return normalizeProvider(direct.provider) ?? direct.provider;\n}\n\nfunction buildRecord(\n  entry: CatalogModelEntry,\n  kind: CatalogSourceKind,\n  pricedAt: string,\n): AiModelRecord | null {\n  const pricingRaw = entry.openrouter.pricing;\n  if (!pricingRaw) return null;\n  if (pricingRaw.input == null && pricingRaw.output == null) return null;\n\n  const modalities = mapModalities(entry.modalities ?? entry.openrouter.modalities);\n  const contextWindow = entry.contextWindow ?? entry.openrouter.contextWindow ?? 0;\n  const maxCompletionTokens =\n    entry.maxCompletionTokens ?? entry.openrouter.maxCompletionTokens ?? null;\n\n  const orSlug = normalizeString(entry.openrouter.modelId);\n  const providerId = directProviderId(entry);\n  const bareModelId = normalizeString(entry.direct.modelId);\n\n  const modelId =\n    kind === \"openrouter\"\n      ? orSlug\n      : canonicalCatalogModelId(providerId, bareModelId);\n\n  const pricingSource: AiModelPricing[\"source\"] =\n    kind === \"openrouter\" ? \"openrouter\" : \"direct\";\n  const pricing = pricingFromCatalog(pricingRaw, pricedAt, pricingSource);\n  const openRouterPricing = pricingToOpenRouterApi(pricingRaw);\n  const openRouter = stubOpenRouterApi(\n    modelId,\n    entry,\n    modalities,\n    openRouterPricing,\n    contextWindow,\n    maxCompletionTokens,\n  );\n\n  const supportedParameters = openRouter.supported_parameters ?? [];\n  const supportsReasoning =\n    entry.openrouter.capabilities?.reasoning === true ||\n    computeSupportsReasoning({\n      supportedParameters,\n      pricing,\n      openRouterPricing,\n    });\n\n  const aliases = new Set<string>();\n  aliases.add(bareModelId);\n  aliases.add(orSlug);\n  if (entry.displayName) aliases.add(entry.displayName);\n  const slash = orSlug.indexOf(\"/\");\n  if (slash > 0) aliases.add(orSlug.slice(slash + 1));\n  if (modelId !== orSlug && modelId.includes(\"/\")) {\n    aliases.add(modelId.slice(modelId.indexOf(\"/\") + 1));\n  }\n\n  return {\n    modelId,\n    name: entry.displayName ?? modelId,\n    providerId: kind === \"openrouter\" && orSlug.includes(\"/\")\n      ? orSlug.split(\"/\")[0]!\n      : providerId,\n    canonicalSlug: entry.openrouter.canonicalSlug ?? orSlug,\n    status: mapStatus(entry.status ?? entry.openrouter.status),\n    description: entry.displayName ?? \"\",\n    created: 0,\n    expirationDate: null,\n    contextLength: contextWindow,\n    maxCompletionTokens,\n    isModerated: false,\n    modality: modalities.modality,\n    inputModalities: modalities.input,\n    outputModalities: modalities.output,\n    tokenizer: \"\",\n    instructType: null,\n    supportedParameters,\n    defaultParameters: null,\n    perRequestLimits: null,\n    pricing,\n    openRouterPricing,\n    architecture: openRouter.architecture,\n    topProvider: openRouter.top_provider,\n    openRouter,\n    aliases: [...aliases],\n    availableOnOpenRouter: Boolean(entry.openrouter.modelId),\n    supportsStreaming: modalities.output.includes(\"text\"),\n    supportsTools: entry.openrouter.capabilities?.toolCalling === true,\n    supportsReasoning,\n    primaryOutputModality: modalities.output[0] ?? \"text\",\n    syncedAt: pricedAt,\n    syncSource: kind === \"openrouter\" ? \"openrouter\" : \"manual\",\n  };\n}\n\n/**\n * Split a unified ai-profiles models catalog into direct + OpenRouter record maps.\n */\nexport function modelsFromAiProfilesCatalog(catalog: ModelsCatalogJson): {\n  direct: Map<string, AiModelRecord>;\n  openrouter: Map<string, AiModelRecord>;\n} {\n  const direct = new Map<string, AiModelRecord>();\n  const openrouter = new Map<string, AiModelRecord>();\n  const pricedAt =\n    catalog.generatedAt ??\n    catalog.sources?.openrouter?.verifiedAt ??\n    new Date().toISOString();\n\n  for (const entry of catalog.models) {\n    const directRecord = buildRecord(entry, \"direct\", pricedAt);\n    if (directRecord) direct.set(directRecord.modelId, directRecord);\n\n    if (entry.openrouter.modelId) {\n      const orRecord = buildRecord(entry, \"openrouter\", pricedAt);\n      if (orRecord) openrouter.set(orRecord.modelId, orRecord);\n    }\n  }\n\n  return { direct, openrouter };\n}\n\n/** @deprecated Use {@link modelsFromAiProfilesCatalog}. */\nexport function modelsFromX12iCatalogFile(\n  file: ModelsCatalogJson,\n  _kind: CatalogSourceKind,\n): Map<string, AiModelRecord> {\n  const { direct, openrouter } = modelsFromAiProfilesCatalog(file);\n  return _kind === \"openrouter\" ? openrouter : direct;\n}\n","import {\n  loadModelsCatalog,\n  loadModelsCatalogWithWarnings,\n  MODELS_CATALOG_REMOTE_URL,\n  type RegistrySourceMode,\n} from \"@x12i/ai-profiles\";\nimport type { AiModelRecord } from \"../models/types.js\";\nimport {\n  DEFAULT_DIRECT_CATALOG_URL,\n  DEFAULT_MODELS_CATALOG_URL,\n  DEFAULT_OPENROUTER_CATALOG_URL,\n} from \"./catalogSources.js\";\nimport { modelsFromAiProfilesCatalog } from \"./normalizeAiProfilesCatalog.js\";\n\nexport {\n  DEFAULT_DIRECT_CATALOG_URL,\n  DEFAULT_MODELS_CATALOG_URL,\n  DEFAULT_OPENROUTER_CATALOG_URL,\n};\n\nexport type LoadCatalogOptions = {\n  /**\n   * Catalog source mode for `@x12i/ai-profiles` (`auto` | `remote` | `bundled`).\n   * When `bundledOnly` is true, forced to `bundled`.\n   */\n  source?: RegistrySourceMode;\n  /** When true, skip remote and use ai-profiles bundled catalog only. */\n  bundledOnly?: boolean;\n  /** @deprecated Ignored — catalog SoT is `@x12i/ai-profiles`. */\n  directCatalogUrl?: string;\n  /** @deprecated Ignored — catalog SoT is `@x12i/ai-profiles`. */\n  openRouterCatalogUrl?: string;\n  /** @deprecated Ignored — fetch timeout is owned by ai-profiles. */\n  fetchTimeoutMs?: number;\n};\n\nexport type LoadedCatalogs = {\n  direct: Map<string, AiModelRecord>;\n  openrouter: Map<string, AiModelRecord>;\n  meta: {\n    directSource: \"remote\" | \"bundled\";\n    openRouterSource: \"remote\" | \"bundled\";\n    directUrl: string;\n    openRouterUrl: string;\n    directCount: number;\n    openRouterCount: number;\n  };\n};\n\nfunction resolveSource(options: LoadCatalogOptions): RegistrySourceMode {\n  if (options.bundledOnly) return \"bundled\";\n  return options.source ?? \"auto\";\n}\n\n/** Load unified models catalog from `@x12i/ai-profiles` (direct + OpenRouter slices). */\nexport async function loadCatalogSources(\n  options: LoadCatalogOptions = {},\n): Promise<LoadedCatalogs> {\n  const source = resolveSource(options);\n  const { catalog, source: loadedFrom } = await loadModelsCatalogWithWarnings({\n    source,\n    ...(source !== \"auto\" ? { refresh: true as const } : {}),\n  });\n\n  const { direct, openrouter } = modelsFromAiProfilesCatalog(catalog);\n  const url = MODELS_CATALOG_REMOTE_URL;\n\n  return {\n    direct,\n    openrouter,\n    meta: {\n      directSource: loadedFrom,\n      openRouterSource: loadedFrom,\n      directUrl: url,\n      openRouterUrl: url,\n      directCount: direct.size,\n      openRouterCount: openrouter.size,\n    },\n  };\n}\n\n/** Load ai-profiles bundled catalog only (CLI / tests). */\nexport async function readBundledCatalogFiles(): Promise<LoadedCatalogs> {\n  return loadCatalogSources({ bundledOnly: true });\n}\n\n/** Convenience: load catalog JSON via ai-profiles without mapping to AiModelRecord. */\nexport async function loadAiProfilesModelsCatalog(\n  options: LoadCatalogOptions = {},\n) {\n  return loadModelsCatalog({\n    source: resolveSource(options),\n  });\n}\n","import { resolveCatalogCacheTtlMs } from \"../cache/modelCache.js\";\nimport { loadCatalogSources, type LoadCatalogOptions, type LoadedCatalogs } from \"./loadCatalogSources.js\";\n\ntype CacheSlot = {\n  loadedAt: number;\n  data: LoadedCatalogs;\n};\n\nconst slots = new Map<string, CacheSlot>();\nconst inflight = new Map<string, Promise<LoadedCatalogs>>();\n\nfunction slotKey(options: LoadCatalogOptions, cacheKey: string): string {\n  return [\n    cacheKey,\n    options.bundledOnly ? \"bundled\" : (options.source ?? \"auto\"),\n  ].join(\"\\0\");\n}\n\nexport function invalidateCatalogLoadCache(cacheKey = \"default\"): void {\n  for (const key of [...slots.keys()]) {\n    if (key.startsWith(`${cacheKey}\\0`)) slots.delete(key);\n  }\n  for (const key of [...inflight.keys()]) {\n    if (key.startsWith(`${cacheKey}\\0`)) inflight.delete(key);\n  }\n}\n\nexport type LoadCatalogCachedOptions = {\n  cacheKey?: string;\n  ttlMs?: number;\n  /** Bypass cache and fetch again (e.g. `catalog refresh`). */\n  forceRefresh?: boolean;\n};\n\n/**\n * Load catalogs with a process-wide in-memory cache.\n * Loads run at most once per TTL window (default 24h); concurrent callers share one in-flight request.\n */\nexport async function loadCatalogSourcesCached(\n  options: LoadCatalogOptions = {},\n  cacheOptions: LoadCatalogCachedOptions = {},\n): Promise<LoadedCatalogs> {\n  const ttlMs = resolveCatalogCacheTtlMs(cacheOptions.ttlMs);\n  const key = slotKey(options, cacheOptions.cacheKey ?? \"default\");\n\n  if (!cacheOptions.forceRefresh) {\n    const slot = slots.get(key);\n    if (slot && Date.now() - slot.loadedAt < ttlMs) {\n      return slot.data;\n    }\n  } else {\n    slots.delete(key);\n    inflight.delete(key);\n  }\n\n  const pending = inflight.get(key);\n  if (pending) return pending;\n\n  const promise = loadCatalogSources(options)\n    .then((data) => {\n      slots.set(key, { loadedAt: Date.now(), data });\n      inflight.delete(key);\n      return data;\n    })\n    .catch((err) => {\n      inflight.delete(key);\n      throw err;\n    });\n\n  inflight.set(key, promise);\n  return promise;\n}\n","import { resolveCatalogCacheTtlMs, writeCachedModels } from \"../cache/modelCache.js\";\nimport {\n  matchModelInAiProfiles,\n  type AiProfilesModelMatch,\n  type MatchModelInAiProfilesOptions,\n} from \"../cost/aiProfilesMatch.js\";\nimport { TEXT_CATALOG_LANE } from \"../profiles/catalogLane.js\";\nimport type { AiModelRecord } from \"../models/types.js\";\nimport { ModelNameResolver } from \"../sync/modelNameResolver/ModelNameResolver.js\";\nimport { normalizeProvider, normalizeString } from \"../sync/modelNameResolver/normalize.js\";\nimport {\n  isEffectiveOpenRouterTransport,\n  loadOpenRouterRoutingEnv,\n} from \"../sync/openRouterRoutingEnv.js\";\nimport type {\n  ModelResolutionInput,\n  ModelResolutionResult,\n  ModelResolutionSuccess,\n  ModelResolverOptions,\n  ResolutionStrategy,\n} from \"../sync/modelNameResolver/types.js\";\nimport { isKnownProfileInput } from \"@x12i/ai-profiles\";\nimport {\n  invalidateCatalogLoadCache,\n  loadCatalogSourcesCached,\n} from \"./catalogLoadCache.js\";\nimport type { LoadCatalogOptions } from \"./loadCatalogSources.js\";\n\nexport type AiModelsCatalogClientOptions = LoadCatalogOptions & {\n  cacheTtlMs?: number;\n  /** Cache scope key (default: `default`). */\n  cacheKey?: string;\n  resolverOptions?: ModelResolverOptions;\n};\n\nfunction isOpenRouterProvider(provider?: string): boolean {\n  return normalizeProvider(provider) === \"openrouter\";\n}\n\nfunction profileResolutionStrategy(\n  matchedVia: AiProfilesModelMatch[\"matchedVia\"],\n): ResolutionStrategy {\n  return matchedVia === \"profile\" ? \"ai-profiles-profile\" : \"ai-profiles-model-id\";\n}\n\nexport class AiModelsCatalogClient {\n  private readonly cacheTtlMs: number;\n  private readonly cacheKey: string;\n  private readonly loadOptions: LoadCatalogOptions;\n  private readonly resolverOptions?: ModelResolverOptions;\n\n  private directModels: Map<string, AiModelRecord> | null = null;\n  private openRouterModels: Map<string, AiModelRecord> | null = null;\n  private loadedAt = 0;\n  private loadPromise: Promise<void> | null = null;\n\n  constructor(options: AiModelsCatalogClientOptions = {}) {\n    this.cacheTtlMs = resolveCatalogCacheTtlMs(options.cacheTtlMs);\n    this.cacheKey = options.cacheKey ?? \"default\";\n    this.loadOptions = {\n      source: options.source,\n      bundledOnly: options.bundledOnly,\n      directCatalogUrl: options.directCatalogUrl,\n      openRouterCatalogUrl: options.openRouterCatalogUrl,\n      fetchTimeoutMs: options.fetchTimeoutMs,\n    };\n    this.resolverOptions = options.resolverOptions;\n  }\n\n  private isInstanceCacheValid(): boolean {\n    return (\n      this.directModels !== null &&\n      this.openRouterModels !== null &&\n      Date.now() - this.loadedAt < this.cacheTtlMs\n    );\n  }\n\n  private applyLoaded(\n    direct: Map<string, AiModelRecord>,\n    openrouter: Map<string, AiModelRecord>,\n  ): void {\n    this.directModels = direct;\n    this.openRouterModels = openrouter;\n    this.loadedAt = Date.now();\n    writeCachedModels(`${this.cacheKey}:direct`, direct, this.cacheTtlMs);\n    writeCachedModels(`${this.cacheKey}:openrouter`, openrouter, this.cacheTtlMs);\n  }\n\n  private async ensureLoaded(forceRefresh = false): Promise<void> {\n    if (!forceRefresh && this.isInstanceCacheValid()) return;\n\n    if (this.loadPromise) return this.loadPromise;\n\n    this.loadPromise = (async () => {\n      const loaded = await loadCatalogSourcesCached(this.loadOptions, {\n        cacheKey: this.cacheKey,\n        ttlMs: this.cacheTtlMs,\n        forceRefresh,\n      });\n      this.applyLoaded(loaded.direct, loaded.openrouter);\n    })().finally(() => {\n      this.loadPromise = null;\n    });\n\n    await this.loadPromise;\n  }\n\n  mergedModels(): Map<string, AiModelRecord> {\n    const map = new Map<string, AiModelRecord>();\n    for (const r of this.directModels?.values() ?? []) {\n      map.set(r.modelId, r);\n    }\n    for (const r of this.openRouterModels?.values() ?? []) {\n      map.set(r.modelId, r);\n    }\n    return map;\n  }\n\n  private catalogForProvider(provider?: string): Map<string, AiModelRecord> {\n    if (isOpenRouterProvider(provider)) {\n      return this.openRouterModels ?? new Map();\n    }\n    return this.directModels ?? new Map();\n  }\n\n  private resolver(\n    models: Map<string, AiModelRecord>,\n    options?: ModelResolverOptions,\n  ): ModelNameResolver {\n    return new ModelNameResolver(models, { ...this.resolverOptions, ...options });\n  }\n\n  async getAllModels(): Promise<Map<string, AiModelRecord>> {\n    await this.ensureLoaded();\n    return this.mergedModels();\n  }\n\n  async getDirectModels(): Promise<Map<string, AiModelRecord>> {\n    await this.ensureLoaded();\n    return new Map(this.directModels);\n  }\n\n  async getOpenRouterModels(): Promise<Map<string, AiModelRecord>> {\n    await this.ensureLoaded();\n    return new Map(this.openRouterModels);\n  }\n\n  async resolveModel(\n    input: ModelResolutionInput,\n    options?: ModelResolverOptions,\n  ): Promise<ModelResolutionResult> {\n    await this.ensureLoaded();\n\n    const mergedOptions = { ...this.resolverOptions, ...options };\n\n    // Local provider passthrough before profile/catalog (handled inside ModelNameResolver).\n    const provider = normalizeProvider(input.provider);\n    if (provider) {\n      const localPass = this.resolver(new Map(), mergedOptions).resolve(input);\n      if (localPass.found && localPass.resolvedVia.includes(\"local-provider-passthrough\")) {\n        return localPass;\n      }\n    }\n\n    // Profile-first: known profile input before any catalog fuzzy match.\n    if (isKnownProfileInput(input.model.trim())) {\n      const viaProfile = await this.resolveViaProfile(input, mergedOptions);\n      if (viaProfile) return viaProfile;\n    }\n\n    let result = this.resolveFromCatalogMaps(input, mergedOptions);\n\n    if (!result.found) {\n      const viaProfile = await this.resolveViaProfile(input, mergedOptions);\n      if (viaProfile) return viaProfile;\n      return result;\n    }\n\n    return this.alignForOpenRouterTransport(result, input);\n  }\n\n  private resolveFromCatalogMaps(\n    input: ModelResolutionInput,\n    options?: ModelResolverOptions,\n  ): ModelResolutionResult {\n    const primary = this.catalogForProvider(input.provider);\n    const secondary = isOpenRouterProvider(input.provider)\n      ? (this.directModels ?? new Map())\n      : (this.openRouterModels ?? new Map());\n\n    let result = this.resolver(primary, options).resolve(input);\n    if (result.found && result.record) {\n      return this.alignForOpenRouterTransport(result, input);\n    }\n\n    if (secondary.size > 0) {\n      const fallback = this.resolver(secondary, options).resolve(input);\n      if (fallback.found && fallback.record) {\n        return this.alignForOpenRouterTransport(fallback, input);\n      }\n      if (!result.found && fallback.found) result = fallback;\n    }\n\n    if (!result.found) {\n      result = this.resolver(this.mergedModels(), options).resolve(input);\n    }\n\n    if (result.found) {\n      return this.alignForOpenRouterTransport(result, input);\n    }\n\n    return result;\n  }\n\n  private alignForOpenRouterTransport(\n    result: ModelResolutionSuccess,\n    input: ModelResolutionInput,\n  ): ModelResolutionSuccess {\n    if (!result.routedViaOpenRouter) return result;\n\n    let { modelId, record } = result;\n    if (!modelId.includes(\"/\")) {\n      const vendor =\n        record?.providerId ??\n        normalizeProvider(input.provider) ??\n        undefined;\n      if (vendor && vendor !== \"openrouter\") {\n        modelId = `${vendor}/${modelId}`;\n      }\n    }\n\n    const orRecord = this.openRouterModels?.get(modelId);\n    if (orRecord) {\n      record = orRecord;\n    }\n\n    if (modelId === result.modelId && record === result.record) return result;\n    return { ...result, modelId, record };\n  }\n\n  private async resolveViaProfile(\n    input: ModelResolutionInput,\n    options?: ModelResolverOptions,\n  ): Promise<ModelResolutionSuccess | null> {\n    const profileOpts: MatchModelInAiProfilesOptions = {\n      catalogLane: options?.catalogLane ?? TEXT_CATALOG_LANE,\n    };\n    const profileMatch = await matchModelInAiProfiles(\n      input.model,\n      input.provider,\n      profileOpts,\n    );\n    if (!profileMatch) return null;\n\n    const strategy = profileResolutionStrategy(profileMatch.matchedVia);\n    const attempts: ModelResolutionInput[] = [];\n    const seen = new Set<string>();\n    const add = (model: string, provider?: string) => {\n      const key = `${provider ?? \"\"}\\0${normalizeString(model)}`;\n      if (seen.has(key)) return;\n      seen.add(key);\n      attempts.push({ model, provider });\n    };\n\n    add(profileMatch.canonicalModelId, input.provider ?? profileMatch.provider);\n    add(profileMatch.canonicalModelId, profileMatch.provider);\n    if (!isOpenRouterProvider(input.provider)) {\n      add(profileMatch.canonicalModelId, \"openrouter\");\n    }\n\n    let catalogResult: ModelResolutionResult | null = null;\n    for (const attempt of attempts) {\n      const result = this.resolveFromCatalogMaps(attempt, options);\n      catalogResult = result;\n      if (result.found && result.record) break;\n    }\n\n    const normalisedInput = normalizeString(input.model);\n    if (!catalogResult?.found) {\n      const routingEnv = options?.routingEnv ?? loadOpenRouterRoutingEnv();\n      const routedViaOpenRouter = isEffectiveOpenRouterTransport(routingEnv, {\n        provider: input.provider ?? profileMatch.provider,\n        modelId: profileMatch.canonicalModelId,\n        routeViaOpenRouter: options?.routeViaOpenRouter,\n      });\n\n      const aligned = this.alignForOpenRouterTransport(\n        {\n          found: true,\n          modelId: profileMatch.canonicalModelId,\n          record: null,\n          routedViaOpenRouter,\n          confidence: 0.85,\n          resolvedVia: [strategy],\n          resolvedReason: `Resolved profile \"${input.model}\" → ${profileMatch.canonicalModelId} via ai-profiles (${profileMatch.matchedVia})`,\n          normalisedInput,\n          profile: profileMatch.profile,\n          choice: profileMatch.choice,\n        },\n        { model: profileMatch.canonicalModelId, provider: profileMatch.provider },\n      );\n\n      return {\n        ...aligned,\n        resolvedVia: [strategy],\n        profile: profileMatch.profile,\n        choice: profileMatch.choice,\n      };\n    }\n\n    const viaProfile = catalogResult as ModelResolutionSuccess;\n    return {\n      ...viaProfile,\n      resolvedVia: [strategy, ...viaProfile.resolvedVia],\n      resolvedReason: `Resolved profile \"${input.model}\" → ${viaProfile.modelId} via ai-profiles (${profileMatch.matchedVia})`,\n      normalisedInput,\n      profile: profileMatch.profile,\n      choice: profileMatch.choice,\n    };\n  }\n\n  async getModel(\n    modelId: string,\n    provider?: string,\n    options?: ModelResolverOptions,\n  ): Promise<AiModelRecord | null> {\n    const result = await this.resolveModel({ model: modelId, provider }, options);\n    return result.found ? result.record : null;\n  }\n\n  /** Clear caches and fetch catalogs again immediately. */\n  async refresh(): Promise<void> {\n    invalidateCatalogLoadCache(this.cacheKey);\n    this.directModels = null;\n    this.openRouterModels = null;\n    this.loadedAt = 0;\n    this.loadPromise = null;\n    await this.ensureLoaded(true);\n  }\n}\n"]}