mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
refactor(models): consolidate prepared catalog construction (#130654)
* refactor(models): consolidate prepared catalog construction * test(models): avoid shadowing registry fixture identifiers
This commit is contained in:
committed by
GitHub
parent
b7d9be0209
commit
8dac217ce9
@@ -1,6 +1,10 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
import { getModelProviderRequestRouteFacts } from "../provider-request-config.js";
|
||||
import { getModelProviderLocalService } from "../provider-local-service.js";
|
||||
import {
|
||||
getModelProviderRequestRouteFacts,
|
||||
getModelProviderRequestTransport,
|
||||
} from "../provider-request-config.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadPluginManifestRegistryCore: vi.fn(),
|
||||
@@ -196,18 +200,42 @@ describe("prepared bundled provider static catalogs", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("projects prepared rows without rerunning hooks", async () => {
|
||||
it("projects heterogeneous prepared rows without rerunning hooks or resolving empty providers", async () => {
|
||||
mocks.resolveRuntimePluginDiscoveryProviders.mockResolvedValue([provider]);
|
||||
mocks.normalizePluginDiscoveryResult.mockReturnValue({
|
||||
google: {
|
||||
api: "fixture-api",
|
||||
baseUrl: "https://fixture.example/v1",
|
||||
authHeader: false,
|
||||
maxTokens: 4096,
|
||||
request: { headers: { "X-Catalog": "prepared" } },
|
||||
localService: { command: "fixture-service" },
|
||||
models: [
|
||||
{
|
||||
id: "gemini-3.1-pro-preview",
|
||||
name: "Gemini Pro",
|
||||
contextWindow: 1_048_576,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0.5 },
|
||||
maxTokens: 0,
|
||||
},
|
||||
{
|
||||
id: "fallback-model",
|
||||
name: "",
|
||||
baseUrl: "",
|
||||
input: [],
|
||||
contextWindow: 0,
|
||||
contextTokens: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
empty: {
|
||||
request: {
|
||||
headers: { "X-Unused": { source: "env", provider: "default", id: "UNUSED_HEADER" } },
|
||||
},
|
||||
models: [],
|
||||
},
|
||||
});
|
||||
|
||||
const metadataSnapshot = createMetadataSnapshot(["google"]);
|
||||
@@ -223,12 +251,37 @@ describe("prepared bundled provider static catalogs", () => {
|
||||
expect.objectContaining({
|
||||
id: "gemini-3.1-pro-preview",
|
||||
provider: "google",
|
||||
api: "fixture-api",
|
||||
baseUrl: "https://fixture.example/v1",
|
||||
authHeader: false,
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: { input: 0.5 },
|
||||
contextWindow: 1_048_576,
|
||||
maxTokens: 0,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "fallback-model",
|
||||
name: "fallback-model",
|
||||
provider: "google",
|
||||
api: "fixture-api",
|
||||
baseUrl: "",
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 0,
|
||||
contextTokens: 0,
|
||||
maxTokens: 4096,
|
||||
}),
|
||||
]);
|
||||
expect(getModelProviderRequestRouteFacts(models[0]!)?.providerMetadataOwners).toBe(
|
||||
metadataSnapshot.owners,
|
||||
);
|
||||
for (const model of models) {
|
||||
expect(getModelProviderRequestRouteFacts(model)?.providerMetadataOwners).toBe(
|
||||
metadataSnapshot.owners,
|
||||
);
|
||||
expect(getModelProviderRequestTransport(model)).toEqual({
|
||||
headers: { "X-Catalog": "prepared" },
|
||||
});
|
||||
expect(getModelProviderLocalService(model)).toEqual({ command: "fixture-service" });
|
||||
}
|
||||
expect(mocks.resolveRuntimePluginDiscoveryProviders).toHaveBeenCalledOnce();
|
||||
expect(mocks.runProviderStaticCatalog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
resolveOwningPluginIdsForProviderRef,
|
||||
} from "../../plugins/providers.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js";
|
||||
import { buildInlineProviderModels } from "./model.inline-provider.js";
|
||||
import { buildInlineProviderModels, type InlineModelEntry } from "./model.inline-provider.js";
|
||||
import {
|
||||
createStaticModelIdMatcher,
|
||||
staticModelIdMatches,
|
||||
@@ -97,38 +97,22 @@ function modelFromStaticCatalogRow(row: NormalizedModelCatalogRow): ProviderRunt
|
||||
};
|
||||
}
|
||||
|
||||
function modelFromProviderStaticCatalog(params: {
|
||||
provider: string;
|
||||
providerConfig: ModelProviderConfig;
|
||||
model: ModelProviderConfig["models"][number];
|
||||
providerMetadataOwners?: PluginMetadataSnapshot["owners"];
|
||||
}): ProviderRuntimeModel {
|
||||
const [model] = buildInlineProviderModels(
|
||||
{
|
||||
[params.provider]: { ...params.providerConfig, models: [params.model] },
|
||||
},
|
||||
{ providerMetadataOwners: params.providerMetadataOwners },
|
||||
);
|
||||
function completeProviderStaticCatalogModel(
|
||||
model: InlineModelEntry,
|
||||
providerConfig: ModelProviderConfig,
|
||||
): ProviderRuntimeModel {
|
||||
return {
|
||||
...model,
|
||||
id: model?.id ?? params.model.id,
|
||||
name: model?.name || params.model.name || params.model.id,
|
||||
provider: params.provider,
|
||||
api: model?.api ?? params.model.api ?? params.providerConfig.api ?? "openai-responses",
|
||||
baseUrl: model?.baseUrl ?? params.model.baseUrl ?? params.providerConfig.baseUrl ?? "",
|
||||
reasoning: model?.reasoning ?? params.model.reasoning ?? false,
|
||||
input: normalizeStaticCatalogInput(model?.input ?? params.model.input),
|
||||
cost: model?.cost ?? normalizeStaticCatalogCost(params.model.cost),
|
||||
contextWindow: model?.contextWindow ?? params.model.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
|
||||
contextTokens: model?.contextTokens ?? params.model.contextTokens,
|
||||
maxTokens:
|
||||
model?.maxTokens ??
|
||||
params.model.maxTokens ??
|
||||
params.providerConfig.maxTokens ??
|
||||
DEFAULT_CONTEXT_TOKENS,
|
||||
...(params.providerConfig.authHeader !== undefined
|
||||
? { authHeader: params.providerConfig.authHeader }
|
||||
: {}),
|
||||
name: model.name || model.id,
|
||||
api: model.api ?? providerConfig.api ?? "openai-responses",
|
||||
baseUrl: model.baseUrl ?? "",
|
||||
reasoning: model.reasoning ?? false,
|
||||
input: normalizeStaticCatalogInput(model.input),
|
||||
cost: model.cost ?? normalizeStaticCatalogCost(undefined),
|
||||
contextWindow: model.contextWindow ?? DEFAULT_CONTEXT_TOKENS,
|
||||
contextTokens: model.contextTokens,
|
||||
maxTokens: model.maxTokens ?? DEFAULT_CONTEXT_TOKENS,
|
||||
...(providerConfig.authHeader !== undefined ? { authHeader: providerConfig.authHeader } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -485,21 +469,20 @@ async function loadBundledProviderStaticCatalogModels(params: {
|
||||
});
|
||||
for (const [providerIdRaw, providerConfig] of Object.entries(normalized)) {
|
||||
const provider = normalizeProviderId(providerIdRaw);
|
||||
if (!provider || !Array.isArray(providerConfig.models)) {
|
||||
// Empty catalogs never resolve request secrets or transport settings.
|
||||
if (
|
||||
!provider ||
|
||||
!Array.isArray(providerConfig.models) ||
|
||||
providerConfig.models.length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const models = modelsByProvider.get(provider) ?? [];
|
||||
models.push(
|
||||
...providerConfig.models.map((model) =>
|
||||
modelFromProviderStaticCatalog({
|
||||
provider,
|
||||
providerConfig,
|
||||
model,
|
||||
...(params.providerMetadataOwners
|
||||
? { providerMetadataOwners: params.providerMetadataOwners }
|
||||
: {}),
|
||||
}),
|
||||
),
|
||||
...buildInlineProviderModels(
|
||||
{ [provider]: providerConfig },
|
||||
{ providerMetadataOwners: params.providerMetadataOwners },
|
||||
).map((model) => completeProviderStaticCatalogModel(model, providerConfig)),
|
||||
);
|
||||
modelsByProvider.set(provider, models);
|
||||
}
|
||||
@@ -639,17 +622,6 @@ function createScopedBundledProviderStaticCatalogModelResolver(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares bundled provider static-catalog lookup.
|
||||
* Each provider hook runs at most once for the resolver lifetime.
|
||||
*/
|
||||
function createBundledProviderStaticCatalogModelResolver(
|
||||
params: BundledProviderStaticCatalogResolverParams = {},
|
||||
): (lookup: BundledStaticCatalogLookup) => Promise<ProviderRuntimeModel | undefined> {
|
||||
const resolveModel = createScopedBundledProviderStaticCatalogModelResolver(params);
|
||||
return async (lookup) => await resolveModel(lookup);
|
||||
}
|
||||
|
||||
function resolveOwnedNestedProviderLookup(params: {
|
||||
lookup: BundledStaticCatalogLookup;
|
||||
resolverParams: BundledProviderStaticCatalogResolverParams;
|
||||
@@ -735,5 +707,5 @@ export async function resolveBundledProviderStaticCatalogModel(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
metadataSnapshot?: PluginMetadataSnapshot;
|
||||
}): Promise<ProviderRuntimeModel | undefined> {
|
||||
return createBundledProviderStaticCatalogModelResolver(params)(params);
|
||||
return createScopedBundledProviderStaticCatalogModelResolver(params)(params);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ConfiguredModelRef } from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import type { ModelCatalogRef } from "@openclaw/model-catalog-core/model-catalog-refs";
|
||||
import type { ProviderCatalogOutcome } from "../plugins/provider-catalog.types.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles/types.js";
|
||||
import type { InlineModelEntry } from "./embedded-agent-runner/model.inline-provider.js";
|
||||
@@ -19,7 +19,7 @@ export type PreparedModelRuntimeAgentBaseFacts = {
|
||||
templateAuthStorage: AuthStorage;
|
||||
credentials: Readonly<AuthStorageData>;
|
||||
providerIds: string[];
|
||||
configuredModelRefs: readonly ConfiguredModelRef[];
|
||||
configuredModelRefs: readonly ModelCatalogRef[];
|
||||
};
|
||||
|
||||
export type PreparedModelRuntimeAgentFacts = PreparedModelRuntimeAgentBaseFacts & {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ConfiguredModelRef } from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import type { ModelCatalogRef } from "@openclaw/model-catalog-core/model-catalog-refs";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import type { InlineModelEntry } from "./embedded-agent-runner/model.inline-provider.js";
|
||||
import type { ModelCatalogEntry } from "./model-catalog.js";
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import type { ModelRegistry } from "./sessions/model-registry.js";
|
||||
|
||||
type ConfiguredCatalogAgentFacts = {
|
||||
configuredModelRefs: readonly ConfiguredModelRef[];
|
||||
configuredModelRefs: readonly ModelCatalogRef[];
|
||||
runtimeCapabilityModels: readonly PreparedRuntimeCapabilityModel[];
|
||||
};
|
||||
|
||||
@@ -50,16 +50,7 @@ function createConfiguredModelCatalogSnapshot(params: {
|
||||
for (const configured of params.configuredRuntimeModels) {
|
||||
addEntry(toStaticCatalogEntry(configured.model));
|
||||
}
|
||||
for (const { value } of params.agentFacts.configuredModelRefs) {
|
||||
const separator = value.indexOf("/");
|
||||
if (separator <= 0 || separator >= value.length - 1) {
|
||||
continue;
|
||||
}
|
||||
const provider = normalizeProviderId(value.slice(0, separator));
|
||||
const modelId = value.slice(separator + 1).trim();
|
||||
if (!provider || !modelId) {
|
||||
continue;
|
||||
}
|
||||
for (const { provider, modelId } of params.agentFacts.configuredModelRefs) {
|
||||
const model = params.templateModelRegistry.find(provider, modelId);
|
||||
if (model) {
|
||||
addEntry(toStaticCatalogEntry(model));
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { ConfiguredModelRef } from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import {
|
||||
buildModelCatalogMergeKey,
|
||||
parseModelCatalogRef,
|
||||
type ModelCatalogRef,
|
||||
} from "@openclaw/model-catalog-core/model-catalog-refs";
|
||||
import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
|
||||
import type { PreparedConfiguredRuntimeModel } from "./prepared-model-runtime.configured.js";
|
||||
|
||||
export function completeConfiguredRuntimeModels(params: {
|
||||
configuredModelRefs: readonly ConfiguredModelRef[];
|
||||
configuredModelRefs: readonly ModelCatalogRef[];
|
||||
configuredRuntimeModels: readonly PreparedConfiguredRuntimeModel[];
|
||||
resolveDynamicModel: (lookup: {
|
||||
provider: string;
|
||||
@@ -22,20 +21,16 @@ export function completeConfiguredRuntimeModels(params: {
|
||||
);
|
||||
const completed: PreparedConfiguredRuntimeModel[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const { value } of params.configuredModelRefs) {
|
||||
const parsed = parseModelCatalogRef(value);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
const key = buildModelCatalogMergeKey(parsed.provider, parsed.modelId);
|
||||
for (const ref of params.configuredModelRefs) {
|
||||
const key = buildModelCatalogMergeKey(ref.provider, ref.modelId);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
const prepared = existing.get(key);
|
||||
const model = prepared?.model ?? params.resolveDynamicModel(parsed);
|
||||
const model = prepared?.model ?? params.resolveDynamicModel(ref);
|
||||
if (model) {
|
||||
completed.push({ provider: parsed.provider, modelId: parsed.modelId, model });
|
||||
completed.push({ ...ref, model });
|
||||
}
|
||||
}
|
||||
return completed;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import {
|
||||
buildModelCatalogMergeKey,
|
||||
parseModelCatalogRef,
|
||||
type ModelCatalogRef,
|
||||
} from "@openclaw/model-catalog-core/model-catalog-refs";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { MODEL_APIS } from "../config/types.models.js";
|
||||
@@ -163,8 +164,7 @@ export function collectConfiguredProviderIdsNeedingStaticCatalog(params: {
|
||||
}
|
||||
|
||||
export function prepareConfiguredRuntimeModels(params: {
|
||||
config: OpenClawConfig;
|
||||
configuredModelRefs?: readonly ConfiguredModelRef[];
|
||||
configuredModelRefs: readonly ModelCatalogRef[];
|
||||
metadataSnapshot: PluginMetadataSnapshot;
|
||||
preparedStaticProviderCatalog?: PreparedProviderStaticCatalog;
|
||||
providerStaticModels: readonly ProviderRuntimeModel[];
|
||||
@@ -176,12 +176,7 @@ export function prepareConfiguredRuntimeModels(params: {
|
||||
}): PreparedConfiguredRuntimeModel[] {
|
||||
const prepared: PreparedConfiguredRuntimeModel[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const { value } of params.configuredModelRefs ?? collectConfiguredModelRefs(params.config)) {
|
||||
const parsed = parseModelCatalogRef(value);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
const { modelId, provider } = parsed;
|
||||
for (const { modelId, provider } of params.configuredModelRefs) {
|
||||
const key = buildModelCatalogMergeKey(provider, modelId);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { parseModelCatalogRef } from "@openclaw/model-catalog-core/model-catalog-refs";
|
||||
import {
|
||||
findNormalizedProviderValue,
|
||||
normalizeProviderId,
|
||||
@@ -113,7 +114,7 @@ function prepareAgentFacts(
|
||||
});
|
||||
const credentials = authFacts.credentials;
|
||||
const templateAuthStorage = authFacts.authStorage;
|
||||
const configuredModelRefs = collectPreparedModelRuntimeConfiguredRefs(
|
||||
const rawConfiguredModelRefs = collectPreparedModelRuntimeConfiguredRefs(
|
||||
input.config,
|
||||
input.agentId,
|
||||
);
|
||||
@@ -123,7 +124,12 @@ function prepareAgentFacts(
|
||||
authStore: authFacts.store,
|
||||
templateAuthStorage,
|
||||
credentials,
|
||||
configuredModelRefs,
|
||||
// Keep order and case-distinct refs: registry lookup remains exact-case even
|
||||
// where static/dynamic completion deduplicates case-insensitive merge keys.
|
||||
configuredModelRefs: rawConfiguredModelRefs.flatMap(({ value }) => {
|
||||
const ref = parseModelCatalogRef(value);
|
||||
return ref ? [ref] : [];
|
||||
}),
|
||||
// Gateway startup prepares only providers named by config/model selection. An unrelated
|
||||
// stored credential must not pull that provider's complete catalog into the admission path.
|
||||
providerIds: [
|
||||
@@ -132,7 +138,7 @@ function prepareAgentFacts(
|
||||
input.config,
|
||||
credentials,
|
||||
catalogMode === "live",
|
||||
configuredModelRefs,
|
||||
rawConfiguredModelRefs,
|
||||
),
|
||||
...parseConfiguredModelVisibilityEntries({
|
||||
cfg: input.config,
|
||||
@@ -344,7 +350,6 @@ export async function prepareWorkspaceBuildGroup(
|
||||
const agentFacts: PreparedModelRuntimeAgentFacts[] = [];
|
||||
for (const facts of agentBaseFacts) {
|
||||
const configuredRuntimeModels = prepareConfiguredRuntimeModels({
|
||||
config: facts.input.config,
|
||||
configuredModelRefs: facts.configuredModelRefs,
|
||||
metadataSnapshot: pluginMetadataSnapshot,
|
||||
...(preparedStaticProviderCatalog ? { preparedStaticProviderCatalog } : {}),
|
||||
@@ -373,18 +378,8 @@ export async function prepareWorkspaceBuildGroup(
|
||||
}
|
||||
const configuredGeneratedCatalogPluginIds = [
|
||||
...new Set(
|
||||
facts.configuredModelRefs.flatMap(({ value }) => {
|
||||
const separator = value.indexOf("/");
|
||||
if (separator <= 0 || separator >= value.length - 1) {
|
||||
return [];
|
||||
}
|
||||
const provider = normalizeProviderId(value.slice(0, separator));
|
||||
const modelId = value.slice(separator + 1).trim();
|
||||
if (
|
||||
!provider ||
|
||||
!modelId ||
|
||||
configuredEntryKeys.has(modelCatalogEntryKey({ provider, id: modelId }))
|
||||
) {
|
||||
facts.configuredModelRefs.flatMap(({ provider, modelId }) => {
|
||||
if (configuredEntryKeys.has(modelCatalogEntryKey({ provider, id: modelId }))) {
|
||||
return [];
|
||||
}
|
||||
const pluginId = resolvePluginModelCatalogOwnerPluginId({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import type { ModelRegistry } from "./sessions/model-registry.js";
|
||||
|
||||
type CreateStaticCatalogResolver =
|
||||
typeof import("./embedded-agent-runner/model.static-catalog.js").createBundledStaticCatalogModelResolver;
|
||||
@@ -29,7 +30,7 @@ const mocks = vi.hoisted(() => {
|
||||
const modelRegistry = {
|
||||
fork: vi.fn((nextAuthStorage: unknown) => ({ authStorage: nextAuthStorage })),
|
||||
getAll: vi.fn(() => []),
|
||||
find: vi.fn(() => null),
|
||||
find: vi.fn<ModelRegistry["find"]>(() => undefined),
|
||||
};
|
||||
const resolveSyntheticAuth = vi.fn(() => ({
|
||||
apiKey: "synthetic-openai-key",
|
||||
@@ -227,6 +228,7 @@ beforeEach(() => {
|
||||
.mockReset()
|
||||
.mockReturnValue(createEmptyPluginRegistry());
|
||||
vi.clearAllMocks();
|
||||
mocks.modelRegistry.find.mockReset();
|
||||
mocks.resolveStaticCatalogModel.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
@@ -524,6 +526,22 @@ describe("prepared model runtime Gateway catalog mode", () => {
|
||||
source: "test",
|
||||
});
|
||||
mocks.loadAgentRuntimePluginRegistryHandle.mockReturnValue(registry);
|
||||
mocks.modelRegistry.find.mockImplementation((registryProvider, registryModelId) =>
|
||||
registryProvider === "registry-only" && registryModelId === "MIXED"
|
||||
? {
|
||||
provider: registryProvider,
|
||||
id: registryModelId,
|
||||
name: "Exact-case registry model",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://registry.invalid/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 32_000,
|
||||
maxTokens: 4096,
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
const providerConfig = {
|
||||
api: "openai-responses" as const,
|
||||
baseUrl: "https://configured.fixture.invalid/v1",
|
||||
@@ -535,7 +553,14 @@ describe("prepared model runtime Gateway catalog mode", () => {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: `${provider}/${modelId}`,
|
||||
fallbacks: ["openai/gpt-5.5", `${provider}/${modelId}`],
|
||||
fallbacks: [
|
||||
"openai/gpt-5.5",
|
||||
`${provider}/${modelId}`,
|
||||
"registry-only/mixed",
|
||||
"REGISTRY-ONLY/MIXED",
|
||||
"bare-alias",
|
||||
"provider-only/",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -578,24 +603,24 @@ describe("prepared model runtime Gateway catalog mode", () => {
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://fixture.invalid/v1",
|
||||
});
|
||||
for (const entries of [
|
||||
snapshot?.modelCatalog.entries,
|
||||
snapshot?.modelCatalog.routeVariants,
|
||||
snapshot?.modelCatalog.staticEntries,
|
||||
]) {
|
||||
for (const entries of [snapshot?.modelCatalog.entries, snapshot?.modelCatalog.routeVariants]) {
|
||||
expect(entries?.map((entry) => `${entry.provider}/${entry.id}`)).toEqual([
|
||||
`${provider}/${modelId}`,
|
||||
"openai/gpt-5.5",
|
||||
"registry-only/MIXED",
|
||||
]);
|
||||
}
|
||||
expect(
|
||||
snapshot?.modelCatalog.staticEntries?.map((entry) => `${entry.provider}/${entry.id}`),
|
||||
).toEqual([`${provider}/${modelId}`, "openai/gpt-5.5"]);
|
||||
expect(
|
||||
snapshot?.modelCatalog.staticEntries?.find((entry) => entry.provider === "openai")
|
||||
?.thinkingLevelMap,
|
||||
).toEqual({ off: null, max: "max" });
|
||||
expect(mocks.prepareStaticCatalog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
providerDiscoveryProviderIds: [provider, "openai"],
|
||||
staticCatalogProviderIds: [provider, "openai"],
|
||||
providerDiscoveryProviderIds: [provider, "openai", "provider-only", "registry-only"],
|
||||
staticCatalogProviderIds: [provider, "openai", "registry-only"],
|
||||
}),
|
||||
);
|
||||
expect(mocks.discoverModels).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -693,70 +693,76 @@ describe("ModelRegistry models.json auth", () => {
|
||||
expect(availableRefs).toContain("nvidia/explicit-empty");
|
||||
});
|
||||
|
||||
it("isolates invalid SQLite-cached plugin catalogs from valid models", () => {
|
||||
const modelsPath = writeModelsJsonWithPluginCatalogs({
|
||||
root: {
|
||||
providers: {
|
||||
custom: {
|
||||
baseUrl: "https://models.example/v1",
|
||||
api: "openai-responses",
|
||||
apiKey: "CUSTOM_API_KEY",
|
||||
models: [{ id: "root-model", name: "Root Model" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginCatalogs: [
|
||||
{
|
||||
pluginRelativePath: join("plugins", "google", PLUGIN_MODEL_CATALOG_FILE),
|
||||
pluginCatalog: {
|
||||
generatedBy: PLUGIN_MODEL_CATALOG_GENERATED_BY,
|
||||
providers: {
|
||||
"google-vertex": {
|
||||
baseUrl: "https://us-central1-aiplatform.googleapis.com/v1",
|
||||
api: "google-vertex",
|
||||
apiKey: "GOOGLE_API_KEY",
|
||||
models: [
|
||||
{
|
||||
id: "gemini-3.1-pro-preview",
|
||||
name: "Gemini 3.1 Pro",
|
||||
contextWindow: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
it.each(["persisted", "captured"] as const)(
|
||||
"isolates invalid %s plugin catalogs from valid models",
|
||||
(source) => {
|
||||
const modelsPath = writeModelsJsonWithPluginCatalogs({
|
||||
root: {
|
||||
providers: {
|
||||
custom: {
|
||||
baseUrl: "https://models.example/v1",
|
||||
api: "openai-responses",
|
||||
apiKey: "CUSTOM_API_KEY",
|
||||
models: [{ id: "root-model", name: "Root Model" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pluginRelativePath: join("plugins", "zai", PLUGIN_MODEL_CATALOG_FILE),
|
||||
pluginCatalog: {
|
||||
generatedBy: PLUGIN_MODEL_CATALOG_GENERATED_BY,
|
||||
providers: {
|
||||
zai: {
|
||||
baseUrl: "https://api.z.ai/api/paas/v4",
|
||||
api: "openai-completions",
|
||||
apiKey: "ZAI_API_KEY",
|
||||
models: [{ id: "glm-5.1", name: "GLM 5.1" }],
|
||||
pluginCatalogs: [
|
||||
{
|
||||
pluginRelativePath: join("plugins", "google", PLUGIN_MODEL_CATALOG_FILE),
|
||||
pluginCatalog: {
|
||||
generatedBy: PLUGIN_MODEL_CATALOG_GENERATED_BY,
|
||||
providers: {
|
||||
"google-vertex": {
|
||||
baseUrl: "https://us-central1-aiplatform.googleapis.com/v1",
|
||||
api: "google-vertex",
|
||||
apiKey: "GOOGLE_API_KEY",
|
||||
models: [
|
||||
{
|
||||
id: "gemini-3.1-pro-preview",
|
||||
name: "Gemini 3.1 Pro",
|
||||
contextWindow: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
{
|
||||
pluginRelativePath: join("plugins", "zai", PLUGIN_MODEL_CATALOG_FILE),
|
||||
pluginCatalog: {
|
||||
generatedBy: PLUGIN_MODEL_CATALOG_GENERATED_BY,
|
||||
providers: {
|
||||
zai: {
|
||||
baseUrl: "https://api.z.ai/api/paas/v4",
|
||||
api: "openai-completions",
|
||||
apiKey: "ZAI_API_KEY",
|
||||
models: [{ id: "glm-5.1", name: "GLM 5.1" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const registry = ModelRegistry.create(AuthStorage.inMemory(), modelsPath, {
|
||||
pluginMetadataSnapshot: pluginOwnerSnapshotEntries([
|
||||
{ providerId: "google-vertex", pluginId: "google" },
|
||||
{ providerId: "zai", pluginId: "zai" },
|
||||
]),
|
||||
});
|
||||
const registry = ModelRegistry.create(AuthStorage.inMemory(), modelsPath, {
|
||||
...(source === "captured"
|
||||
? { pluginCatalogs: listPersistedPluginModelCatalogs(dirname(modelsPath)) }
|
||||
: {}),
|
||||
pluginMetadataSnapshot: pluginOwnerSnapshotEntries([
|
||||
{ providerId: "google-vertex", pluginId: "google" },
|
||||
{ providerId: "zai", pluginId: "zai" },
|
||||
]),
|
||||
});
|
||||
|
||||
expect(registry.getError()).toContain(
|
||||
"Provider google-vertex, model gemini-3.1-pro-preview: invalid contextWindow",
|
||||
);
|
||||
expect(registry.find("custom", "root-model")?.name).toBe("Root Model");
|
||||
expect(registry.find("zai", "glm-5.1")?.name).toBe("GLM 5.1");
|
||||
expect(registry.find("google-vertex", "gemini-3.1-pro-preview")).toBeUndefined();
|
||||
});
|
||||
expect(registry.getError()).toContain(
|
||||
"Provider google-vertex, model gemini-3.1-pro-preview: invalid contextWindow",
|
||||
);
|
||||
expect(registry.find("custom", "root-model")?.name).toBe("Root Model");
|
||||
expect(registry.find("zai", "glm-5.1")?.name).toBe("GLM 5.1");
|
||||
expect(registry.find("google-vertex", "gemini-3.1-pro-preview")).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it("repairs missing-api generated rows before repeated registry loads", () => {
|
||||
const modelsPath = writeModelsJsonWithPluginCatalogs({
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
} from "./model-registry-runtime.js";
|
||||
import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.js";
|
||||
import {
|
||||
clearConfigValueCache,
|
||||
resolveConfigValueOrThrow,
|
||||
resolveConfigValueUncached,
|
||||
resolveHeadersOrThrow,
|
||||
@@ -312,9 +311,6 @@ function mergeCompat(
|
||||
return merged as Model["compat"];
|
||||
}
|
||||
|
||||
/** Clear the config value command cache. Exported for testing. */
|
||||
export const clearApiKeyCache = clearConfigValueCache;
|
||||
|
||||
/**
|
||||
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
||||
*/
|
||||
@@ -583,13 +579,9 @@ export class ModelRegistry {
|
||||
if (options.includePluginCatalogs !== false) {
|
||||
let pluginCatalogs: readonly PersistedPluginModelCatalog[] = [];
|
||||
try {
|
||||
if (this.pluginCatalogs) {
|
||||
pluginCatalogs = this.pluginCatalogs;
|
||||
} else {
|
||||
const loaded = loadPersistedPluginModelCatalogs(dirname(modelsJsonPath));
|
||||
pluginCatalogs = loaded.catalogs;
|
||||
pluginCatalogErrors.push(...loaded.warnings);
|
||||
}
|
||||
const loaded = loadPersistedPluginModelCatalogs(dirname(modelsJsonPath));
|
||||
pluginCatalogs = loaded.catalogs;
|
||||
pluginCatalogErrors.push(...loaded.warnings);
|
||||
} catch (error) {
|
||||
pluginCatalogErrors.push(
|
||||
`Failed to load generated plugin model catalogs: ${
|
||||
@@ -597,21 +589,10 @@ export class ModelRegistry {
|
||||
}`,
|
||||
);
|
||||
}
|
||||
for (const pluginCatalog of pluginCatalogs) {
|
||||
const pluginResult = this.loadCustomModels(
|
||||
`sqlite:plugin-model-catalog/${pluginCatalog.pluginId}`,
|
||||
{
|
||||
catalogPluginId: pluginCatalog.pluginId,
|
||||
contents: pluginCatalog.contents,
|
||||
includePluginCatalogs: false,
|
||||
requireGeneratedCatalog: true,
|
||||
},
|
||||
);
|
||||
if (pluginResult.error) {
|
||||
pluginCatalogErrors.push(pluginResult.error);
|
||||
continue;
|
||||
}
|
||||
models.push(...pluginResult.models);
|
||||
const pluginResult = this.loadCapturedPluginCatalogs(pluginCatalogs);
|
||||
models.push(...pluginResult.models);
|
||||
if (pluginResult.error) {
|
||||
pluginCatalogErrors.push(pluginResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,8 +135,3 @@ export function resolveHeadersOrThrow(
|
||||
}
|
||||
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
||||
}
|
||||
|
||||
/** Clear the config value command cache. Exported for testing. */
|
||||
export function clearConfigValueCache(): void {
|
||||
commandResultCache.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user