mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: exact dated models work after gateway ready (#121119)
* fix(agents): publish exact configured model facts * fix(agents): keep prepared model completion internal * fix(agents): scope prepared model plugin lookup * refactor(agents): split prepared model configured facts * fix(agents): keep configured catalog types acyclic
This commit is contained in:
committed by
GitHub
parent
dd5b4d0d30
commit
af4ffb2e8e
@@ -40,13 +40,33 @@ describe("unreleased Claude generations", () => {
|
||||
expect(resolveModel("claude-opus-4-8")?.params?.canonicalModelId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("leaves pre-4.6 and snapshot-dated ids alone", () => {
|
||||
it("does not mistake snapshot dates for minor versions", () => {
|
||||
// claude-opus-4-20250514 is 4.0; a naive parse reads 4.20 and would treat it
|
||||
// as newer than every released generation.
|
||||
expect(resolveModel("claude-opus-4-20250514")?.params?.canonicalModelId).toBeUndefined();
|
||||
expect(supportsClaudeAdaptiveThinking({ id: "claude-haiku-4-5-20251001" })).toBe(false);
|
||||
});
|
||||
|
||||
it("clones released snapshot ids from their dateless manifest template", () => {
|
||||
expect(resolveModel("claude-haiku-4-5-20251001")).toEqual({
|
||||
id: "claude-haiku-4-5-20251001",
|
||||
name: "claude-haiku-4-5-20251001",
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
mediaInput: {
|
||||
image: { maxSidePx: 1568, preferredSidePx: 1568, tokenMode: "provider" },
|
||||
},
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 64_000,
|
||||
compat: { codeMode: "preferred" },
|
||||
});
|
||||
expect(resolveModel("claude-haiku-4-9-20251001")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("carries manifest catalog compat onto hand-built modern rows", () => {
|
||||
// The hand-built forward-compat row replaces the catalog row when the
|
||||
// runtime prefers plugin-resolved modern models. Dropping compat here
|
||||
|
||||
@@ -384,6 +384,33 @@ function resolveAnthropic46ForwardCompatModel(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAnthropicSnapshotModel(
|
||||
ctx: ProviderResolveDynamicModelContext,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
const modelId = ctx.modelId.trim();
|
||||
const normalizedModelId = normalizeLowercaseStringOrEmpty(modelId);
|
||||
const match = /^(claude-[a-z0-9]+(?:-[a-z0-9]+)*)-\d{8}$/.exec(normalizedModelId);
|
||||
if (
|
||||
modelId !== normalizedModelId ||
|
||||
normalizeLowercaseStringOrEmpty(ctx.provider) !== PROVIDER_ID ||
|
||||
!match
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const templateId = match[1]!;
|
||||
const captured = cloneFirstTemplateModel({
|
||||
providerId: PROVIDER_ID,
|
||||
modelId,
|
||||
templateIds: [templateId],
|
||||
ctx,
|
||||
});
|
||||
if (captured) {
|
||||
return captured;
|
||||
}
|
||||
const template = resolveAnthropicManifestModel(templateId);
|
||||
return template ? { ...template, id: modelId, name: modelId } : undefined;
|
||||
}
|
||||
|
||||
/** Newest Claude generation whose request contract this plugin encodes. */
|
||||
const ANTHROPIC_NEWEST_KNOWN_GENERATION = { major: 5, minor: 0 } as const;
|
||||
|
||||
@@ -436,28 +463,40 @@ function resolveAnthropicUnreleasedCanonicalModelId(modelId: string): string {
|
||||
return /(?:^|-)claude-sonnet-/.test(modelId) ? "claude-sonnet-5" : "claude-opus-5";
|
||||
}
|
||||
|
||||
// Lazily indexed manifest compat per provider so hand-built dynamic rows keep
|
||||
// catalog capability metadata even when the run's model registry is empty
|
||||
// (for example env-key-only runs without a generated models.json).
|
||||
let anthropicManifestCompatIndex: Map<string, ModelCompatConfig> | undefined;
|
||||
// Dynamic rows use the manifest as the provider-owned offline contract when a lifecycle registry
|
||||
// has no template yet. Keeping one normalized index avoids reparsing catalog metadata per run.
|
||||
let anthropicManifestModelIndex: Map<string, ProviderRuntimeModel> | undefined;
|
||||
|
||||
function resolveAnthropicManifestModel(modelId: string): ProviderRuntimeModel | undefined {
|
||||
if (!anthropicManifestModelIndex) {
|
||||
anthropicManifestModelIndex = new Map();
|
||||
const catalog = buildAnthropicCatalogProvider();
|
||||
for (const model of catalog.models ?? []) {
|
||||
const api = model.api ?? catalog.api;
|
||||
const baseUrl = model.baseUrl ?? catalog.baseUrl;
|
||||
if (api && baseUrl) {
|
||||
anthropicManifestModelIndex.set(model.id, {
|
||||
...model,
|
||||
input: model.input.filter(
|
||||
(item): item is "text" | "image" => item === "text" || item === "image",
|
||||
),
|
||||
provider: PROVIDER_ID,
|
||||
api,
|
||||
baseUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return anthropicManifestModelIndex.get(modelId);
|
||||
}
|
||||
|
||||
function resolveAnthropicManifestCompat(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
): ModelCompatConfig | undefined {
|
||||
if (!anthropicManifestCompatIndex) {
|
||||
anthropicManifestCompatIndex = new Map();
|
||||
const providers = manifest.modelCatalog?.providers ?? {};
|
||||
for (const [providerId, catalog] of Object.entries(providers)) {
|
||||
for (const model of catalog.models ?? []) {
|
||||
const compat = (model as { compat?: ModelCompatConfig }).compat;
|
||||
if (compat) {
|
||||
anthropicManifestCompatIndex.set(`${providerId}/${model.id}`, compat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return anthropicManifestCompatIndex.get(`${provider}/${modelId}`);
|
||||
return normalizeLowercaseStringOrEmpty(provider) === PROVIDER_ID
|
||||
? resolveAnthropicManifestModel(modelId)?.compat
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function buildAnthropicForwardCompatModel(
|
||||
@@ -528,6 +567,7 @@ function resolveAnthropicForwardCompatModel(
|
||||
ctx: ProviderResolveDynamicModelContext,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
return (
|
||||
resolveAnthropicSnapshotModel(ctx) ??
|
||||
resolveAnthropic46ForwardCompatModel({
|
||||
ctx,
|
||||
dashModelId: ANTHROPIC_OPUS_48_MODEL_ID,
|
||||
|
||||
@@ -363,7 +363,7 @@ async function buildSnapshotBatch(
|
||||
}
|
||||
const registryMs = performance.now() - registryStartedAt;
|
||||
const preparedAgentFacts = [...preparedInputs.values()];
|
||||
const configuredRuntimeModelCount = preparedAgentFacts.reduce(
|
||||
const configuredRuntimeModelCount = [...preparedCatalogs.values()].reduce(
|
||||
(count, facts) => count + facts.configuredRuntimeModels.length,
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { ConfiguredModelRef } from "@openclaw/model-catalog-core/configured-model-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";
|
||||
import type { ModelCatalogSnapshot } from "./model-catalog.types.js";
|
||||
import {
|
||||
toStaticCatalogEntry,
|
||||
type PreparedConfiguredRuntimeModel,
|
||||
} from "./prepared-model-runtime.configured.js";
|
||||
import type { ModelRegistry } from "./sessions/model-registry.js";
|
||||
|
||||
type ConfiguredCatalogAgentFacts = {
|
||||
configuredModelRefs: readonly ConfiguredModelRef[];
|
||||
};
|
||||
|
||||
type ConfiguredCatalogWorkspaceFacts = {
|
||||
configuredCatalogEntries: readonly ModelCatalogEntry[];
|
||||
inlineProviderModels: readonly InlineModelEntry[];
|
||||
};
|
||||
|
||||
type ConfiguredRuntimeFacts = {
|
||||
templateModelRegistry: ModelRegistry;
|
||||
modelCatalog: ModelCatalogSnapshot;
|
||||
configuredRuntimeModels: readonly PreparedConfiguredRuntimeModel[];
|
||||
inlineProviderModels: readonly InlineModelEntry[];
|
||||
};
|
||||
|
||||
export function modelCatalogEntryKey(entry: Pick<ModelCatalogEntry, "id" | "provider">): string {
|
||||
return `${normalizeProviderId(entry.provider)}\0${entry.id.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
function createConfiguredModelCatalogSnapshot(params: {
|
||||
agentFacts: ConfiguredCatalogAgentFacts;
|
||||
workspaceFacts: ConfiguredCatalogWorkspaceFacts;
|
||||
templateModelRegistry: ModelRegistry;
|
||||
configuredRuntimeModels: readonly PreparedConfiguredRuntimeModel[];
|
||||
}): ModelCatalogSnapshot {
|
||||
const entries = new Map<string, ModelCatalogEntry>();
|
||||
const addEntry = (entry: ModelCatalogEntry) => {
|
||||
const key = modelCatalogEntryKey(entry);
|
||||
if (!entries.has(key)) {
|
||||
entries.set(key, entry);
|
||||
}
|
||||
};
|
||||
for (const entry of params.workspaceFacts.configuredCatalogEntries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
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;
|
||||
}
|
||||
const model = params.templateModelRegistry.find(provider, modelId);
|
||||
if (model) {
|
||||
addEntry(toStaticCatalogEntry(model));
|
||||
}
|
||||
}
|
||||
const configuredEntries = [...entries.values()];
|
||||
const staticEntries = params.configuredRuntimeModels.map(({ model }) =>
|
||||
toStaticCatalogEntry(model),
|
||||
);
|
||||
return {
|
||||
entries: configuredEntries,
|
||||
routeVariants: configuredEntries,
|
||||
...(staticEntries.length > 0 ? { staticEntries } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function prepareConfiguredRuntimeFacts(params: {
|
||||
agentFacts: ConfiguredCatalogAgentFacts;
|
||||
workspaceFacts: ConfiguredCatalogWorkspaceFacts;
|
||||
templateModelRegistry: ModelRegistry;
|
||||
configuredRuntimeModels: readonly PreparedConfiguredRuntimeModel[];
|
||||
}): ConfiguredRuntimeFacts {
|
||||
return {
|
||||
templateModelRegistry: params.templateModelRegistry,
|
||||
modelCatalog: createConfiguredModelCatalogSnapshot(params),
|
||||
configuredRuntimeModels: params.configuredRuntimeModels,
|
||||
inlineProviderModels: params.workspaceFacts.inlineProviderModels,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { ConfiguredModelRef } from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import {
|
||||
buildModelCatalogMergeKey,
|
||||
parseModelCatalogRef,
|
||||
} 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[];
|
||||
configuredRuntimeModels: readonly PreparedConfiguredRuntimeModel[];
|
||||
resolveDynamicModel: (lookup: {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
}) => ProviderRuntimeModel | undefined;
|
||||
}): PreparedConfiguredRuntimeModel[] {
|
||||
const existing = new Map(
|
||||
params.configuredRuntimeModels.map((configured) => [
|
||||
buildModelCatalogMergeKey(configured.provider, configured.modelId),
|
||||
configured,
|
||||
]),
|
||||
);
|
||||
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);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
const prepared = existing.get(key);
|
||||
const model = prepared?.model ?? params.resolveDynamicModel(parsed);
|
||||
if (model) {
|
||||
completed.push({ provider: parsed.provider, modelId: parsed.modelId, model });
|
||||
}
|
||||
}
|
||||
return completed;
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import {
|
||||
collectConfiguredModelRefs,
|
||||
type ConfiguredModelRef,
|
||||
} from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import {
|
||||
buildModelCatalogMergeKey,
|
||||
parseModelCatalogRef,
|
||||
} 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";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
@@ -125,15 +129,12 @@ export function collectConfiguredProviderIdsNeedingStaticCatalog(params: {
|
||||
}): string[] {
|
||||
const providerIds = new Set<string>();
|
||||
for (const { value } of params.configuredModelRefs ?? collectConfiguredModelRefs(params.config)) {
|
||||
const separator = value.indexOf("/");
|
||||
if (separator <= 0 || separator >= value.length - 1) {
|
||||
const parsed = parseModelCatalogRef(value);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
const provider = normalizeProviderId(value.slice(0, separator));
|
||||
const modelId = value.slice(separator + 1).trim();
|
||||
const { provider, modelId } = parsed;
|
||||
if (
|
||||
!provider ||
|
||||
!modelId ||
|
||||
hasConfiguredInlineProviderModel(
|
||||
params.config,
|
||||
provider,
|
||||
@@ -164,16 +165,12 @@ export function prepareConfiguredRuntimeModels(params: {
|
||||
const prepared: PreparedConfiguredRuntimeModel[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const { value } of params.configuredModelRefs ?? collectConfiguredModelRefs(params.config)) {
|
||||
const separator = value.indexOf("/");
|
||||
if (separator <= 0 || separator >= value.length - 1) {
|
||||
const parsed = parseModelCatalogRef(value);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
const provider = normalizeProviderId(value.slice(0, separator));
|
||||
const modelId = value.slice(separator + 1).trim();
|
||||
if (!provider || !modelId) {
|
||||
continue;
|
||||
}
|
||||
const key = `${provider}\0${modelId.toLowerCase()}`;
|
||||
const { modelId, provider } = parsed;
|
||||
const key = buildModelCatalogMergeKey(provider, modelId);
|
||||
if (seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import type { ConfiguredModelRef } from "@openclaw/model-catalog-core/configured-model-refs";
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import {
|
||||
findNormalizedProviderValue,
|
||||
normalizeProviderId,
|
||||
} from "@openclaw/model-catalog-core/provider-id";
|
||||
import { stableStringify } from "@openclaw/normalization-core";
|
||||
import type { PreparedMessageToolCatalog } from "../channels/plugins/message-action-discovery.js";
|
||||
import { sha256Base64Url } from "../infra/crypto-digest.js";
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
getPreparedMessageToolCatalogForRegistry,
|
||||
} from "../plugins/prepared-message-tool-catalog.js";
|
||||
import type { PreparedProviderStaticCatalog } from "../plugins/provider-discovery.js";
|
||||
import { resolveLoadedProviderRuntimePlugin } from "../plugins/provider-hook-runtime.js";
|
||||
import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js";
|
||||
import { withPluginRuntimeRegistryScope } from "../plugins/runtime/gateway-request-scope.js";
|
||||
import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js";
|
||||
@@ -42,6 +46,11 @@ import {
|
||||
resolvePluginModelCatalogOwnerPluginId,
|
||||
type PersistedPluginModelCatalog,
|
||||
} from "./plugin-model-catalog.js";
|
||||
import {
|
||||
modelCatalogEntryKey,
|
||||
prepareConfiguredRuntimeFacts,
|
||||
} from "./prepared-model-runtime.configured-catalog.js";
|
||||
import { completeConfiguredRuntimeModels } from "./prepared-model-runtime.configured-completion.js";
|
||||
import {
|
||||
collectPreparedModelRuntimeConfiguredRefs,
|
||||
collectConfiguredProviderIdsNeedingStaticCatalog,
|
||||
@@ -465,76 +474,6 @@ export function isPreparedModelCatalogFull(snapshot: ModelCatalogSnapshot): bool
|
||||
return fullModelCatalogSnapshots.has(snapshot);
|
||||
}
|
||||
|
||||
function modelCatalogEntryKey(entry: Pick<ModelCatalogEntry, "id" | "provider">): string {
|
||||
return `${normalizeProviderId(entry.provider)}\0${entry.id.trim().toLowerCase()}`;
|
||||
}
|
||||
|
||||
function createConfiguredModelCatalogSnapshot(params: {
|
||||
agentFacts: PreparedModelRuntimeAgentFacts;
|
||||
workspaceFacts: PreparedModelRuntimeWorkspaceFacts;
|
||||
templateModelRegistry: ModelRegistry;
|
||||
configuredRuntimeModels: readonly PreparedConfiguredRuntimeModel[];
|
||||
}): ModelCatalogSnapshot {
|
||||
const entries = new Map<string, ModelCatalogEntry>();
|
||||
const addEntry = (entry: ModelCatalogEntry) => {
|
||||
const key = modelCatalogEntryKey(entry);
|
||||
if (!entries.has(key)) {
|
||||
entries.set(key, entry);
|
||||
}
|
||||
};
|
||||
for (const entry of params.workspaceFacts.configuredCatalogEntries) {
|
||||
addEntry(entry);
|
||||
}
|
||||
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;
|
||||
}
|
||||
const model = params.templateModelRegistry.find(provider, modelId);
|
||||
if (model) {
|
||||
addEntry(toStaticCatalogEntry(model));
|
||||
}
|
||||
}
|
||||
const configuredEntries = [...entries.values()];
|
||||
const staticEntries = params.configuredRuntimeModels.map(({ model }) =>
|
||||
toStaticCatalogEntry(model),
|
||||
);
|
||||
return {
|
||||
entries: configuredEntries,
|
||||
routeVariants: configuredEntries,
|
||||
...(staticEntries.length > 0 ? { staticEntries } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function prepareConfiguredRuntimeFacts(
|
||||
agentFacts: PreparedModelRuntimeAgentFacts,
|
||||
workspaceFacts: PreparedModelRuntimeWorkspaceFacts,
|
||||
sharedTemplateModelRegistry: ModelRegistry,
|
||||
): PreparedModelRuntimeCatalogFacts {
|
||||
const { configuredRuntimeModels } = agentFacts;
|
||||
const { inlineProviderModels } = workspaceFacts;
|
||||
const templateModelRegistry = sharedTemplateModelRegistry;
|
||||
return {
|
||||
templateModelRegistry,
|
||||
modelCatalog: createConfiguredModelCatalogSnapshot({
|
||||
agentFacts,
|
||||
workspaceFacts,
|
||||
templateModelRegistry,
|
||||
configuredRuntimeModels,
|
||||
}),
|
||||
configuredRuntimeModels,
|
||||
inlineProviderModels,
|
||||
};
|
||||
}
|
||||
|
||||
function captureModelsJsonContents(agentDir: string): string | null {
|
||||
try {
|
||||
return fs.readFileSync(path.join(agentDir, "models.json"), "utf8");
|
||||
@@ -641,12 +580,50 @@ export function prepareConfiguredRuntimeFactsBatch(params: {
|
||||
},
|
||||
);
|
||||
registryCount += 1;
|
||||
for (const facts of group.agentFacts) {
|
||||
catalogs.set(
|
||||
facts.input,
|
||||
prepareConfiguredRuntimeFacts(facts, params.workspaceFacts, templateModelRegistry),
|
||||
);
|
||||
}
|
||||
// The captured registry exists only after agent-owned catalog parsing. Complete static misses
|
||||
// here so turn facts stay within this lifecycle generation without starting live discovery.
|
||||
withPluginRuntimeRegistryScope(params.workspaceFacts.pluginRegistry, () => {
|
||||
for (const facts of group.agentFacts) {
|
||||
const { input } = facts;
|
||||
const configuredRuntimeModels = params.workspaceFacts.pluginRegistry
|
||||
? completeConfiguredRuntimeModels({
|
||||
configuredModelRefs: facts.configuredModelRefs,
|
||||
configuredRuntimeModels: facts.configuredRuntimeModels,
|
||||
resolveDynamicModel: ({ provider, modelId }) => {
|
||||
const providerConfig =
|
||||
input.config.models?.providers?.[provider] ??
|
||||
findNormalizedProviderValue(input.config.models?.providers, provider);
|
||||
return (
|
||||
resolveLoadedProviderRuntimePlugin({
|
||||
provider,
|
||||
modelId,
|
||||
config: input.config,
|
||||
workspaceDir: input.workspaceDir,
|
||||
env: facts.env,
|
||||
})?.resolveDynamicModel?.({
|
||||
config: input.config,
|
||||
agentDir: input.agentDir,
|
||||
workspaceDir: input.workspaceDir,
|
||||
provider,
|
||||
modelId,
|
||||
modelRegistry: templateModelRegistry,
|
||||
providerConfig,
|
||||
}) ?? undefined
|
||||
);
|
||||
},
|
||||
})
|
||||
: facts.configuredRuntimeModels;
|
||||
catalogs.set(
|
||||
input,
|
||||
prepareConfiguredRuntimeFacts({
|
||||
agentFacts: facts,
|
||||
workspaceFacts: params.workspaceFacts,
|
||||
templateModelRegistry,
|
||||
configuredRuntimeModels,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return { catalogs, registryCount };
|
||||
}
|
||||
|
||||
@@ -409,6 +409,107 @@ describe("prepared model runtime Gateway catalog mode", () => {
|
||||
expect(mocks.discoverModels).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("publishes exact dynamic configured models without building a live catalog", async () => {
|
||||
const provider = "fixture-provider";
|
||||
const modelId = "fixture-model-2026-08-09";
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const resolveDynamicModel = vi.fn(
|
||||
(context: { provider: string; modelId: string; modelRegistry: unknown }) => ({
|
||||
id: context.modelId,
|
||||
name: "Fixture dated model",
|
||||
provider: context.provider,
|
||||
api: "openai-responses" as const,
|
||||
baseUrl: "https://fixture.invalid/v1",
|
||||
reasoning: false,
|
||||
input: ["text" as const],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 64_000,
|
||||
maxTokens: 8_192,
|
||||
}),
|
||||
);
|
||||
registry.providers.push({
|
||||
pluginId: provider,
|
||||
provider: { id: provider, label: "Fixture provider", auth: [], resolveDynamicModel },
|
||||
source: "test",
|
||||
});
|
||||
mocks.loadAgentRuntimePluginRegistryHandle.mockReturnValue(registry);
|
||||
const providerConfig = {
|
||||
api: "openai-responses" as const,
|
||||
baseUrl: "https://configured.fixture.invalid/v1",
|
||||
models: [],
|
||||
};
|
||||
const config = {
|
||||
models: { providers: { [provider]: providerConfig } },
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: `${provider}/${modelId}`,
|
||||
fallbacks: ["openai/gpt-5.5", `${provider}/${modelId}`],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await refreshPreparedModelRuntimeSnapshots(config, {
|
||||
gatewayLifecycle: true,
|
||||
catalogMode: "static",
|
||||
});
|
||||
|
||||
expect(resolveDynamicModel).toHaveBeenCalledOnce();
|
||||
expect(mocks.discoverModels.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
resolveDynamicModel.mock.invocationCallOrder[0]!,
|
||||
);
|
||||
expect(resolveDynamicModel).toHaveBeenCalledWith({
|
||||
config,
|
||||
agentDir: "/tmp/prepared-static-agent",
|
||||
workspaceDir: "/tmp/prepared-static-workspace",
|
||||
provider,
|
||||
modelId,
|
||||
modelRegistry: mocks.modelRegistry,
|
||||
providerConfig,
|
||||
});
|
||||
const snapshot = getPreparedModelRuntimeSnapshot({
|
||||
agentId: "default",
|
||||
config,
|
||||
agentDir: "/tmp/prepared-static-agent",
|
||||
inheritedAuthDir: "/tmp/prepared-static-agent",
|
||||
workspaceDir: "/tmp/prepared-static-workspace",
|
||||
});
|
||||
expect(
|
||||
snapshot?.configuredRuntimeModels.map(
|
||||
(configured) => `${configured.provider}/${configured.modelId}`,
|
||||
),
|
||||
).toEqual([`${provider}/${modelId}`, "openai/gpt-5.5"]);
|
||||
expect(snapshot?.configuredRuntimeModels[0]?.model).toMatchObject({
|
||||
provider,
|
||||
id: modelId,
|
||||
name: "Fixture dated model",
|
||||
api: "openai-responses",
|
||||
baseUrl: "https://fixture.invalid/v1",
|
||||
});
|
||||
for (const entries of [
|
||||
snapshot?.modelCatalog.entries,
|
||||
snapshot?.modelCatalog.routeVariants,
|
||||
snapshot?.modelCatalog.staticEntries,
|
||||
]) {
|
||||
expect(entries?.map((entry) => `${entry.provider}/${entry.id}`)).toEqual([
|
||||
`${provider}/${modelId}`,
|
||||
"openai/gpt-5.5",
|
||||
]);
|
||||
}
|
||||
expect(mocks.prepareStaticCatalog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
providerDiscoveryProviderIds: [provider, "openai"],
|
||||
staticCatalogProviderIds: [provider, "openai"],
|
||||
}),
|
||||
);
|
||||
expect(mocks.discoverModels).toHaveBeenCalledOnce();
|
||||
expect(mocks.buildPreparedModelCatalogSnapshot).not.toHaveBeenCalled();
|
||||
expect(mocks.loadStaticCatalog).not.toHaveBeenCalled();
|
||||
expect(mocks.planOpenClawModelsJsonSource).not.toHaveBeenCalled();
|
||||
expect(mocks.ensureOpenClawModelsJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not request a static provider hook when manifest facts resolve the configured model", async () => {
|
||||
mocks.resolveStaticCatalogModel.mockReturnValue({
|
||||
id: "gpt-5.5",
|
||||
|
||||
Reference in New Issue
Block a user