mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 20:05:46 -06:00
fix(agents): keep model resolution on one plugin generation (#125569)
* fix(agents): pin model resolution to prepared generation * test(agents): complete prepared runtime fixture
This commit is contained in:
committed by
GitHub
parent
3ab27a2900
commit
d9b69c5b6f
@@ -1827,11 +1827,11 @@ src/agents/embedded-agent-runner/history.ts 3
|
||||
src/agents/embedded-agent-runner/message-visibility.ts 13
|
||||
src/agents/embedded-agent-runner/model-context-tokens.ts 1
|
||||
src/agents/embedded-agent-runner/model.compat.ts 1
|
||||
src/agents/embedded-agent-runner/model.configured-fallback.ts 2
|
||||
src/agents/embedded-agent-runner/model.configured-overrides.ts 3
|
||||
src/agents/embedded-agent-runner/model.configured-fallback.ts 1
|
||||
src/agents/embedded-agent-runner/model.configured-overrides.ts 2
|
||||
src/agents/embedded-agent-runner/model.inline-provider.ts 2
|
||||
src/agents/embedded-agent-runner/model.provider-hooks.ts 10
|
||||
src/agents/embedded-agent-runner/model.registry-resolution.ts 7
|
||||
src/agents/embedded-agent-runner/model.registry-resolution.ts 6
|
||||
src/agents/embedded-agent-runner/model.ts 5
|
||||
src/agents/embedded-agent-runner/openrouter-model-capabilities.ts 2
|
||||
src/agents/embedded-agent-runner/prepared-compaction-runtime.ts 1
|
||||
|
||||
+127
-12
@@ -5,11 +5,21 @@ import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coerci
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SessionEntry } from "../config/sessions.js";
|
||||
import type { ProviderResolveModelRoutesContext } from "../plugin-sdk/provider-model-types.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { resolvePluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
|
||||
import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { getPluginRuntimeGenerationRegistry } from "../plugins/runtime/generation-scope.js";
|
||||
import {
|
||||
looksLikeSecretSentinel,
|
||||
mintSecretSentinel,
|
||||
resolveSecretSentinel,
|
||||
} from "../secrets/sentinel.js";
|
||||
import {
|
||||
createModelGenerationFixture,
|
||||
GENERATION_WORKSPACE_DIR,
|
||||
publishCurrentModelGeneration,
|
||||
resetModelGenerationFixtureState,
|
||||
} from "./embedded-agent-runner/model.generation-scope.test-support.js";
|
||||
import type { AgentHarnessHostCapabilities } from "./harness/host-capability-types.js";
|
||||
import type { AgentHarness } from "./harness/types.js";
|
||||
import type { AgentRuntimeAuthPlan } from "./runtime-plan/types.js";
|
||||
@@ -63,6 +73,10 @@ const hasUsableCustomProviderApiKeyMock = vi.fn((..._args: unknown[]) => false);
|
||||
const resolveProviderEntryApiKeyProfileReferenceMock = vi.fn((_params?: unknown): unknown => ({
|
||||
kind: "none",
|
||||
}));
|
||||
const preparedRuntimeSnapshotState = vi.hoisted(() => ({
|
||||
snapshot: undefined as unknown,
|
||||
useSnapshotPluginRegistry: false,
|
||||
}));
|
||||
|
||||
vi.mock("../llm/stream.js", async () => {
|
||||
const original = await vi.importActual<typeof import("../llm/stream.js")>("../llm/stream.js");
|
||||
@@ -127,6 +141,10 @@ vi.mock("./prepared-model-runtime.js", () => ({
|
||||
...workspaceOptions,
|
||||
});
|
||||
return {
|
||||
...(preparedRuntimeSnapshotState.snapshot as object),
|
||||
...(preparedRuntimeSnapshotState.useSnapshotPluginRegistry
|
||||
? {}
|
||||
: { pluginRegistry: getActivePluginRegistry() }),
|
||||
agentId: params.agentId,
|
||||
agentDir: params.agentDir,
|
||||
config: params.config,
|
||||
@@ -349,6 +367,7 @@ const DEFAULT_STORE_PATH = "/tmp/sessions.json";
|
||||
const DEFAULT_QUESTION = "What changed?";
|
||||
const MATH_QUESTION = "What is 17 * 19?";
|
||||
const MATH_ANSWER = "323";
|
||||
let defaultPluginMetadataSnapshot: ReturnType<typeof resolvePluginMetadataSnapshot> | undefined;
|
||||
|
||||
const DEFAULT_USAGE = {
|
||||
input: 1,
|
||||
@@ -659,6 +678,7 @@ function mockOpenAIPlatformProfile(): void {
|
||||
describe("runBtwSideQuestion", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
resetModelGenerationFixtureState();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -709,6 +729,15 @@ describe("runBtwSideQuestion", () => {
|
||||
resolveProviderEntryApiKeyProfileReferenceMock.mockReset();
|
||||
resolveProviderEntryApiKeyProfileReferenceMock.mockReturnValue({ kind: "none" });
|
||||
clearAgentHarnesses();
|
||||
defaultPluginMetadataSnapshot ??= resolvePluginMetadataSnapshot({
|
||||
config: {},
|
||||
workspaceDir: "/tmp/workspace",
|
||||
allowCurrent: false,
|
||||
});
|
||||
preparedRuntimeSnapshotState.snapshot = {
|
||||
metadataSnapshot: defaultPluginMetadataSnapshot,
|
||||
};
|
||||
preparedRuntimeSnapshotState.useSnapshotPluginRegistry = false;
|
||||
|
||||
readFileMock.mockResolvedValue("mock transcript");
|
||||
loadTranscriptEventsMock.mockResolvedValue([]);
|
||||
@@ -881,6 +910,81 @@ describe("runBtwSideQuestion", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps model, runtime auth, and stream selection on prepared A after current advances to B", async () => {
|
||||
const cfg = { agents: { entries: { main: { default: true } } } } as never;
|
||||
const generationA = createModelGenerationFixture({
|
||||
config: cfg,
|
||||
label: "btw-a",
|
||||
provider: "local-proxy",
|
||||
requestProvider: "local-proxy",
|
||||
modelId: "side-model",
|
||||
});
|
||||
const generationB = createModelGenerationFixture({
|
||||
config: cfg,
|
||||
label: "btw-b",
|
||||
provider: "local-proxy",
|
||||
requestProvider: "local-proxy",
|
||||
modelId: "side-model",
|
||||
});
|
||||
preparedRuntimeSnapshotState.snapshot = generationA.preparedModelRuntime;
|
||||
preparedRuntimeSnapshotState.useSnapshotPluginRegistry = true;
|
||||
resolveAgentWorkspaceDirMock.mockReturnValue(GENERATION_WORKSPACE_DIR);
|
||||
publishCurrentModelGeneration(generationB);
|
||||
const runtimeAuthA = vi.fn(async () => ({ apiKey: "runtime-auth-a" }));
|
||||
const runtimeAuthB = vi.fn(async () => ({ apiKey: "runtime-auth-b" }));
|
||||
const streamA = vi.fn(
|
||||
(model: { name?: string }, _context: unknown, options?: { apiKey?: string }) => {
|
||||
const apiKey = options?.apiKey;
|
||||
const resolvedApiKey =
|
||||
apiKey && looksLikeSecretSentinel(apiKey) ? resolveSecretSentinel(apiKey) : apiKey;
|
||||
return makeAsyncEvents([
|
||||
createDoneEvent(`${model.name ?? "missing model"} / ${resolvedApiKey} / Stream A`),
|
||||
]);
|
||||
},
|
||||
);
|
||||
const streamB = vi.fn(() =>
|
||||
makeAsyncEvents([createDoneEvent("Generation B / runtime-auth-b / Stream B")]),
|
||||
);
|
||||
const activeGenerationRegistry = () =>
|
||||
getPluginRuntimeGenerationRegistry() ?? getActivePluginRegistry();
|
||||
resolveModelWithRegistryMock.mockImplementation(() => {
|
||||
const snapshot = getCurrentPluginMetadataSnapshot({
|
||||
config: cfg,
|
||||
workspaceDir: GENERATION_WORKSPACE_DIR,
|
||||
});
|
||||
const label = snapshot === generationA.metadataSnapshot ? "A" : "B";
|
||||
return {
|
||||
provider: "local-proxy",
|
||||
id: "side-model",
|
||||
name: `Generation ${label}`,
|
||||
api: "openai-responses",
|
||||
baseUrl: `https://generation-${label.toLowerCase()}.example.test/v1`,
|
||||
};
|
||||
});
|
||||
prepareProviderRuntimeAuthMock.mockImplementation(async () =>
|
||||
activeGenerationRegistry() === generationA.pluginRegistry
|
||||
? await runtimeAuthA()
|
||||
: await runtimeAuthB(),
|
||||
);
|
||||
registerProviderStreamForModelMock.mockImplementation(() =>
|
||||
activeGenerationRegistry() === generationA.pluginRegistry ? streamA : streamB,
|
||||
);
|
||||
|
||||
await expect(
|
||||
runSideQuestion({ cfg, provider: "local-proxy", model: "side-model" }),
|
||||
).resolves.toEqual({ text: "Generation A / runtime-auth-a / Stream A" });
|
||||
expect(runtimeAuthA).toHaveBeenCalledOnce();
|
||||
expect(runtimeAuthB).not.toHaveBeenCalled();
|
||||
expect(streamA).toHaveBeenCalledOnce();
|
||||
expect(streamA).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: "Generation A" }),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(streamB).not.toHaveBeenCalled();
|
||||
expect(streamSimpleMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes Codex-selected BTW questions through the harness side-question hook", async () => {
|
||||
const supports = vi.fn(supportsPreparedOpenAIAuth);
|
||||
const codexSideQuestionMock = registerCodexSideQuestionHarness({
|
||||
@@ -1525,14 +1629,16 @@ describe("runBtwSideQuestion", () => {
|
||||
|
||||
expect(result).toEqual({ text: "Copilot fallback answer." });
|
||||
expect(ensureSelectedAgentHarnessPluginMock).toHaveBeenCalledOnce();
|
||||
expect(ensureSelectedAgentHarnessPluginMock).toHaveBeenCalledWith({
|
||||
provider: "github-copilot",
|
||||
modelId: "gpt-4o",
|
||||
config: expect.any(Object),
|
||||
agentId: "main",
|
||||
sessionKey: DEFAULT_SESSION_KEY,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
expect(ensureSelectedAgentHarnessPluginMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "github-copilot",
|
||||
modelId: "gpt-4o",
|
||||
config: expect.any(Object),
|
||||
agentId: "main",
|
||||
sessionKey: DEFAULT_SESSION_KEY,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
}),
|
||||
);
|
||||
expect(streamSimpleMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -1774,6 +1880,13 @@ describe("runBtwSideQuestion", () => {
|
||||
});
|
||||
requireApiKeyMock.mockReturnValueOnce("claude-cli-access");
|
||||
resolveSessionAuthProfileOverrideMock.mockResolvedValueOnce(undefined);
|
||||
resolveModelAsyncMock.mockResolvedValueOnce({
|
||||
model: {
|
||||
provider: DEFAULT_PROVIDER,
|
||||
id: DEFAULT_MODEL,
|
||||
api: "anthropic-messages",
|
||||
},
|
||||
});
|
||||
mockDoneAnswer("Claude CLI answer.");
|
||||
|
||||
const result = await runSideQuestion();
|
||||
@@ -1787,10 +1900,12 @@ describe("runBtwSideQuestion", () => {
|
||||
externalCliProviderIds: ["claude-cli"],
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
expectRecordFields(mockArg(getApiKeyForModelMock, 0, 0), {
|
||||
profileId: "anthropic:claude-cli",
|
||||
store: claudeAuthStore,
|
||||
});
|
||||
expect(getApiKeyForModelMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
profileId: "anthropic:claude-cli",
|
||||
store: claudeAuthStore,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rematerializes the direct model when automatic auth rotates to a SecretRef backup", async () => {
|
||||
|
||||
+635
-622
File diff suppressed because it is too large
Load Diff
@@ -33,10 +33,7 @@ import {
|
||||
resolveProviderRequestTimeoutMs,
|
||||
resolveProviderTransport,
|
||||
} from "./model.provider-hooks.js";
|
||||
import {
|
||||
resolveBundledStaticCatalogModel,
|
||||
type ManifestModelCatalogProviderAliasMetadata,
|
||||
} from "./model.static-catalog.js";
|
||||
import type { ManifestModelCatalogProviderAliasMetadata } from "./model.static-catalog.js";
|
||||
|
||||
export function buildConfiguredFallbackModel(params: {
|
||||
provider: string;
|
||||
@@ -45,6 +42,7 @@ export function buildConfiguredFallbackModel(params: {
|
||||
agentDir?: string;
|
||||
manifestAlias: ManifestModelCatalogProviderAliasMetadata;
|
||||
providerMetadataOwners?: PluginMetadataSnapshotOwnerMaps;
|
||||
getStaticCatalogModel?: () => StaticCatalogFallbackModel | undefined;
|
||||
workspaceDir?: string;
|
||||
runtimeHooks?: ProviderRuntimeHooks;
|
||||
}): Model | undefined {
|
||||
@@ -55,13 +53,7 @@ export function buildConfiguredFallbackModel(params: {
|
||||
if (!hasConfiguredFallbackSurface({ providerConfig, configuredModel, modelId })) {
|
||||
return undefined;
|
||||
}
|
||||
const staticCatalogModel = resolveBundledStaticCatalogModel({
|
||||
provider,
|
||||
modelId,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
}) as StaticCatalogFallbackModel | undefined;
|
||||
const staticCatalogModel = params.getStaticCatalogModel?.();
|
||||
const metadataModel = configuredModel ?? staticCatalogModel;
|
||||
const fallbackMediaInput = mergeModelMediaInput(
|
||||
staticCatalogModel?.mediaInput,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { asOptionalRecord as readModelParams } from "@openclaw/normalization-core/record-coerce";
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { ModelCompatConfig, ModelMediaInputConfig } from "../../config/types.models.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { Api, Model } from "../../llm/types.js";
|
||||
import type { PluginMetadataSnapshotOwnerMaps } from "../../plugins/plugin-metadata-snapshot.types.js";
|
||||
@@ -38,17 +37,9 @@ import {
|
||||
resolveProviderRequestTimeoutMs,
|
||||
resolveProviderTransport,
|
||||
} from "./model.provider-hooks.js";
|
||||
import {
|
||||
resolveBundledStaticCatalogModel,
|
||||
type ManifestModelCatalogProviderAliasMetadata,
|
||||
} from "./model.static-catalog.js";
|
||||
import type { ManifestModelCatalogProviderAliasMetadata } from "./model.static-catalog.js";
|
||||
|
||||
export type StaticCatalogFallbackModel = Model & {
|
||||
compat?: ModelCompatConfig;
|
||||
contextTokens?: number;
|
||||
params?: Record<string, unknown>;
|
||||
mediaInput?: ModelMediaInputConfig;
|
||||
};
|
||||
export type StaticCatalogFallbackModel = ProviderRuntimeModel;
|
||||
|
||||
export function shouldSuppressConfiguredModel(params: {
|
||||
provider: string;
|
||||
@@ -332,6 +323,7 @@ export function applyConfiguredProviderOverrides(params: {
|
||||
preferDiscoveredModelMetadata?: boolean;
|
||||
preferDiscoveredTransport?: boolean;
|
||||
staticCatalogModel?: StaticCatalogFallbackModel;
|
||||
getStaticCatalogModel?: () => ProviderRuntimeModel | undefined;
|
||||
workspaceDir?: string;
|
||||
}): ProviderRuntimeModel {
|
||||
const { providerConfig, modelId } = params;
|
||||
@@ -397,15 +389,7 @@ export function applyConfiguredProviderOverrides(params: {
|
||||
? findConfiguredProviderModel(providerConfig, params.provider, discoveredModel.id)
|
||||
: undefined);
|
||||
const configuredStaticCatalogModel =
|
||||
configuredModel &&
|
||||
(params.staticCatalogModel ??
|
||||
(resolveBundledStaticCatalogModel({
|
||||
provider: params.provider,
|
||||
modelId,
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
}) as StaticCatalogFallbackModel | undefined));
|
||||
configuredModel && (params.staticCatalogModel ?? params.getStaticCatalogModel?.());
|
||||
const metadataOverrideModel =
|
||||
params.preferDiscoveredModelMetadata && isModelsAddMetadataModel({ model: configuredModel })
|
||||
? undefined
|
||||
|
||||
+32
-12
@@ -2,6 +2,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ModelProviderConfig, OpenClawConfig } from "../../config/config.js";
|
||||
import { discoverModels } from "../agent-model-discovery.js";
|
||||
import type { PreparedModelRuntimeSnapshot } from "../prepared-model-runtime.js";
|
||||
import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js";
|
||||
|
||||
vi.mock("../../plugins/provider-runtime.js", () => ({
|
||||
@@ -82,23 +83,42 @@ vi.mock("../model-suppression.js", () => ({
|
||||
|
||||
vi.mock("../prepared-model-runtime.js", async () => {
|
||||
const discovery = await import("../agent-model-discovery.js");
|
||||
const { createPluginMetadataSnapshot } =
|
||||
await import("../../config/plugin-auto-enable.test-helpers.js");
|
||||
const createSnapshot = (input: {
|
||||
agentDir: string;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
}) => ({
|
||||
createStores: () => {
|
||||
const authStorage = discovery.discoverAuthStorage(input.agentDir);
|
||||
const modelRegistry = discovery.discoverModels(authStorage, input.agentDir, {
|
||||
...(input.config ? { config: input.config } : {}),
|
||||
}) => {
|
||||
const config = input.config ?? {};
|
||||
return {
|
||||
agentDir: input.agentDir,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
activeProjectKeys: [],
|
||||
allowGatewaySubagentBinding: false,
|
||||
config,
|
||||
authModes: {},
|
||||
metadataSnapshot: createPluginMetadataSnapshot({
|
||||
config,
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
});
|
||||
if (!("fork" in modelRegistry)) {
|
||||
Object.assign(modelRegistry, { fork: () => modelRegistry });
|
||||
}
|
||||
return { authStorage, modelRegistry };
|
||||
},
|
||||
});
|
||||
}),
|
||||
modelCatalog: { entries: [], routeVariants: [] },
|
||||
configuredRuntimeModels: [],
|
||||
inlineProviderModels: [],
|
||||
createStores: () => {
|
||||
const authStorage = discovery.discoverAuthStorage(input.agentDir);
|
||||
const modelRegistry = discovery.discoverModels(authStorage, input.agentDir, {
|
||||
...(input.config ? { config: input.config } : {}),
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
});
|
||||
if (!("fork" in modelRegistry)) {
|
||||
Object.assign(modelRegistry, { fork: () => modelRegistry });
|
||||
}
|
||||
return { authStorage, modelRegistry };
|
||||
},
|
||||
} satisfies PreparedModelRuntimeSnapshot;
|
||||
};
|
||||
return {
|
||||
getPreparedModelRuntimeSnapshot: createSnapshot,
|
||||
loadPreparedModelRuntimeSnapshot: async (input: Parameters<typeof createSnapshot>[0]) =>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import { vi } from "vitest";
|
||||
import { createPluginMetadataSnapshot } from "../../config/plugin-auto-enable.test-helpers.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { setCurrentPluginMetadataSnapshot } from "../../plugins/current-plugin-metadata-snapshot.js";
|
||||
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||
import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import type { PreparedModelRuntimeSnapshot } from "../prepared-model-runtime.js";
|
||||
import { AuthStorage, ModelRegistry } from "../sessions/index.js";
|
||||
|
||||
const GENERATION_MODEL_ID = "generation-model";
|
||||
const GENERATION_REQUEST_PROVIDER = "generation-alias";
|
||||
export const GENERATION_WORKSPACE_DIR = "/tmp/openclaw-model-generation-scope";
|
||||
|
||||
type ImagePolicy = NonNullable<NonNullable<ProviderRuntimeModel["mediaInput"]>["image"]>;
|
||||
|
||||
export function createModelGenerationFixture(params: {
|
||||
config: OpenClawConfig;
|
||||
createStores?: PreparedModelRuntimeSnapshot["createStores"];
|
||||
label: string;
|
||||
modelId?: string;
|
||||
prepareDynamicModel?: () => Promise<void>;
|
||||
provider?: string;
|
||||
requestProvider?: string;
|
||||
runtimeApi?: ProviderRuntimeModel["api"];
|
||||
runtimeBaseUrl?: string;
|
||||
runtimeImagePolicy?: ImagePolicy;
|
||||
runtimeAugment?: boolean;
|
||||
staticImagePolicy?: ImagePolicy;
|
||||
suppress?: boolean;
|
||||
withRegistry?: boolean;
|
||||
}) {
|
||||
const provider = params.provider ?? `generation-${params.label}`;
|
||||
const requestProvider = params.requestProvider ?? GENERATION_REQUEST_PROVIDER;
|
||||
const modelId = params.modelId ?? GENERATION_MODEL_ID;
|
||||
const staticImagePolicy = params.staticImagePolicy ?? {
|
||||
maxSidePx: params.label === "a" ? 1_111 : 2_222,
|
||||
};
|
||||
const plugin = {
|
||||
id: `generation-plugin-${params.label}`,
|
||||
enabledByDefault: true,
|
||||
channels: [],
|
||||
providers: [provider],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
origin: "bundled",
|
||||
rootDir: `/tmp/generation-plugin-${params.label}`,
|
||||
source: `/tmp/generation-plugin-${params.label}/index.js`,
|
||||
manifestPath: `/tmp/generation-plugin-${params.label}/openclaw.plugin.json`,
|
||||
modelCatalog: {
|
||||
...(requestProvider === provider ? {} : { aliases: { [requestProvider]: { provider } } }),
|
||||
...(params.runtimeAugment === undefined ? {} : { runtimeAugment: params.runtimeAugment }),
|
||||
discovery: { [provider]: "static" },
|
||||
providers: {
|
||||
[provider]: {
|
||||
api: "openai-completions",
|
||||
baseUrl: `https://${provider}.example.test/v1`,
|
||||
models: [
|
||||
{
|
||||
id: modelId,
|
||||
name: `Static ${params.label.toUpperCase()}`,
|
||||
mediaInput: { image: staticImagePolicy },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
...(params.suppress ? { suppressions: [{ provider, model: modelId }] } : {}),
|
||||
},
|
||||
} satisfies PluginManifestRecord;
|
||||
const metadataSnapshot = createPluginMetadataSnapshot({
|
||||
config: params.config,
|
||||
manifestRegistry: { plugins: [plugin], diagnostics: [] },
|
||||
workspaceDir: GENERATION_WORKSPACE_DIR,
|
||||
});
|
||||
const pluginRegistry = createEmptyPluginRegistry();
|
||||
const resolveDynamicModel = vi.fn(() => ({
|
||||
id: modelId,
|
||||
name: `Runtime ${params.label.toUpperCase()}`,
|
||||
provider,
|
||||
api: params.runtimeApi ?? ("openai-completions" as const),
|
||||
baseUrl: params.runtimeBaseUrl ?? `https://${provider}.example.test/v1`,
|
||||
reasoning: false,
|
||||
input: ["text" as const],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 8_192,
|
||||
maxTokens: 2_048,
|
||||
...(params.runtimeImagePolicy ? { mediaInput: { image: params.runtimeImagePolicy } } : {}),
|
||||
}));
|
||||
pluginRegistry.providers.push({
|
||||
pluginId: plugin.id,
|
||||
source: plugin.source,
|
||||
provider: {
|
||||
id: provider,
|
||||
label: `Generation ${params.label.toUpperCase()}`,
|
||||
auth: [],
|
||||
...(params.prepareDynamicModel ? { prepareDynamicModel: params.prepareDynamicModel } : {}),
|
||||
resolveDynamicModel,
|
||||
},
|
||||
});
|
||||
const createStores =
|
||||
params.createStores ??
|
||||
(() => {
|
||||
const authStorage = AuthStorage.inMemory({});
|
||||
return { authStorage, modelRegistry: ModelRegistry.inMemory(authStorage) };
|
||||
});
|
||||
const preparedModelRuntime = {
|
||||
agentDir: "/tmp/openclaw-model-generation-agent",
|
||||
workspaceDir: GENERATION_WORKSPACE_DIR,
|
||||
activeProjectKeys: [],
|
||||
config: params.config,
|
||||
authModes: {},
|
||||
metadataSnapshot,
|
||||
...(params.withRegistry === false ? {} : { pluginRegistry }),
|
||||
allowGatewaySubagentBinding: false,
|
||||
modelCatalog: { entries: [], routeVariants: [] },
|
||||
configuredRuntimeModels: [],
|
||||
inlineProviderModels: [],
|
||||
createStores,
|
||||
} satisfies PreparedModelRuntimeSnapshot;
|
||||
return {
|
||||
metadataSnapshot,
|
||||
modelId,
|
||||
pluginRegistry,
|
||||
preparedModelRuntime,
|
||||
provider,
|
||||
requestProvider,
|
||||
resolveDynamicModel,
|
||||
runtimeImagePolicy: params.runtimeImagePolicy,
|
||||
staticImagePolicy,
|
||||
};
|
||||
}
|
||||
|
||||
export function publishCurrentModelGeneration(
|
||||
generation: ReturnType<typeof createModelGenerationFixture>,
|
||||
): void {
|
||||
setCurrentPluginMetadataSnapshot(generation.metadataSnapshot, {
|
||||
config: generation.preparedModelRuntime.config,
|
||||
workspaceDir: GENERATION_WORKSPACE_DIR,
|
||||
});
|
||||
setActivePluginRegistry(
|
||||
generation.pluginRegistry,
|
||||
`generation-${generation.provider}`,
|
||||
"default",
|
||||
GENERATION_WORKSPACE_DIR,
|
||||
);
|
||||
}
|
||||
|
||||
export function resetModelGenerationFixtureState(): void {
|
||||
setCurrentPluginMetadataSnapshot(undefined);
|
||||
resetPluginRuntimeStateForTest();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/generation-scope.js";
|
||||
import {
|
||||
createModelGenerationFixture,
|
||||
publishCurrentModelGeneration,
|
||||
resetModelGenerationFixtureState,
|
||||
} from "./model.generation-scope.test-support.js";
|
||||
import { resolveModel, resolveModelAsync } from "./model.js";
|
||||
|
||||
async function resolveGeneration(generation: ReturnType<typeof createModelGenerationFixture>) {
|
||||
const { preparedModelRuntime } = generation;
|
||||
const stores = preparedModelRuntime.createStores();
|
||||
return await resolveModelAsync(
|
||||
generation.requestProvider,
|
||||
generation.modelId,
|
||||
preparedModelRuntime.agentDir,
|
||||
preparedModelRuntime.config,
|
||||
{
|
||||
...stores,
|
||||
allowBundledStaticCatalogFallback: true,
|
||||
preparedModelRuntime,
|
||||
skipAgentDiscovery: true,
|
||||
workspaceDir: preparedModelRuntime.workspaceDir,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("model runtime generation scope", () => {
|
||||
beforeEach(() => {
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetModelGenerationFixtureState();
|
||||
});
|
||||
|
||||
it("keeps alias, suppression, static metadata, and runtime hooks on the prepared generation", async () => {
|
||||
const config = {} satisfies OpenClawConfig;
|
||||
const generationA = createModelGenerationFixture({ config, label: "a" });
|
||||
const generationB = createModelGenerationFixture({ config, label: "b", suppress: true });
|
||||
publishCurrentModelGeneration(generationB);
|
||||
|
||||
const result = await resolveGeneration(generationA);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model).toMatchObject({
|
||||
provider: generationA.provider,
|
||||
name: "Runtime A",
|
||||
mediaInput: { image: generationA.staticImagePolicy },
|
||||
});
|
||||
expect(generationA.resolveDynamicModel).toHaveBeenCalled();
|
||||
expect(generationB.resolveDynamicModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps concurrent prepared generations isolated across awaited runtime hooks", async () => {
|
||||
const config = {} satisfies OpenClawConfig;
|
||||
let arrivals = 0;
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const prepareDynamicModel = async () => {
|
||||
arrivals += 1;
|
||||
if (arrivals === 2) {
|
||||
release();
|
||||
}
|
||||
await gate;
|
||||
};
|
||||
const generationA = createModelGenerationFixture({
|
||||
config,
|
||||
label: "a",
|
||||
prepareDynamicModel,
|
||||
});
|
||||
const generationB = createModelGenerationFixture({
|
||||
config,
|
||||
label: "b",
|
||||
prepareDynamicModel,
|
||||
});
|
||||
publishCurrentModelGeneration(generationB);
|
||||
|
||||
const [resultA, resultB] = await Promise.all([
|
||||
resolveGeneration(generationA),
|
||||
resolveGeneration(generationB),
|
||||
]);
|
||||
|
||||
expect(resultA.model).toMatchObject({
|
||||
provider: generationA.provider,
|
||||
name: "Runtime A",
|
||||
mediaInput: { image: generationA.staticImagePolicy },
|
||||
});
|
||||
expect(resultB.model).toMatchObject({
|
||||
provider: generationB.provider,
|
||||
name: "Runtime B",
|
||||
mediaInput: { image: generationB.staticImagePolicy },
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps metadata-only prepared generations from borrowing current runtime hooks", async () => {
|
||||
const config = {} satisfies OpenClawConfig;
|
||||
const generationA = createModelGenerationFixture({
|
||||
config,
|
||||
label: "a",
|
||||
withRegistry: false,
|
||||
});
|
||||
const generationB = createModelGenerationFixture({ config, label: "b" });
|
||||
publishCurrentModelGeneration(generationB);
|
||||
|
||||
const result = await resolveGeneration(generationA);
|
||||
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.model).toMatchObject({
|
||||
provider: generationA.provider,
|
||||
name: "Static A",
|
||||
mediaInput: { image: generationA.staticImagePolicy },
|
||||
});
|
||||
expect(generationB.resolveDynamicModel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps synchronous resolution on the exact scoped generation", () => {
|
||||
const config = {} satisfies OpenClawConfig;
|
||||
const generationA = createModelGenerationFixture({ config, label: "a" });
|
||||
const generationB = createModelGenerationFixture({ config, label: "b" });
|
||||
publishCurrentModelGeneration(generationB);
|
||||
const stores = generationA.preparedModelRuntime.createStores();
|
||||
|
||||
const result = withPluginRuntimeGenerationScope(generationA.preparedModelRuntime, () =>
|
||||
resolveModel(
|
||||
generationA.requestProvider,
|
||||
generationA.modelId,
|
||||
generationA.preparedModelRuntime.agentDir,
|
||||
config,
|
||||
{
|
||||
...stores,
|
||||
workspaceDir: generationA.preparedModelRuntime.workspaceDir,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.model).toMatchObject({
|
||||
provider: generationA.provider,
|
||||
name: "Runtime A",
|
||||
});
|
||||
expect(generationB.resolveDynamicModel).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,7 @@ export function resolveExplicitModelWithRegistry(params: {
|
||||
workspaceDir?: string;
|
||||
runtimeHooks?: ProviderRuntimeHooks;
|
||||
preparedInlineProviderModels?: readonly InlineModelEntry[];
|
||||
preparedStaticCatalogModel?: StaticCatalogFallbackModel;
|
||||
getStaticCatalogModel?: () => StaticCatalogFallbackModel | undefined;
|
||||
}): ExplicitModelResolution | undefined {
|
||||
const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir, runtimeHooks } = params;
|
||||
const providerMetadataOwners = getRegistryProviderMetadataOwners(modelRegistry);
|
||||
@@ -91,15 +91,7 @@ export function resolveExplicitModelWithRegistry(params: {
|
||||
) {
|
||||
return { kind: "suppressed" };
|
||||
}
|
||||
const staticCatalogModel =
|
||||
params.preparedStaticCatalogModel ??
|
||||
(resolveBundledStaticCatalogModel({
|
||||
provider,
|
||||
modelId,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
}) as StaticCatalogFallbackModel | undefined);
|
||||
const staticCatalogModel = params.getStaticCatalogModel?.();
|
||||
return {
|
||||
kind: "resolved",
|
||||
source: "configured",
|
||||
@@ -176,6 +168,7 @@ export function resolveExplicitModelWithRegistry(params: {
|
||||
manifestAlias: params.manifestAlias,
|
||||
providerMetadataOwners,
|
||||
runtimeHooks,
|
||||
getStaticCatalogModel: params.getStaticCatalogModel,
|
||||
workspaceDir,
|
||||
}),
|
||||
runtimeHooks,
|
||||
@@ -272,6 +265,7 @@ function resolvePluginDynamicModelWithRegistry(params: {
|
||||
authProfileMode?: AuthProfileCredential["type"] | "aws-sdk";
|
||||
preferredProfile?: string;
|
||||
runtimeHooks?: ProviderRuntimeHooks;
|
||||
getStaticCatalogModel?: () => StaticCatalogFallbackModel | undefined;
|
||||
}): Model | undefined {
|
||||
const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir } = params;
|
||||
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
|
||||
@@ -330,6 +324,7 @@ function resolvePluginDynamicModelWithRegistry(params: {
|
||||
runtimeHooks,
|
||||
workspaceDir,
|
||||
preferDiscoveredModelMetadata,
|
||||
getStaticCatalogModel: params.getStaticCatalogModel,
|
||||
});
|
||||
return normalizeResolvedModel({
|
||||
provider,
|
||||
@@ -342,9 +337,7 @@ function resolvePluginDynamicModelWithRegistry(params: {
|
||||
}
|
||||
|
||||
export function resolveRuntimePreferredSuppressedModel(
|
||||
params: ResolveModelWithRegistryParams & {
|
||||
manifestAlias: ManifestModelCatalogProviderAliasMetadata;
|
||||
},
|
||||
params: ResolveModelWithPreparedRegistryParams,
|
||||
): Model | undefined {
|
||||
const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS;
|
||||
if (!shouldCompareProviderRuntimeResolvedModel({ ...params, runtimeHooks })) {
|
||||
@@ -430,10 +423,13 @@ type ResolveModelWithRegistryParams = {
|
||||
skipConfiguredFallback?: boolean;
|
||||
};
|
||||
|
||||
type ResolveModelWithPreparedRegistryParams = ResolveModelWithRegistryParams & {
|
||||
manifestAlias: ManifestModelCatalogProviderAliasMetadata;
|
||||
getStaticCatalogModel?: () => StaticCatalogFallbackModel | undefined;
|
||||
};
|
||||
|
||||
export function resolveModelWithPreparedRegistry(
|
||||
params: ResolveModelWithRegistryParams & {
|
||||
manifestAlias: ManifestModelCatalogProviderAliasMetadata;
|
||||
},
|
||||
params: ResolveModelWithPreparedRegistryParams,
|
||||
): Model | undefined {
|
||||
// Competing activated owners leave credentials and transport authority unresolved.
|
||||
// Refuse the route before configured fallbacks can accidentally select either owner.
|
||||
@@ -477,11 +473,27 @@ export function resolveModelWithRegistry(
|
||||
): Model | undefined {
|
||||
const workspaceDir = params.workspaceDir ?? params.cfg?.agents?.defaults?.workspace;
|
||||
const normalizedRef = normalizeProviderModelRef({ ...params, workspaceDir });
|
||||
let staticCatalogResolved = false;
|
||||
let staticCatalogModel: StaticCatalogFallbackModel | undefined;
|
||||
const getStaticCatalogModel = () => {
|
||||
if (!staticCatalogResolved) {
|
||||
staticCatalogResolved = true;
|
||||
staticCatalogModel = resolveBundledStaticCatalogModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg: params.cfg,
|
||||
workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
});
|
||||
}
|
||||
return staticCatalogModel;
|
||||
};
|
||||
return resolveModelWithPreparedRegistry({
|
||||
...params,
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
getStaticCatalogModel,
|
||||
...(workspaceDir !== undefined ? { workspaceDir } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,7 +191,14 @@ vi.mock("../prepared-model-runtime.js", async () => {
|
||||
Object.assign(modelRegistry, { fork: () => modelRegistry });
|
||||
}
|
||||
const snapshot = {
|
||||
agentDir: input.agentDir,
|
||||
...(workspaceDir ? { workspaceDir } : {}),
|
||||
activeProjectKeys: [],
|
||||
config: input.config ?? {},
|
||||
authModes: {},
|
||||
metadataSnapshot: { plugins: [] },
|
||||
allowGatewaySubagentBinding: false,
|
||||
modelCatalog: { entries: [], routeVariants: [] },
|
||||
configuredRuntimeModels: preparedSnapshotState.configuredRuntimeModels,
|
||||
inlineProviderModels: preparedSnapshotState.inlineProviderModels,
|
||||
createStores: () => ({ authStorage, modelRegistry }),
|
||||
@@ -1009,6 +1016,18 @@ describe("resolveModel", () => {
|
||||
makeMistralCatalogModel({ input: ["text"] }),
|
||||
);
|
||||
|
||||
const preparedModelRuntime = {
|
||||
agentDir: "/tmp/agent",
|
||||
activeProjectKeys: [],
|
||||
allowGatewaySubagentBinding: false,
|
||||
config: {},
|
||||
authModes: {},
|
||||
metadataSnapshot: { plugins: [] } as never,
|
||||
modelCatalog: { entries: [], routeVariants: [] },
|
||||
configuredRuntimeModels: [],
|
||||
inlineProviderModels: [],
|
||||
createStores: () => ({ authStorage: {} as never, modelRegistry: {} as never }),
|
||||
} satisfies PreparedModelRuntimeSnapshot;
|
||||
const result = await resolveModelAsync(
|
||||
"mistral",
|
||||
"mistral-medium-3-5",
|
||||
@@ -1018,7 +1037,7 @@ describe("resolveModel", () => {
|
||||
allowBundledStaticCatalogFallback: true,
|
||||
authStorage: { mocked: true } as never,
|
||||
modelRegistry: { find: vi.fn(() => null) } as never,
|
||||
preparedModelRuntime: {} as PreparedModelRuntimeSnapshot,
|
||||
preparedModelRuntime,
|
||||
runtimeHooks: createRuntimeHooks(),
|
||||
skipAgentDiscovery: true,
|
||||
},
|
||||
@@ -1594,7 +1613,7 @@ describe("resolveModel", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not use bundled static catalog rows unless the caller opts in", async () => {
|
||||
it("does not read manifest or provider static rows when bundled fallback is disabled", async () => {
|
||||
const result = await resolveModelAsync(
|
||||
"mistral",
|
||||
"mistral-medium-3-5",
|
||||
@@ -1609,6 +1628,7 @@ describe("resolveModel", () => {
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe("Unknown model: mistral/mistral-medium-3-5");
|
||||
expect(resolveBundledStaticCatalogModelMock).not.toHaveBeenCalled();
|
||||
expect(resolveBundledProviderStaticCatalogModelMock).not.toHaveBeenCalled();
|
||||
expect(discoverAuthStorage).not.toHaveBeenCalled();
|
||||
expect(discoverModels).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -3056,6 +3076,8 @@ describe("resolveModel", () => {
|
||||
|
||||
expect(result.model).toBeUndefined();
|
||||
expect(result.error).toBe("Unknown model: azure-openai-responses/gpt-5.5");
|
||||
expect(resolveBundledStaticCatalogModelMock).not.toHaveBeenCalled();
|
||||
expect(resolveBundledProviderStaticCatalogModelMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { Model } from "../../llm/types.js";
|
||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||
import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/generation-scope.js";
|
||||
import { resolveDefaultAgentDir } from "../agent-scope.js";
|
||||
import type { AuthProfileCredential } from "../auth-profiles/types.js";
|
||||
import { resolveLegacyInheritedAuthDir } from "../legacy-inherited-auth-dir.js";
|
||||
@@ -25,6 +26,7 @@ import { buildConfiguredFallbackModel } from "./model.configured-fallback.js";
|
||||
import {
|
||||
applyConfiguredProviderOverrides,
|
||||
resolveConfiguredProviderConfig,
|
||||
type StaticCatalogFallbackModel,
|
||||
} from "./model.configured-overrides.js";
|
||||
import {
|
||||
DEFAULT_PROVIDER_RUNTIME_HOOKS,
|
||||
@@ -144,45 +146,70 @@ export function resolveModel(
|
||||
`prepared model runtime is not published for synchronous model resolution (${resolvedAgentDir}); use resolveModelAsync before lifecycle publication`,
|
||||
);
|
||||
}
|
||||
const workspaceDir =
|
||||
options?.workspaceDir ?? preparedSnapshot?.workspaceDir ?? derivedWorkspaceDir;
|
||||
const normalizedRef = normalizeProviderModelRef({ provider, modelId, cfg, workspaceDir });
|
||||
const preparedStores = preparedSnapshot?.createStores();
|
||||
const authStorage = options?.authStorage ?? preparedStores!.authStorage;
|
||||
const modelRegistry =
|
||||
options?.modelRegistry ??
|
||||
(options?.authStorage
|
||||
? preparedStores!.modelRegistry.fork(authStorage)
|
||||
: preparedStores!.modelRegistry);
|
||||
const runtimeHooks = resolveRuntimeHooks(options);
|
||||
const model = resolveModelWithPreparedRegistry({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileMode: options?.authProfileMode,
|
||||
preferredProfile: options?.preferredProfile,
|
||||
runtimeHooks,
|
||||
});
|
||||
if (model) {
|
||||
return { model, authStorage, modelRegistry };
|
||||
}
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
const preparedModelRuntime = preparedSnapshot;
|
||||
const resolve = () => {
|
||||
const workspaceDir =
|
||||
options?.workspaceDir ?? preparedModelRuntime?.workspaceDir ?? derivedWorkspaceDir;
|
||||
const normalizedRef = normalizeProviderModelRef({ provider, modelId, cfg, workspaceDir });
|
||||
const preparedStores =
|
||||
!options?.authStorage || !options?.modelRegistry
|
||||
? preparedModelRuntime?.createStores()
|
||||
: undefined;
|
||||
const authStorage = options?.authStorage ?? preparedStores!.authStorage;
|
||||
const modelRegistry =
|
||||
options?.modelRegistry ??
|
||||
(options?.authStorage
|
||||
? preparedStores!.modelRegistry.fork(authStorage)
|
||||
: preparedStores!.modelRegistry);
|
||||
const runtimeHooks = resolveRuntimeHooks(options);
|
||||
let staticCatalogResolved = false;
|
||||
let staticCatalogModel: StaticCatalogFallbackModel | undefined;
|
||||
const getStaticCatalogModel = () => {
|
||||
if (!staticCatalogResolved) {
|
||||
staticCatalogResolved = true;
|
||||
staticCatalogModel = resolveBundledStaticCatalogModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
});
|
||||
}
|
||||
return staticCatalogModel;
|
||||
};
|
||||
const model = resolveModelWithPreparedRegistry({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileMode: options?.authProfileMode,
|
||||
preferredProfile: options?.preferredProfile,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
getStaticCatalogModel,
|
||||
});
|
||||
if (model) {
|
||||
return { model, authStorage, modelRegistry };
|
||||
}
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
};
|
||||
};
|
||||
return preparedModelRuntime
|
||||
? withPluginRuntimeGenerationScope(preparedModelRuntime, resolve)
|
||||
: resolve();
|
||||
}
|
||||
|
||||
export async function resolveModelAsync(
|
||||
@@ -203,23 +230,27 @@ export async function resolveModelAsync(
|
||||
options?.workspaceDir,
|
||||
options?.agentId,
|
||||
);
|
||||
const explicitPreparedRuntime = options?.preparedModelRuntime;
|
||||
const emptyDiscoveryStores =
|
||||
options?.skipAgentDiscovery && (!options.authStorage || !options.modelRegistry)
|
||||
? createEmptyAgentDiscoveryStores()
|
||||
: undefined;
|
||||
const publishedSnapshot =
|
||||
!emptyDiscoveryStores && (!options?.authStorage || !options?.modelRegistry)
|
||||
? resolvePreparedAgentSnapshot(
|
||||
resolvedAgentDir,
|
||||
cfg,
|
||||
options?.workspaceDir,
|
||||
derivedWorkspaceDir,
|
||||
options?.agentId,
|
||||
)
|
||||
: undefined;
|
||||
const needsPreparedSnapshot =
|
||||
!explicitPreparedRuntime &&
|
||||
!emptyDiscoveryStores &&
|
||||
(!options?.authStorage || !options?.modelRegistry);
|
||||
const publishedSnapshot = needsPreparedSnapshot
|
||||
? resolvePreparedAgentSnapshot(
|
||||
resolvedAgentDir,
|
||||
cfg,
|
||||
options?.workspaceDir,
|
||||
derivedWorkspaceDir,
|
||||
options?.agentId,
|
||||
)
|
||||
: undefined;
|
||||
const preparedSnapshot =
|
||||
publishedSnapshot ??
|
||||
(!emptyDiscoveryStores && (!options?.authStorage || !options?.modelRegistry)
|
||||
(needsPreparedSnapshot
|
||||
? await loadPreparedModelRuntimeSnapshot({
|
||||
...(options?.agentId ? { agentId: options.agentId } : {}),
|
||||
agentDir: resolvedAgentDir,
|
||||
@@ -228,238 +259,254 @@ export async function resolveModelAsync(
|
||||
...(derivedWorkspaceDir ? { workspaceDir: derivedWorkspaceDir } : {}),
|
||||
})
|
||||
: undefined);
|
||||
const workspaceDir =
|
||||
options?.workspaceDir ?? preparedSnapshot?.workspaceDir ?? derivedWorkspaceDir;
|
||||
const normalizedRef = normalizeProviderModelRef({ provider, modelId, cfg, workspaceDir });
|
||||
const preparedStores = preparedSnapshot?.createStores();
|
||||
const fallbackStores =
|
||||
emptyDiscoveryStores ?? preparedStores ?? createEmptyAgentDiscoveryStores();
|
||||
const authStorage = options?.authStorage ?? fallbackStores.authStorage;
|
||||
const modelRegistry =
|
||||
options?.modelRegistry ??
|
||||
(options?.authStorage
|
||||
? fallbackStores.modelRegistry.fork(authStorage)
|
||||
: fallbackStores.modelRegistry);
|
||||
const runtimeHooks = resolveRuntimeHooks(options);
|
||||
// Route-projected cfg owns transport/auth; the snapshot contributes generation facts only.
|
||||
const preparedModelRuntime = options?.preparedModelRuntime ?? preparedSnapshot;
|
||||
const preparedStaticCatalogModel = preparedModelRuntime?.configuredRuntimeModels?.find(
|
||||
({ modelId: candidateId, provider: rowProvider }) =>
|
||||
staticModelIdMatches({
|
||||
candidateId,
|
||||
rowProvider,
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
}),
|
||||
)?.model;
|
||||
if (normalizedRef.manifestAlias.ambiguous) {
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
const preparedModelRuntime = explicitPreparedRuntime ?? preparedSnapshot;
|
||||
const resolve = async () => {
|
||||
const workspaceDir =
|
||||
options?.workspaceDir ?? preparedModelRuntime?.workspaceDir ?? derivedWorkspaceDir;
|
||||
const normalizedRef = normalizeProviderModelRef({ provider, modelId, cfg, workspaceDir });
|
||||
const preparedStores =
|
||||
!options?.authStorage || !options?.modelRegistry
|
||||
? preparedModelRuntime?.createStores()
|
||||
: undefined;
|
||||
const fallbackStores =
|
||||
emptyDiscoveryStores ?? preparedStores ?? createEmptyAgentDiscoveryStores();
|
||||
const authStorage = options?.authStorage ?? fallbackStores.authStorage;
|
||||
const modelRegistry =
|
||||
options?.modelRegistry ??
|
||||
(options?.authStorage
|
||||
? fallbackStores.modelRegistry.fork(authStorage)
|
||||
: fallbackStores.modelRegistry);
|
||||
const runtimeHooks = resolveRuntimeHooks(options);
|
||||
let staticCatalogResolved = false;
|
||||
let staticCatalogModel: StaticCatalogFallbackModel | undefined;
|
||||
const getManifestStaticCatalogModel = () => {
|
||||
if (!staticCatalogResolved) {
|
||||
staticCatalogResolved = true;
|
||||
staticCatalogModel =
|
||||
preparedModelRuntime?.configuredRuntimeModels?.find(
|
||||
({ modelId: candidateId, provider: rowProvider }) =>
|
||||
staticModelIdMatches({
|
||||
candidateId,
|
||||
rowProvider,
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
}),
|
||||
)?.model ??
|
||||
resolveBundledStaticCatalogModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
...(preparedModelRuntime
|
||||
? { metadataSnapshot: preparedModelRuntime.metadataSnapshot }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
return staticCatalogModel;
|
||||
};
|
||||
}
|
||||
const explicitModel = resolveExplicitModelWithRegistry({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
preparedInlineProviderModels: preparedModelRuntime?.inlineProviderModels,
|
||||
preparedStaticCatalogModel,
|
||||
});
|
||||
if (explicitModel?.kind === "suppressed") {
|
||||
const suppressedRuntimeModel = resolveRuntimePreferredSuppressedModel({
|
||||
if (normalizedRef.manifestAlias.ambiguous) {
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
};
|
||||
}
|
||||
const explicitModel = resolveExplicitModelWithRegistry({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
...(options?.agentRuntimeId ? { agentRuntimeId: options.agentRuntimeId } : {}),
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileMode: options?.authProfileMode,
|
||||
preferredProfile: options?.preferredProfile,
|
||||
runtimeHooks,
|
||||
preparedInlineProviderModels: preparedModelRuntime?.inlineProviderModels,
|
||||
getStaticCatalogModel: getManifestStaticCatalogModel,
|
||||
});
|
||||
if (suppressedRuntimeModel) {
|
||||
return { model: suppressedRuntimeModel, authStorage, modelRegistry };
|
||||
}
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
};
|
||||
}
|
||||
const providerConfig = resolveConfiguredProviderConfig(cfg, normalizedRef.provider);
|
||||
const authProfile = resolveDynamicModelAuthProfile({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileMode: options?.authProfileMode,
|
||||
preferredProfile: options?.preferredProfile,
|
||||
});
|
||||
const preparedMetadataSnapshot = preparedModelRuntime?.metadataSnapshot;
|
||||
let staticCatalogLookup: Promise<ProviderRuntimeModel | undefined> | undefined;
|
||||
const resolveStaticCatalogModel = async () => {
|
||||
if (!options?.allowBundledStaticCatalogFallback) {
|
||||
return undefined;
|
||||
}
|
||||
staticCatalogLookup ??= (async () => {
|
||||
if (preparedStaticCatalogModel) {
|
||||
return preparedStaticCatalogModel;
|
||||
}
|
||||
const manifestModel = resolveBundledStaticCatalogModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
...(preparedMetadataSnapshot ? { metadataSnapshot: preparedMetadataSnapshot } : {}),
|
||||
});
|
||||
if (manifestModel) {
|
||||
return manifestModel;
|
||||
}
|
||||
return await resolveBundledProviderStaticCatalogModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
...(preparedMetadataSnapshot ? { metadataSnapshot: preparedMetadataSnapshot } : {}),
|
||||
});
|
||||
})();
|
||||
return await staticCatalogLookup;
|
||||
};
|
||||
const resolveStaticCatalogFallbackModel = async () => {
|
||||
const catalogModel = await resolveStaticCatalogModel();
|
||||
if (!catalogModel) {
|
||||
return undefined;
|
||||
}
|
||||
const overriddenStaticCatalogModel = applyConfiguredProviderOverrides({
|
||||
provider: normalizedRef.provider,
|
||||
discoveredModel: catalogModel,
|
||||
providerConfig,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
runtimeHooks,
|
||||
workspaceDir,
|
||||
preferDiscoveredModelMetadata: true,
|
||||
preferDiscoveredTransport: options?.preferBundledStaticCatalogTransport,
|
||||
staticCatalogModel: catalogModel,
|
||||
});
|
||||
return normalizeResolvedModel({
|
||||
provider: normalizedRef.provider,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
model: overriddenStaticCatalogModel,
|
||||
runtimeHooks,
|
||||
});
|
||||
};
|
||||
const resolveDynamicAttempt = async () => {
|
||||
await runtimeHooks.prepareProviderDynamicModel({
|
||||
provider: normalizedRef.provider,
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
context: {
|
||||
config: cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
...(options?.agentRuntimeId ? { agentRuntimeId: options.agentRuntimeId } : {}),
|
||||
workspaceDir,
|
||||
if (explicitModel?.kind === "suppressed") {
|
||||
const suppressedRuntimeModel = resolveRuntimePreferredSuppressedModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
providerConfig,
|
||||
...authProfile,
|
||||
},
|
||||
});
|
||||
return resolveModelWithPreparedRegistry({
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
...(options?.agentRuntimeId ? { agentRuntimeId: options.agentRuntimeId } : {}),
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileMode: options?.authProfileMode,
|
||||
preferredProfile: options?.preferredProfile,
|
||||
runtimeHooks,
|
||||
getStaticCatalogModel: getManifestStaticCatalogModel,
|
||||
});
|
||||
if (suppressedRuntimeModel) {
|
||||
return { model: suppressedRuntimeModel, authStorage, modelRegistry };
|
||||
}
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
};
|
||||
}
|
||||
const providerConfig = resolveConfiguredProviderConfig(cfg, normalizedRef.provider);
|
||||
const authProfile = resolveDynamicModelAuthProfile({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
...(options?.agentRuntimeId ? { agentRuntimeId: options.agentRuntimeId } : {}),
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileMode: options?.authProfileMode,
|
||||
preferredProfile: options?.preferredProfile,
|
||||
runtimeHooks,
|
||||
...(options?.allowBundledStaticCatalogFallback ? { skipConfiguredFallback: true } : {}),
|
||||
});
|
||||
};
|
||||
const providerRuntimeMetadataShouldWin = shouldCompareProviderRuntimeResolvedModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
});
|
||||
let model =
|
||||
explicitModel?.kind === "resolved" && !providerRuntimeMetadataShouldWin
|
||||
? explicitModel.model
|
||||
: undefined;
|
||||
model ??= await resolveDynamicAttempt();
|
||||
if (!model && !explicitModel && options?.allowBundledStaticCatalogFallback) {
|
||||
model = await resolveStaticCatalogFallbackModel();
|
||||
}
|
||||
if (!model && !explicitModel && options?.allowBundledStaticCatalogFallback) {
|
||||
model = buildConfiguredFallbackModel({
|
||||
const preparedMetadataSnapshot = preparedModelRuntime?.metadataSnapshot;
|
||||
let providerStaticCatalogLookup: Promise<ProviderRuntimeModel | undefined> | undefined;
|
||||
const resolveStaticCatalogModel = async () => {
|
||||
if (!options?.allowBundledStaticCatalogFallback) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
getManifestStaticCatalogModel() ??
|
||||
(await (providerStaticCatalogLookup ??= resolveBundledProviderStaticCatalogModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
workspaceDir,
|
||||
...(preparedMetadataSnapshot ? { metadataSnapshot: preparedMetadataSnapshot } : {}),
|
||||
})))
|
||||
);
|
||||
};
|
||||
const resolveStaticCatalogFallbackModel = async () => {
|
||||
const catalogModel = await resolveStaticCatalogModel();
|
||||
if (!catalogModel) {
|
||||
return undefined;
|
||||
}
|
||||
const overriddenStaticCatalogModel = applyConfiguredProviderOverrides({
|
||||
provider: normalizedRef.provider,
|
||||
discoveredModel: catalogModel,
|
||||
providerConfig,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
runtimeHooks,
|
||||
workspaceDir,
|
||||
preferDiscoveredModelMetadata: true,
|
||||
preferDiscoveredTransport: options?.preferBundledStaticCatalogTransport,
|
||||
staticCatalogModel: catalogModel,
|
||||
});
|
||||
return normalizeResolvedModel({
|
||||
provider: normalizedRef.provider,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
model: overriddenStaticCatalogModel,
|
||||
runtimeHooks,
|
||||
});
|
||||
};
|
||||
const resolveDynamicAttempt = async () => {
|
||||
await runtimeHooks.prepareProviderDynamicModel({
|
||||
provider: normalizedRef.provider,
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
context: {
|
||||
config: cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
...(options?.agentRuntimeId ? { agentRuntimeId: options.agentRuntimeId } : {}),
|
||||
workspaceDir,
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
providerConfig,
|
||||
...authProfile,
|
||||
},
|
||||
});
|
||||
return resolveModelWithPreparedRegistry({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
modelRegistry,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
...(options?.agentRuntimeId ? { agentRuntimeId: options.agentRuntimeId } : {}),
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
authProfileId: options?.authProfileId,
|
||||
authProfileMode: options?.authProfileMode,
|
||||
preferredProfile: options?.preferredProfile,
|
||||
runtimeHooks,
|
||||
getStaticCatalogModel: getManifestStaticCatalogModel,
|
||||
...(options?.allowBundledStaticCatalogFallback ? { skipConfiguredFallback: true } : {}),
|
||||
});
|
||||
};
|
||||
const providerRuntimeMetadataShouldWin = shouldCompareProviderRuntimeResolvedModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
});
|
||||
}
|
||||
if (model && options?.allowBundledStaticCatalogFallback) {
|
||||
const staticMediaInput = (await resolveStaticCatalogModel())?.mediaInput;
|
||||
const resolvedMediaInput = (model as ProviderRuntimeModel).mediaInput;
|
||||
const mediaInput = mergeModelMediaInput(staticMediaInput, resolvedMediaInput);
|
||||
if (mediaInput) {
|
||||
model = { ...(model as ProviderRuntimeModel), mediaInput } as typeof model;
|
||||
let model =
|
||||
explicitModel?.kind === "resolved" && !providerRuntimeMetadataShouldWin
|
||||
? explicitModel.model
|
||||
: undefined;
|
||||
model ??= await resolveDynamicAttempt();
|
||||
if (!model && !explicitModel && options?.allowBundledStaticCatalogFallback) {
|
||||
model = await resolveStaticCatalogFallbackModel();
|
||||
}
|
||||
}
|
||||
if (model) {
|
||||
return { model, authStorage, modelRegistry };
|
||||
}
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
if (!model && !explicitModel && options?.allowBundledStaticCatalogFallback) {
|
||||
model = buildConfiguredFallbackModel({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
manifestAlias: normalizedRef.manifestAlias,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
getStaticCatalogModel: getManifestStaticCatalogModel,
|
||||
});
|
||||
}
|
||||
if (model && options?.allowBundledStaticCatalogFallback) {
|
||||
const staticMediaInput = (await resolveStaticCatalogModel())?.mediaInput;
|
||||
const resolvedMediaInput = (model as ProviderRuntimeModel).mediaInput;
|
||||
const mediaInput = mergeModelMediaInput(staticMediaInput, resolvedMediaInput);
|
||||
if (mediaInput) {
|
||||
model = { ...(model as ProviderRuntimeModel), mediaInput } as typeof model;
|
||||
}
|
||||
}
|
||||
if (model) {
|
||||
return { model, authStorage, modelRegistry };
|
||||
}
|
||||
return {
|
||||
error: buildUnknownModelError({
|
||||
provider: normalizedRef.provider,
|
||||
modelId: normalizedRef.model,
|
||||
cfg,
|
||||
agentDir: resolvedAgentDir,
|
||||
workspaceDir,
|
||||
runtimeHooks,
|
||||
}),
|
||||
authStorage,
|
||||
modelRegistry,
|
||||
};
|
||||
};
|
||||
return preparedModelRuntime
|
||||
? await withPluginRuntimeGenerationScope(preparedModelRuntime, resolve)
|
||||
: await resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Verifies plugin manifest suppression rules, cache reuse, and lifecycle clears.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPluginMetadataSnapshot } from "../config/plugin-auto-enable.test-helpers.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
buildManifestBuiltInModelSuppressionResolver: vi.fn(),
|
||||
@@ -12,8 +14,12 @@ vi.mock("../plugins/manifest-model-suppression.js", () => ({
|
||||
buildManifestBuiltInModelSuppressionResolver: mocks.buildManifestBuiltInModelSuppressionResolver,
|
||||
}));
|
||||
|
||||
import { setCurrentPluginMetadataSnapshotState } from "../plugins/current-plugin-metadata-state.js";
|
||||
import {
|
||||
getCurrentPluginMetadataSnapshot,
|
||||
setCurrentPluginMetadataSnapshot,
|
||||
} from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../plugins/plugin-metadata-lifecycle.js";
|
||||
import { withPluginRuntimeGenerationScope } from "../plugins/runtime/generation-scope.js";
|
||||
import {
|
||||
buildShouldSuppressBuiltInModelCore,
|
||||
shouldSuppressBuiltInModelCore,
|
||||
@@ -28,6 +34,7 @@ describe("model suppression", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setCurrentPluginMetadataSnapshot(undefined);
|
||||
if (originalBundledPluginsDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
@@ -101,12 +108,20 @@ describe("model suppression", () => {
|
||||
.mockReturnValueOnce(firstResolver)
|
||||
.mockReturnValueOnce(secondResolver);
|
||||
|
||||
setCurrentPluginMetadataSnapshotState({ id: "first" }, undefined);
|
||||
const firstSnapshot = createPluginMetadataSnapshot({
|
||||
config,
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
});
|
||||
const secondSnapshot = createPluginMetadataSnapshot({
|
||||
config,
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
});
|
||||
setCurrentPluginMetadataSnapshot(firstSnapshot, { config });
|
||||
expect(shouldSuppressBuiltInModelCore({ provider: "openai", id: "gpt-5.3", config })).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
setCurrentPluginMetadataSnapshotState({ id: "second" }, undefined);
|
||||
setCurrentPluginMetadataSnapshot(secondSnapshot, { config });
|
||||
expect(shouldSuppressBuiltInModelCore({ provider: "openai", id: "gpt-5.3", config })).toBe(
|
||||
false,
|
||||
);
|
||||
@@ -116,6 +131,60 @@ describe("model suppression", () => {
|
||||
expect(secondResolver).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps concurrent scoped suppression resolvers isolated from process-current metadata", async () => {
|
||||
const config = {} satisfies OpenClawConfig;
|
||||
const snapshotA = createPluginMetadataSnapshot({
|
||||
config,
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
});
|
||||
const snapshotB = createPluginMetadataSnapshot({
|
||||
config,
|
||||
manifestRegistry: { plugins: [], diagnostics: [] },
|
||||
});
|
||||
setCurrentPluginMetadataSnapshot(snapshotB, { config });
|
||||
mocks.buildManifestBuiltInModelSuppressionResolver.mockImplementation(() => {
|
||||
const snapshot = getCurrentPluginMetadataSnapshot({ config, env: process.env });
|
||||
return () =>
|
||||
snapshot === snapshotA ? { suppress: true, errorMessage: "generation A" } : undefined;
|
||||
});
|
||||
let releaseA!: () => void;
|
||||
let markAReady!: () => void;
|
||||
const holdA = new Promise<void>((resolve) => {
|
||||
releaseA = resolve;
|
||||
});
|
||||
const aReady = new Promise<void>((resolve) => {
|
||||
markAReady = resolve;
|
||||
});
|
||||
const resultA = withPluginRuntimeGenerationScope(
|
||||
{ config, metadataSnapshot: snapshotA },
|
||||
async () => {
|
||||
const result = shouldSuppressBuiltInModelCore({
|
||||
provider: "openai",
|
||||
id: "generation-model",
|
||||
config,
|
||||
});
|
||||
markAReady();
|
||||
await holdA;
|
||||
return result;
|
||||
},
|
||||
);
|
||||
await aReady;
|
||||
|
||||
const resultB = await withPluginRuntimeGenerationScope(
|
||||
{ config, metadataSnapshot: snapshotB },
|
||||
async () =>
|
||||
shouldSuppressBuiltInModelCore({
|
||||
provider: "openai",
|
||||
id: "generation-model",
|
||||
config,
|
||||
}),
|
||||
);
|
||||
releaseA();
|
||||
|
||||
await expect(resultA).resolves.toBe(true);
|
||||
expect(resultB).toBe(false);
|
||||
});
|
||||
|
||||
it("refreshes manifest suppression resolver when process env plugin metadata inputs change", () => {
|
||||
const firstResolver = vi.fn().mockReturnValue(undefined);
|
||||
const secondResolver = vi.fn().mockReturnValue(undefined);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../packages/normalization-core/src/string-coerce.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { getCurrentPluginMetadataSnapshotState } from "../plugins/current-plugin-metadata-state.js";
|
||||
import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js";
|
||||
import { buildManifestBuiltInModelSuppressionResolver } from "../plugins/manifest-model-suppression.js";
|
||||
import { resolvePluginControlPlaneFingerprint } from "../plugins/plugin-control-plane-context.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "../plugins/plugin-metadata-lifecycle.js";
|
||||
@@ -46,7 +46,7 @@ function resolveCachedManifestSuppressionResolver(params: {
|
||||
});
|
||||
const cwd = process.cwd();
|
||||
const envFingerprint = resolvePluginMetadataEnvFingerprint(params.env);
|
||||
const metadataSnapshot = getCurrentPluginMetadataSnapshotState().snapshot;
|
||||
const metadataSnapshot = getCurrentPluginMetadataSnapshot(params);
|
||||
if (
|
||||
cached !== undefined &&
|
||||
cached.config === params.config &&
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
MediaUnderstandingProvider,
|
||||
} from "../../plugin-sdk/media-understanding.js";
|
||||
import type { AuthProfileStore } from "../auth-profiles/types.js";
|
||||
import type { resolveBundledStaticCatalogModel } from "../embedded-agent-runner/model.static-catalog.js";
|
||||
import type { PreparedModelRuntimeSnapshot } from "../prepared-model-runtime.js";
|
||||
import type {
|
||||
coerceImageAssistantText,
|
||||
@@ -61,7 +60,6 @@ type ImageToolProviderDeps = {
|
||||
describeImagesWithModel: typeof describeImagesWithModel;
|
||||
resolveAutoMediaKeyProviders: typeof resolveAutoMediaKeyProviders;
|
||||
resolveDefaultMediaModel: typeof resolveDefaultMediaModel;
|
||||
resolveBundledStaticCatalogModel: typeof resolveBundledStaticCatalogModel;
|
||||
resolveModelAsync: ResolveModelAsync;
|
||||
resolveRegisteredMediaUnderstandingProvider(params: {
|
||||
providerId: string;
|
||||
|
||||
@@ -8,7 +8,6 @@ import { isInboundPathAllowed } from "@openclaw/media-core/inbound-path-policy";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { createPluginMetadataSnapshot } from "../../config/plugin-auto-enable.test-helpers.js";
|
||||
import type { ModelDefinitionConfig } from "../../config/types.models.js";
|
||||
import { encodePngRgba, fillPixel } from "../../media/png-encode.js";
|
||||
import type {
|
||||
@@ -16,16 +15,15 @@ import type {
|
||||
ImagesDescriptionRequest,
|
||||
MediaUnderstandingProvider,
|
||||
} from "../../plugin-sdk/media-understanding.js";
|
||||
import {
|
||||
getCurrentPluginMetadataSnapshot,
|
||||
installTemporaryCurrentPluginMetadataSnapshot,
|
||||
} from "../../plugins/current-plugin-metadata-snapshot.js";
|
||||
import type { PluginManifestRecord } from "../../plugins/manifest-registry.js";
|
||||
import { withEnvAsync } from "../../test-utils/env.js";
|
||||
import { withFetchPreconnect } from "../../test-utils/fetch-mock.js";
|
||||
import type { AuthProfileCredential, AuthProfileStore } from "../auth-profiles/types.js";
|
||||
import {
|
||||
createModelGenerationFixture,
|
||||
publishCurrentModelGeneration,
|
||||
resetModelGenerationFixtureState,
|
||||
} from "../embedded-agent-runner/model.generation-scope.test-support.js";
|
||||
import { minimaxUnderstandImage } from "../minimax-vlm.js";
|
||||
import type { PreparedModelRuntimeSnapshot } from "../prepared-model-runtime.js";
|
||||
import {
|
||||
createContainerWorkspaceSandboxFsBridge,
|
||||
createHostSandboxFsBridge,
|
||||
@@ -3191,115 +3189,64 @@ describe("image compression policy", () => {
|
||||
it("keeps runtime augmentation pinned to the prepared plugin generation", async () => {
|
||||
const provider = "prepared-image-provider";
|
||||
const model = "prepared-image-model";
|
||||
const workspaceDir = "/fake/prepared-image-workspace";
|
||||
const cfg = {} satisfies OpenClawConfig;
|
||||
const createSnapshot = (
|
||||
runtimeAugment: boolean,
|
||||
imagePolicy: {
|
||||
maxBytes: number;
|
||||
preferredSidePx: number;
|
||||
tokenMode: "detail" | "provider";
|
||||
},
|
||||
) => {
|
||||
const plugin = {
|
||||
id: "prepared-image-plugin",
|
||||
enabledByDefault: true,
|
||||
channels: [],
|
||||
providers: [provider],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
origin: "bundled",
|
||||
rootDir: "/fake/prepared-image-plugin",
|
||||
source: "/fake/prepared-image-plugin/index.js",
|
||||
manifestPath: "/fake/prepared-image-plugin/openclaw.plugin.json",
|
||||
modelCatalog: {
|
||||
runtimeAugment,
|
||||
discovery: { [provider]: "static" },
|
||||
providers: {
|
||||
[provider]: {
|
||||
baseUrl: "https://prepared-image.example.test/v1",
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: model,
|
||||
name: "Prepared image model",
|
||||
mediaInput: { image: imagePolicy },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies PluginManifestRecord;
|
||||
return createPluginMetadataSnapshot({
|
||||
config: cfg,
|
||||
manifestRegistry: { plugins: [plugin], diagnostics: [] },
|
||||
workspaceDir,
|
||||
});
|
||||
};
|
||||
const preparedSnapshot = createSnapshot(true, {
|
||||
maxBytes: 1_000_000,
|
||||
preferredSidePx: 1_280,
|
||||
tokenMode: "detail",
|
||||
});
|
||||
const currentSnapshot = createSnapshot(false, {
|
||||
maxBytes: 2_000_000,
|
||||
preferredSidePx: 2_560,
|
||||
tokenMode: "provider",
|
||||
});
|
||||
const preparedModelRuntime = {
|
||||
agentDir: "/fake/prepared-image-agent",
|
||||
workspaceDir,
|
||||
activeProjectKeys: [],
|
||||
allowGatewaySubagentBinding: false,
|
||||
const generationA = createModelGenerationFixture({
|
||||
config: cfg,
|
||||
authModes: {},
|
||||
metadataSnapshot: preparedSnapshot,
|
||||
modelCatalog: { entries: [], routeVariants: [] },
|
||||
configuredRuntimeModels: [],
|
||||
inlineProviderModels: [],
|
||||
createStores: () => ({ authStorage: {} as never, modelRegistry: {} as never }),
|
||||
} satisfies PreparedModelRuntimeSnapshot;
|
||||
const modelModule = await import("../embedded-agent-runner/model.js");
|
||||
const resolveModelAsyncSpy = vi.spyOn(modelModule, "resolveModelAsync");
|
||||
label: "image-a",
|
||||
provider,
|
||||
requestProvider: provider,
|
||||
modelId: model,
|
||||
runtimeAugment: true,
|
||||
staticImagePolicy: {
|
||||
maxBytes: 1_000_000,
|
||||
preferredSidePx: 1_280,
|
||||
tokenMode: "detail",
|
||||
},
|
||||
runtimeImagePolicy: { maxSidePx: 1_440 },
|
||||
});
|
||||
const generationB = createModelGenerationFixture({
|
||||
config: cfg,
|
||||
label: "image-b",
|
||||
provider,
|
||||
requestProvider: provider,
|
||||
modelId: model,
|
||||
runtimeAugment: true,
|
||||
staticImagePolicy: {
|
||||
maxBytes: 2_000_000,
|
||||
preferredSidePx: 2_560,
|
||||
tokenMode: "provider",
|
||||
},
|
||||
runtimeImagePolicy: { maxSidePx: 2_880 },
|
||||
});
|
||||
installImageUnderstandingProviderDeps([], {
|
||||
useDefaultResolveModelAsync: true,
|
||||
});
|
||||
const currentLease = installTemporaryCurrentPluginMetadataSnapshot(currentSnapshot, {
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
});
|
||||
publishCurrentModelGeneration(generationB);
|
||||
|
||||
try {
|
||||
expect(getCurrentPluginMetadataSnapshot({ config: cfg, workspaceDir })).toBe(currentSnapshot);
|
||||
await expect(
|
||||
testing.resolveImageCompressionPolicy({
|
||||
cfg,
|
||||
imageModelConfig: { primary: `${provider}/${model}` },
|
||||
imageCount: 1,
|
||||
preparedModelRuntime,
|
||||
workspaceDir,
|
||||
preparedModelRuntime: generationA.preparedModelRuntime,
|
||||
workspaceDir: generationA.preparedModelRuntime.workspaceDir,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
imageCount: 1,
|
||||
models: [
|
||||
{
|
||||
maxSidePx: 1_440,
|
||||
maxBytes: 1_000_000,
|
||||
preferredSidePx: 1_280,
|
||||
tokenMode: "detail",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(resolveModelAsyncSpy).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
resolveModelAsyncSpy.mock.calls.map((call) => call[4]?.skipProviderRuntimeHooks),
|
||||
).toEqual([true, false]);
|
||||
for (const call of resolveModelAsyncSpy.mock.calls) {
|
||||
expect(call[4]?.preparedModelRuntime).toBe(preparedModelRuntime);
|
||||
}
|
||||
expect(generationA.resolveDynamicModel).toHaveBeenCalled();
|
||||
expect(generationB.resolveDynamicModel).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
resolveModelAsyncSpy.mockRestore();
|
||||
currentLease.release();
|
||||
resetModelGenerationFixtureState();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3343,6 +3290,7 @@ describe("image compression policy", () => {
|
||||
});
|
||||
|
||||
it("uses bundled Anthropic media limits without runtime provider hooks", async () => {
|
||||
installImageUnderstandingProviderDeps([], { useDefaultResolveModelAsync: true });
|
||||
await expect(
|
||||
testing.resolveImageCompressionPolicy({
|
||||
cfg: {},
|
||||
|
||||
@@ -35,10 +35,7 @@ import { isManifestPluginAvailableForControlPlane } from "../../plugins/manifest
|
||||
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import type { AuthProfileStore } from "../auth-profiles/types.js";
|
||||
import {
|
||||
bundledStaticCatalogProviderUsesRuntimeAugment,
|
||||
resolveBundledStaticCatalogModel,
|
||||
} from "../embedded-agent-runner/model.static-catalog.js";
|
||||
import { bundledStaticCatalogProviderUsesRuntimeAugment } from "../embedded-agent-runner/model.static-catalog.js";
|
||||
import { isMinimaxVlmProvider } from "../minimax-vlm.js";
|
||||
import {
|
||||
resolveImageFallbackCandidates,
|
||||
@@ -137,7 +134,6 @@ const imageToolProviderDeps = {
|
||||
describeImagesWithModel,
|
||||
resolveAutoMediaKeyProviders,
|
||||
resolveDefaultMediaModel,
|
||||
resolveBundledStaticCatalogModel,
|
||||
resolveModelAsync: resolveModelAsyncDefault,
|
||||
resolveRegisteredMediaUnderstandingProvider,
|
||||
resolveImageCompressionPolicy,
|
||||
@@ -201,7 +197,6 @@ const testing = {
|
||||
describeImagesWithModel?: typeof describeImagesWithModel;
|
||||
resolveAutoMediaKeyProviders?: typeof resolveAutoMediaKeyProviders;
|
||||
resolveDefaultMediaModel?: typeof resolveDefaultMediaModel;
|
||||
resolveBundledStaticCatalogModel?: typeof resolveBundledStaticCatalogModel;
|
||||
resolveModelAsync?: ResolveModelAsync;
|
||||
resolveRegisteredMediaUnderstandingProvider?: typeof resolveRegisteredMediaUnderstandingProvider;
|
||||
resolveImageCompressionPolicy?: typeof resolveImageCompressionPolicy;
|
||||
@@ -219,8 +214,6 @@ const testing = {
|
||||
overrides?.resolveAutoMediaKeyProviders ?? resolveAutoMediaKeyProviders;
|
||||
imageToolProviderDeps.resolveDefaultMediaModel =
|
||||
overrides?.resolveDefaultMediaModel ?? resolveDefaultMediaModel;
|
||||
imageToolProviderDeps.resolveBundledStaticCatalogModel =
|
||||
overrides?.resolveBundledStaticCatalogModel ?? resolveBundledStaticCatalogModel;
|
||||
imageToolProviderDeps.resolveModelAsync =
|
||||
overrides?.resolveModelAsync ?? resolveModelAsyncDefault;
|
||||
imageToolProviderDeps.resolveRegisteredMediaUnderstandingProvider =
|
||||
@@ -468,24 +461,6 @@ function mergeImageCompressionPolicies(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBundledStaticCompressionModelPolicy(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
model: string;
|
||||
workspaceDir?: string;
|
||||
preparedModelRuntime?: PreparedModelRuntimeSnapshot;
|
||||
}): ImageCompressionModelPolicy {
|
||||
const model = imageToolProviderDeps.resolveBundledStaticCatalogModel({
|
||||
provider: params.provider,
|
||||
modelId: params.model,
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
includeRuntimeDiscovery: true,
|
||||
metadataSnapshot: params.preparedModelRuntime?.metadataSnapshot,
|
||||
});
|
||||
return model?.mediaInput?.image ?? {};
|
||||
}
|
||||
|
||||
function providerUsesRuntimeModelAugment(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
@@ -578,14 +553,10 @@ async function resolveCompressionModelPolicy(params: {
|
||||
workspaceDir?: string;
|
||||
preparedModelRuntime?: PreparedModelRuntimeSnapshot;
|
||||
}): Promise<ImageCompressionModelPolicy> {
|
||||
const configuredStaticPolicy = await resolveCompressionModelPolicyWithHooks({
|
||||
const staticPolicy = await resolveCompressionModelPolicyWithHooks({
|
||||
...params,
|
||||
skipProviderRuntimeHooks: true,
|
||||
});
|
||||
const staticPolicy = mergeImageCompressionPolicies({
|
||||
runtimePolicy: resolveBundledStaticCompressionModelPolicy(params),
|
||||
staticPolicy: configuredStaticPolicy,
|
||||
});
|
||||
if (
|
||||
imageCompressionPolicyHasDimensionLimit(staticPolicy) ||
|
||||
!providerUsesRuntimeModelAugment({
|
||||
|
||||
Reference in New Issue
Block a user