refactor(catalog): centralize provider cache IDs

This commit is contained in:
Roy
2026-07-27 01:25:13 +00:00
parent ef21aae4f6
commit 54ae233ebc
6 changed files with 92 additions and 43 deletions
@@ -0,0 +1,47 @@
export interface ModelCacheProviderIdOptions {
apiKey?: string;
baseUrl?: string;
}
export function getDefaultModelDiscoveryBaseUrl(providerId: string): string | undefined {
switch (providerId) {
case "litellm":
return Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
case "opencode-go":
return "https://opencode.ai/zen/go/v1";
case "opencode-zen":
return "https://opencode.ai/zen/v1";
case "vllm":
return "http://127.0.0.1:8000/v1";
default:
return undefined;
}
}
/** Resolve the cache namespace used by a provider's model-manager options without constructing those options. */
export function resolveModelCacheProviderId(providerId: string, options: ModelCacheProviderIdOptions = {}): string {
switch (providerId) {
case "cursor":
return "cursor:max-mode-v2";
case "litellm": {
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
return `litellm:rich-v5:${Bun.hash(baseUrl).toString(36)}`;
}
case "opencode-go":
case "opencode-zen": {
const configuredBaseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
const trimmedBaseUrl = configuredBaseUrl.endsWith("/") ? configuredBaseUrl.slice(0, -1) : configuredBaseUrl;
const discoveryBaseUrl = trimmedBaseUrl.endsWith("/v1") ? trimmedBaseUrl : `${trimmedBaseUrl}/v1`;
const scope = `${options.apiKey ?? ""}\u0000${discoveryBaseUrl}`;
return `${providerId}:models-v1:${Bun.hash(scope).toString(36)}`;
}
case "openrouter":
return "openrouter:pseudo-api";
case "vllm": {
const baseUrl = options.baseUrl ?? getDefaultModelDiscoveryBaseUrl(providerId)!;
return `vllm:${Bun.hash(baseUrl).toString(36)}`;
}
default:
return providerId;
}
}
@@ -1,3 +1,4 @@
export * from "./cache-provider-id";
export * from "./descriptor-types";
export * from "./descriptors";
export * from "./google";
@@ -27,6 +27,7 @@ import {
parseGitHubCopilotApiKey,
} from "../wire/github-copilot";
import { createBundledReferenceMap, createReferenceResolver, toModelSpec } from "./bundled-references";
import { getDefaultModelDiscoveryBaseUrl, resolveModelCacheProviderId } from "./cache-provider-id";
const MODELS_DEV_URL = "https://models.dev/api.json";
@@ -2065,28 +2066,19 @@ function openCodeBaseUrlForApi(api: Api, basePath: string): string {
return api === "anthropic-messages" ? basePath : `${basePath}/v1`;
}
function openCodeModelCacheProviderId(
providerId: "opencode-go" | "opencode-zen",
apiKey: string | undefined,
discoveryBaseUrl: string,
): string {
// OpenCode catalogs are entitlement-scoped; isolate authoritative rows by credential and endpoint.
const scope = `${apiKey ?? ""}\u0000${discoveryBaseUrl}`;
return `${providerId}:models-v1:${Bun.hash(scope).toString(36)}`;
}
function openCodeModelManagerOptions(
providerId: "opencode-go" | "opencode-zen",
defaultBasePath: string,
config?: OpenCodeModelManagerConfig,
): ModelManagerOptions<Api> {
const apiKey = config?.apiKey;
const defaultBaseUrl = getDefaultModelDiscoveryBaseUrl(providerId)!;
const defaultBasePath = defaultBaseUrl.endsWith("/v1") ? defaultBaseUrl.slice(0, -3) : defaultBaseUrl;
const basePath = normalizeOpenCodeBasePath(config?.baseUrl, defaultBasePath);
const discoveryBaseUrl = openCodeBaseUrlForApi("openai-completions", basePath);
const references = createBundledReferenceMap<Api>(providerId);
return {
providerId,
cacheProviderId: openCodeModelCacheProviderId(providerId, apiKey, discoveryBaseUrl),
cacheProviderId: resolveModelCacheProviderId(providerId, { apiKey, baseUrl: discoveryBaseUrl }),
dynamicModelsAuthoritative: true,
...(apiKey && {
fetchDynamicModels: () =>
@@ -2120,11 +2112,11 @@ function openCodeModelManagerOptions(
}
export function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen", config);
return openCodeModelManagerOptions("opencode-zen", config);
}
export function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go", config);
return openCodeModelManagerOptions("opencode-go", config);
}
// ---------------------------------------------------------------------------
@@ -2211,7 +2203,7 @@ export function openrouterModelManagerOptions(
// Older builds cached OpenRouter discovery rows as `api: "openai-completions"`.
// Namespace the refreshed pseudo-API cache separately so those rows cannot
// override bundled `api: "openrouter"` models during online-if-uncached startup.
cacheProviderId: "openrouter:pseudo-api",
cacheProviderId: resolveModelCacheProviderId("openrouter"),
fetchDynamicModels: () =>
fetchOpenAICompatibleModels({
api: "openrouter",
@@ -3970,7 +3962,7 @@ export function litellmModelManagerOptions(
config?: LiteLLMModelManagerConfig,
): ModelManagerOptions<"openai-completions"> {
const apiKey = config?.apiKey;
const baseUrl = config?.baseUrl ?? Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1";
const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("litellm")!;
return {
providerId: "litellm",
// rich-v5 invalidates rows cached before rich metadata pricing was mapped.
@@ -3979,7 +3971,7 @@ export function litellmModelManagerOptions(
// and filtered placeholder-only `all-team-models` rows. Bump the version
// whenever the mappers below change, or warm authoritative caches keep
// serving pre-change rows for the full TTL.
cacheProviderId: `litellm:rich-v5:${Bun.hash(baseUrl).toString(36)}`,
cacheProviderId: resolveModelCacheProviderId("litellm", { baseUrl }),
// litellm is a local-only proxy and is never bundled in models.json (that
// would leak the machine's localhost catalog). Prefer the proxy's richer
// management metadata, then enrich ids against models.dev with the bundled
@@ -4026,11 +4018,11 @@ export interface VllmModelManagerConfig {
export function vllmModelManagerOptions(config?: VllmModelManagerConfig): ModelManagerOptions<"openai-completions"> {
const apiKey = config?.apiKey;
const baseUrl = config?.baseUrl ?? "http://127.0.0.1:8000/v1";
const baseUrl = config?.baseUrl ?? getDefaultModelDiscoveryBaseUrl("vllm")!;
const references = createBundledReferenceMap<"openai-completions">("vllm" as Parameters<typeof getBundledModels>[0]);
return {
providerId: "vllm",
cacheProviderId: `vllm:${Bun.hash(baseUrl).toString(36)}`,
cacheProviderId: resolveModelCacheProviderId("vllm", { baseUrl }),
fetchDynamicModels: () =>
fetchOpenAICompatibleModels({
api: "openai-completions",
@@ -4,6 +4,7 @@ import type { DevinModelDiscoveryOptions } from "../discovery/devin";
import { buildGitLabDuoWorkflowFallbackModel, fetchGitLabDuoWorkflowModels } from "../discovery/gitlab-duo-workflow";
import type { ModelManagerOptions } from "../model-manager";
import type { FetchImpl, ModelSpec } from "../types";
import { resolveModelCacheProviderId } from "./cache-provider-id";
// ---------------------------------------------------------------------------
// OpenAI Codex
@@ -94,13 +95,11 @@ export interface CursorModelManagerConfig {
clientVersion?: string;
}
const CURSOR_CACHE_PROVIDER_ID = "cursor:max-mode-v2";
export function cursorModelManagerOptions(config: CursorModelManagerConfig = {}): ModelManagerOptions<"cursor-agent"> {
const { apiKey, baseUrl, clientVersion } = config;
return {
providerId: "cursor",
cacheProviderId: CURSOR_CACHE_PROVIDER_ID,
cacheProviderId: resolveModelCacheProviderId("cursor"),
...(apiKey
? {
fetchDynamicModels: async () => {
@@ -0,0 +1,25 @@
import { expect, test } from "bun:test";
import { PROVIDER_DESCRIPTORS, resolveModelCacheProviderId } from "@oh-my-pi/pi-catalog/provider-models";
test("lightweight cache resolver matches every descriptor default", () => {
for (const descriptor of PROVIDER_DESCRIPTORS) {
const options = descriptor.createModelManagerOptions({});
expect(resolveModelCacheProviderId(descriptor.providerId)).toBe(options.cacheProviderId ?? descriptor.providerId);
}
});
test("lightweight cache resolver matches scoped descriptor inputs", () => {
const cases = [
{ providerId: "litellm", baseUrl: "http://litellm.example:4100/v1" },
{ providerId: "opencode-go", baseUrl: "https://opencode.example/go" },
{ providerId: "opencode-zen", baseUrl: "https://opencode.example/zen/v1/" },
{ providerId: "vllm", baseUrl: "http://vllm.example:8000/v1" },
] as const;
for (const { providerId, baseUrl } of cases) {
const descriptor = PROVIDER_DESCRIPTORS.find(candidate => candidate.providerId === providerId);
if (!descriptor) throw new Error(`Missing descriptor for ${providerId}`);
const config = { apiKey: "cache-test-key", baseUrl };
const options = descriptor.createModelManagerOptions(config);
expect(resolveModelCacheProviderId(providerId, config)).toBe(options.cacheProviderId ?? providerId);
}
});
@@ -26,6 +26,7 @@ import {
type OpenAICodexAccount,
openaiCodexModelManagerOptions,
PROVIDER_DESCRIPTORS,
resolveModelCacheProviderId,
} from "@oh-my-pi/pi-catalog/provider-models";
import {
collapseBuiltModelVariants,
@@ -44,18 +45,6 @@ const STARTUP_MODEL_CACHE_PROVIDER_IDS: readonly string[] = [
...SPECIAL_MODEL_MANAGER_PROVIDER_IDS,
];
// Cache namespaces whose descriptor defaults intentionally differ from the
// provider id. Keep startup cache reads lightweight: constructing all provider
// manager options also constructs their bundled reference maps.
const DEFAULT_STARTUP_MODEL_CACHE_PROVIDER_IDS: Readonly<Record<string, string>> = {
cursor: "cursor:max-mode-v2",
litellm: `litellm:rich-v5:${Bun.hash(Bun.env.LITELLM_BASE_URL ?? "http://localhost:4000/v1").toString(36)}`,
"opencode-go": `opencode-go:models-v1:${Bun.hash("\u0000https://opencode.ai/zen/go/v1").toString(36)}`,
"opencode-zen": `opencode-zen:models-v1:${Bun.hash("\u0000https://opencode.ai/zen/v1").toString(36)}`,
openrouter: "openrouter:pseudo-api",
vllm: `vllm:${Bun.hash("http://127.0.0.1:8000/v1").toString(36)}`,
};
// Sentinels for local-only OAuth tokens — declared inline to avoid loading
// provider modules at startup. Must match packages/ai/src/registry/llama-cpp.ts,
// packages/ai/src/registry/lm-studio.ts, and packages/ai/src/registry/vllm.ts.
@@ -1231,15 +1220,11 @@ export class ModelRegistry {
}
#resolveStartupModelCacheProviderId(providerId: string): string {
const explicitBaseUrl =
this.#runtimeProviderOverrides.get(providerId)?.baseUrl ?? this.#providerOverrides.get(providerId)?.baseUrl;
if (explicitBaseUrl === undefined && !this.#hasFullSnapshot) {
return DEFAULT_STARTUP_MODEL_CACHE_PROVIDER_IDS[providerId] ?? providerId;
}
const descriptor = PROVIDER_DESCRIPTORS.find(candidate => candidate.providerId === providerId);
if (!descriptor) return providerId;
const baseUrl = explicitBaseUrl ?? this.getProviderBaseUrl(providerId);
return descriptor.createModelManagerOptions({ baseUrl, fetch: this.#fetch }).cacheProviderId ?? providerId;
const baseUrl =
this.#runtimeProviderOverrides.get(providerId)?.baseUrl ??
this.#providerOverrides.get(providerId)?.baseUrl ??
(this.#hasFullSnapshot ? this.getProviderBaseUrl(providerId) : undefined);
return resolveModelCacheProviderId(providerId, { baseUrl });
}
#loadCachedStandardProviderModels(): { models: Model<Api>[]; authoritativeFreshProviders: Set<string> } {