refactor(catalog): centralize provider cache IDs
This commit is contained in:
@@ -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> } {
|
||||
|
||||
Reference in New Issue
Block a user