mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
perf(agents): keep turn-path model catalog reads off the full live build (#120834)
* perf(agents): keep turn-path model catalog reads off the full live build First agent turns (embedded and cron) resolved thinking capability through loadPreparedModelCatalogSnapshot without readOnly, which materialized the full live model-runtime catalog: ambient synthetic-auth discovery fanned out to every registered provider and loaded plugin discovery modules through jiti source transform (3,172 TS modules, 36s event-loop block, +600MB heap, 58.7s model-selection on a cold gateway). - add loadProviderScopedThinkingCatalog: manifest metadata first, then a provider-scoped read-only static catalog, then scoped live discovery only for runtime-discovery providers (preserves #116584 Ollama semantics) - route scopedLiveProviderDiscovery through the scoped read-only loader - scope live-mode ambient synthetic-auth refs to the requested providers - bound the last-resort synthetic-auth sweep to discovery entry modules - memoize per-turn plugin skill dir resolution/republish (single-slot, lifecycle-cleared; was a full walk + symlink republish every turn) Cold first turn 72.7s -> ~22s wall (remaining cost is provider prefill of the ~19.5k-token default prompt); model-selection 58,726ms -> 124ms. * test(agents): align model-catalog.runtime mocks with scoped thinking catalog seam Explicit vi.mock factories must export every binding prod touches; the new loadProviderScopedThinkingCatalog export is now mocked everywhere the module is stubbed, and the live-model-switch Ollama hydration test asserts the new provider-scoped seam instead of the retired unscoped snapshot call shape. * test(agents): export scoped thinking catalog from every prepared-catalog mock; split synthetic-auth helpers - add loadProviderScopedThinkingCatalog to all explicit prepared-model-catalog and model-catalog.runtime mock factories (vi.mock factories must export every binding prod touches) - move synthetic-auth ref scoping/resolution into prepared-model-runtime.synthetic-auth.ts; keeps facts under the max-lines cap * test(agents): prove scoped thinking hydration for runtime-only models Boundary proof for the ClawSweeper review gap: the three-tier helper stops at manifest or scoped-static when they resolve, and runs provider-scoped live discovery (no broad fanout) only for runtime-only models; cron selection hydrates through the same scoped helper and skips it entirely for thinking=off. * test(agents): accept rest args in scoped thinking catalog mocks
This commit is contained in:
committed by
GitHub
parent
562129d5df
commit
7a8eee4a36
@@ -87,6 +87,7 @@ vi.mock("./model-catalog.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./model-catalog.runtime.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogSnapshot: vi.fn(async () => ({
|
||||
entries: [],
|
||||
routeVariants: [],
|
||||
|
||||
@@ -97,6 +97,7 @@ const state = vi.hoisted(() => ({
|
||||
resolveSupportedThinkingLevelMock: vi.fn(({ level }: { level?: string }) => level),
|
||||
resolveThinkingDefaultMock: vi.fn((_args: unknown) => "low"),
|
||||
loadManifestModelCatalogMock: vi.fn(() => []),
|
||||
loadProviderScopedThinkingCatalogMock: vi.fn((_params: unknown) => undefined),
|
||||
loadPreparedModelCatalogSnapshotMock: vi.fn(
|
||||
async (): Promise<ModelCatalogSnapshot> => ({
|
||||
entries: [],
|
||||
@@ -551,6 +552,11 @@ vi.mock("./model-catalog.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./model-catalog.runtime.js", () => ({
|
||||
// The scoped thinking catalog hydrates from the same runtime snapshot the test controls.
|
||||
loadProviderScopedThinkingCatalog: async (params: unknown) => {
|
||||
state.loadProviderScopedThinkingCatalogMock(params);
|
||||
return (await state.loadPreparedModelCatalogSnapshotMock()).entries;
|
||||
},
|
||||
loadPreparedModelCatalogSnapshot: state.loadPreparedModelCatalogSnapshotMock,
|
||||
}));
|
||||
|
||||
@@ -3523,11 +3529,15 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
allowModelOverride: true,
|
||||
});
|
||||
|
||||
expect(state.loadPreparedModelCatalogSnapshotMock).toHaveBeenCalledWith({
|
||||
config: state.runtimeConfigMock,
|
||||
agentId: "default",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
expect(state.loadProviderScopedThinkingCatalogMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: state.runtimeConfigMock,
|
||||
provider: "ollama",
|
||||
model: "minimax-m3:cloud",
|
||||
agentId: "default",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
}),
|
||||
);
|
||||
const thinkingArgs = requireRecord(
|
||||
mockCallArg(state.isThinkingLevelSupportedMock),
|
||||
"thinking args",
|
||||
|
||||
@@ -96,6 +96,7 @@ vi.mock("../model-catalog.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../model-catalog.runtime.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogSnapshot: vi.fn(async () => ({
|
||||
entries: [],
|
||||
routeVariants: [],
|
||||
|
||||
@@ -512,15 +512,16 @@ export async function resolveEmbeddedModelSelection(params: {
|
||||
primaryConfiguredThinkLevel !== "off" &&
|
||||
!hasResolvedThinkingCatalogEntry({ catalog: catalogForThinking, provider, model })
|
||||
) {
|
||||
const { loadPreparedModelCatalogSnapshot } = await import("../model-catalog.runtime.js");
|
||||
// Thinking capability is a per-model fact; never materialize the full live catalog here.
|
||||
const { loadProviderScopedThinkingCatalog } = await import("../model-catalog.runtime.js");
|
||||
const runtimeCatalog = normalizeThinkingCatalogProviders(
|
||||
(
|
||||
await loadPreparedModelCatalogSnapshot({
|
||||
config: params.cfg,
|
||||
agentId: params.sessionAgentId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
})
|
||||
).entries,
|
||||
await loadProviderScopedThinkingCatalog({
|
||||
config: params.cfg,
|
||||
provider,
|
||||
model,
|
||||
...(params.sessionAgentId ? { agentId: params.sessionAgentId } : {}),
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
}),
|
||||
);
|
||||
const allowedRuntimeCatalog = createModelVisibilityPolicy({
|
||||
cfg: params.cfg,
|
||||
|
||||
@@ -65,6 +65,7 @@ vi.mock("../config/runtime-source-projection.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogOwnerSnapshot: contextTestState.loadModelCatalogOwnerSnapshot,
|
||||
getPublishedPreparedModelCatalogOwnerSnapshot:
|
||||
contextTestState.getPublishedModelCatalogOwnerSnapshot,
|
||||
|
||||
@@ -3,4 +3,5 @@ export { loadManifestModelCatalog } from "./model-catalog.js";
|
||||
export {
|
||||
loadPreparedModelCatalog,
|
||||
loadPreparedModelCatalogSnapshot,
|
||||
loadProviderScopedThinkingCatalog,
|
||||
} from "./prepared-model-catalog.js";
|
||||
|
||||
@@ -66,6 +66,7 @@ const authProfilesMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("./prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogOwnerSnapshot: async (params?: unknown) => ({
|
||||
...(modelCatalogMocks.ownerWorkspaceDir
|
||||
? { workspaceDir: modelCatalogMocks.ownerWorkspaceDir }
|
||||
|
||||
@@ -15,6 +15,7 @@ import { runProviderAuthWarmWorkerInput } from "./model-provider-auth.worker.js"
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
vi.mock("./prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogOwnerSnapshot: vi.fn(
|
||||
async (params: { agentDir: string; agentId?: string; config: OpenClawConfig }) => ({
|
||||
agentDir: params.agentDir,
|
||||
|
||||
@@ -32,6 +32,7 @@ vi.mock("../plugins/current-plugin-metadata-snapshot.js", () => ({
|
||||
|
||||
vi.mock("./model-catalog.runtime.js", () => ({
|
||||
loadManifestModelCatalog: () => [],
|
||||
loadProviderScopedThinkingCatalog: async () => [],
|
||||
loadPreparedModelCatalog: async () => [],
|
||||
loadPreparedModelCatalogSnapshot: loadPreparedModelCatalogSnapshotMock,
|
||||
}));
|
||||
|
||||
@@ -200,6 +200,7 @@ function createConfigModuleMock() {
|
||||
|
||||
function createModelCatalogModuleMock() {
|
||||
return {
|
||||
loadProviderScopedThinkingCatalog: async () => [],
|
||||
loadPreparedModelCatalog: async () => [
|
||||
{
|
||||
provider: "anthropic",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// Boundary proof for the turn-path thinking fallback: manifest first, then a provider-scoped
|
||||
// static catalog, then scoped live discovery only for runtime-only models (e.g. Ollama).
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const manifestCatalogMock = vi.fn((..._args: unknown[]): Array<Record<string, unknown>> => []);
|
||||
const scopedStaticMock = vi.fn(
|
||||
async (..._args: unknown[]): Promise<Record<string, unknown>> => ({
|
||||
entries: [],
|
||||
routeVariants: [],
|
||||
}),
|
||||
);
|
||||
const scopedLiveMock = vi.fn(
|
||||
async (..._args: unknown[]): Promise<Record<string, unknown>> => ({
|
||||
entries: [],
|
||||
routeVariants: [],
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("./model-catalog.js", () => ({
|
||||
loadManifestModelCatalog: (...args: unknown[]) => manifestCatalogMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./prepared-model-runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./prepared-model-runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
// No published lifecycle owner: force the scoped read-only builders to run.
|
||||
prepareModelRuntimeSnapshot: vi.fn(async (input: { agentDir: string }) => {
|
||||
throw new actual.PreparedModelRuntimeOwnerNotPublishedError(
|
||||
`not published for test (${input.agentDir})`,
|
||||
);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./prepared-model-runtime.scoped-catalog.js", () => ({
|
||||
prepareScopedReadOnlyModelCatalog: (...args: unknown[]) => scopedStaticMock(...args),
|
||||
prepareScopedReadOnlyLiveModelCatalog: (...args: unknown[]) => scopedLiveMock(...args),
|
||||
}));
|
||||
|
||||
const ollamaEntry = {
|
||||
provider: "ollama",
|
||||
id: "minimax-m3:cloud",
|
||||
name: "minimax-m3:cloud",
|
||||
reasoning: true,
|
||||
};
|
||||
|
||||
describe("loadProviderScopedThinkingCatalog", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
manifestCatalogMock.mockReturnValue([]);
|
||||
scopedStaticMock.mockResolvedValue({ entries: [], routeVariants: [] });
|
||||
scopedLiveMock.mockResolvedValue({ entries: [], routeVariants: [] });
|
||||
});
|
||||
|
||||
it("resolves manifest-backed models without any scoped catalog build", async () => {
|
||||
manifestCatalogMock.mockReturnValue([
|
||||
{ provider: "openai", id: "gpt-5.6-luna", reasoning: true },
|
||||
]);
|
||||
const { loadProviderScopedThinkingCatalog } = await import("./prepared-model-catalog.js");
|
||||
const catalog = await loadProviderScopedThinkingCatalog({
|
||||
config: {},
|
||||
provider: "openai",
|
||||
model: "gpt-5.6-luna",
|
||||
});
|
||||
expect(catalog).toEqual([
|
||||
expect.objectContaining({ provider: "openai", id: "gpt-5.6-luna", reasoning: true }),
|
||||
]);
|
||||
expect(scopedStaticMock).not.toHaveBeenCalled();
|
||||
expect(scopedLiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("stops at the scoped static catalog when it resolves the entry", async () => {
|
||||
scopedStaticMock.mockResolvedValue({
|
||||
entries: [{ provider: "acme", id: "static-model", reasoning: false }],
|
||||
routeVariants: [],
|
||||
});
|
||||
const { loadProviderScopedThinkingCatalog } = await import("./prepared-model-catalog.js");
|
||||
const catalog = await loadProviderScopedThinkingCatalog({
|
||||
config: {},
|
||||
provider: "acme",
|
||||
model: "static-model",
|
||||
});
|
||||
expect(catalog).toEqual([
|
||||
expect.objectContaining({ provider: "acme", id: "static-model", reasoning: false }),
|
||||
]);
|
||||
expect(scopedStaticMock).toHaveBeenCalledTimes(1);
|
||||
expect(scopedStaticMock).toHaveBeenCalledWith(expect.anything(), ["acme"]);
|
||||
expect(scopedLiveMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs provider-scoped live discovery for runtime-only models and keeps their thinking", async () => {
|
||||
scopedLiveMock.mockResolvedValue({ entries: [ollamaEntry], routeVariants: [] });
|
||||
const { loadProviderScopedThinkingCatalog } = await import("./prepared-model-catalog.js");
|
||||
const catalog = await loadProviderScopedThinkingCatalog({
|
||||
config: {},
|
||||
provider: "ollama",
|
||||
model: "minimax-m3:cloud",
|
||||
});
|
||||
expect(catalog).toEqual([expect.objectContaining(ollamaEntry)]);
|
||||
// Live discovery stays scoped to the requested provider: no broad plugin fanout.
|
||||
expect(scopedLiveMock).toHaveBeenCalledTimes(1);
|
||||
expect(scopedLiveMock).toHaveBeenCalledWith(expect.anything(), ["ollama"]);
|
||||
expect(scopedStaticMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,14 @@ import {
|
||||
type PreparedModelRuntimeInput,
|
||||
type PreparedModelRuntimeSnapshot,
|
||||
} from "./prepared-model-runtime.js";
|
||||
import { prepareScopedReadOnlyModelCatalog } from "./prepared-model-runtime.scoped-catalog.js";
|
||||
import {
|
||||
prepareScopedReadOnlyLiveModelCatalog,
|
||||
prepareScopedReadOnlyModelCatalog,
|
||||
} from "./prepared-model-runtime.scoped-catalog.js";
|
||||
import {
|
||||
hasResolvedThinkingCatalogEntry,
|
||||
normalizeThinkingCatalogProviders,
|
||||
} from "./thinking-runtime.js";
|
||||
|
||||
export type LoadPreparedModelCatalogParams = {
|
||||
agentId?: string;
|
||||
@@ -34,6 +41,8 @@ export type LoadPreparedModelCatalogParams = {
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
providerDiscoveryProviderIds?: readonly string[];
|
||||
/** Scoped read-only loads may run live discovery for the scoped providers only. */
|
||||
scopedLiveProviderDiscovery?: boolean;
|
||||
allowGatewaySubagentBinding?: boolean;
|
||||
};
|
||||
|
||||
@@ -287,9 +296,59 @@ async function loadScopedReadOnlyModelCatalog(
|
||||
}
|
||||
}
|
||||
}
|
||||
return prepareScopedReadOnlyModelCatalog(
|
||||
activationExact,
|
||||
params.providerDiscoveryProviderIds ?? [],
|
||||
const prepareScoped =
|
||||
params.scopedLiveProviderDiscovery === true
|
||||
? prepareScopedReadOnlyLiveModelCatalog
|
||||
: prepareScopedReadOnlyModelCatalog;
|
||||
return prepareScoped(activationExact, params.providerDiscoveryProviderIds ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn-path capability reads (thinking levels and similar per-model facts) must stay off the
|
||||
* full live catalog build: manifest metadata first, then a provider-scoped read-only catalog,
|
||||
* then scoped live discovery only for providers whose models exist solely at runtime.
|
||||
*/
|
||||
export async function loadProviderScopedThinkingCatalog(params: {
|
||||
config: OpenClawConfig;
|
||||
provider: string;
|
||||
model: string;
|
||||
agentId?: string;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
}): Promise<ModelCatalogEntry[]> {
|
||||
const { loadManifestModelCatalog } = await import("./model-catalog.js");
|
||||
const manifestCatalog = normalizeThinkingCatalogProviders(
|
||||
loadManifestModelCatalog({
|
||||
config: params.config,
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
}),
|
||||
);
|
||||
const scopedParams = {
|
||||
config: params.config,
|
||||
...(params.agentId ? { agentId: params.agentId } : {}),
|
||||
...(params.agentDir ? { agentDir: params.agentDir } : {}),
|
||||
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
||||
readOnly: true,
|
||||
providerDiscoveryProviderIds: [params.provider],
|
||||
} satisfies LoadPreparedModelCatalogParams;
|
||||
const entryResolved = (catalog: readonly ModelCatalogEntry[]) =>
|
||||
hasResolvedThinkingCatalogEntry({ catalog, provider: params.provider, model: params.model });
|
||||
if (entryResolved(manifestCatalog)) {
|
||||
return manifestCatalog;
|
||||
}
|
||||
const scopedStatic = normalizeThinkingCatalogProviders(
|
||||
(await loadPreparedModelCatalogSnapshot(scopedParams)).entries,
|
||||
);
|
||||
if (entryResolved(scopedStatic)) {
|
||||
return scopedStatic;
|
||||
}
|
||||
return normalizeThinkingCatalogProviders(
|
||||
(
|
||||
await loadPreparedModelCatalogSnapshot({
|
||||
...scopedParams,
|
||||
scopedLiveProviderDiscovery: true,
|
||||
})
|
||||
).entries,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import type { PreparedProviderStaticCatalog } from "../plugins/provider-discover
|
||||
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";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
import type { AgentCredentialMap } from "./agent-auth-credentials.js";
|
||||
import { resolveAmbientAgentCredentialsForDiscovery } from "./agent-auth-discovery.js";
|
||||
import {
|
||||
@@ -56,6 +55,11 @@ import {
|
||||
type PreparedInboundRegistryLoader,
|
||||
} from "./prepared-model-runtime.inbound-registry.js";
|
||||
import { prepareOwnedPluginLoadContext } from "./prepared-model-runtime.plugin-context.js";
|
||||
import {
|
||||
listPreparedSyntheticAuthProviderRefs,
|
||||
resolvePreparedSyntheticAuth,
|
||||
scopeSyntheticAuthProviderRefs,
|
||||
} from "./prepared-model-runtime.synthetic-auth.js";
|
||||
import type {
|
||||
PreparedModelRuntimeBuildStats,
|
||||
PreparedModelRuntimeCatalogMode,
|
||||
@@ -158,40 +162,6 @@ function prepareAgentFacts(
|
||||
};
|
||||
}
|
||||
|
||||
function listPreparedSyntheticAuthProviderRefs(providers: readonly ProviderPlugin[]): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
providers.flatMap((provider) =>
|
||||
typeof provider.resolveSyntheticAuth === "function"
|
||||
? [provider.id, ...(provider.aliases ?? []), ...(provider.hookAliases ?? [])]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function resolvePreparedSyntheticAuth(params: {
|
||||
config: PreparedModelRuntimeInput["config"];
|
||||
provider: string;
|
||||
providers: readonly ProviderPlugin[];
|
||||
}): { apiKey?: string } | undefined {
|
||||
const normalizedProvider = normalizeProviderId(params.provider);
|
||||
const providerPlugin = params.providers.find((candidate) =>
|
||||
[candidate.id, ...(candidate.aliases ?? []), ...(candidate.hookAliases ?? [])].some(
|
||||
(ref) => normalizeProviderId(ref) === normalizedProvider,
|
||||
),
|
||||
);
|
||||
return (
|
||||
providerPlugin?.resolveSyntheticAuth?.({
|
||||
config: params.config,
|
||||
provider: params.provider,
|
||||
providerConfig: Object.entries(params.config.models?.providers ?? {}).find(
|
||||
([providerId]) => normalizeProviderId(providerId) === normalizedProvider,
|
||||
)?.[1],
|
||||
}) ?? undefined
|
||||
);
|
||||
}
|
||||
|
||||
export async function prepareWorkspaceBuildGroup(
|
||||
inputs: readonly PreparedModelRuntimeInput[],
|
||||
catalogMode: PreparedModelRuntimeCatalogMode,
|
||||
@@ -302,13 +272,16 @@ export async function prepareWorkspaceBuildGroup(
|
||||
syntheticAuthProviderRefs:
|
||||
catalogMode === "static"
|
||||
? listPreparedSyntheticAuthProviderRefs(preparedSyntheticAuthProviders)
|
||||
: resolveRuntimeSyntheticAuthProviderRefs({
|
||||
config: input.config,
|
||||
env,
|
||||
index: pluginMetadataSnapshot.index,
|
||||
registryDiagnostics: pluginMetadataSnapshot.registryDiagnostics,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
}),
|
||||
: scopeSyntheticAuthProviderRefs(
|
||||
resolveRuntimeSyntheticAuthProviderRefs({
|
||||
config: input.config,
|
||||
env,
|
||||
index: pluginMetadataSnapshot.index,
|
||||
registryDiagnostics: pluginMetadataSnapshot.registryDiagnostics,
|
||||
...(input.workspaceDir ? { workspaceDir: input.workspaceDir } : {}),
|
||||
}),
|
||||
options.providerDiscoveryProviderIds,
|
||||
),
|
||||
...(catalogMode === "static"
|
||||
? {
|
||||
resolveSyntheticAuth: (provider: string) =>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/** Synthetic-auth provider ref selection and prepared-catalog resolution for model-runtime builds. */
|
||||
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { ProviderPlugin } from "../plugins/types.js";
|
||||
|
||||
// Provider-scoped live builds must not fan ambient synthetic-auth discovery out to every
|
||||
// registered provider; each unscoped ref can force a full plugin module load on the read path.
|
||||
export function scopeSyntheticAuthProviderRefs(
|
||||
refs: readonly string[],
|
||||
providerDiscoveryProviderIds: readonly string[] | undefined,
|
||||
): string[] {
|
||||
if (!providerDiscoveryProviderIds) {
|
||||
return [...refs];
|
||||
}
|
||||
const scoped = new Set(providerDiscoveryProviderIds.map((id) => normalizeProviderId(id)));
|
||||
return refs.filter((ref) => scoped.has(normalizeProviderId(ref)));
|
||||
}
|
||||
|
||||
export function listPreparedSyntheticAuthProviderRefs(
|
||||
providers: readonly ProviderPlugin[],
|
||||
): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
providers.flatMap((provider) =>
|
||||
typeof provider.resolveSyntheticAuth === "function"
|
||||
? [provider.id, ...(provider.aliases ?? []), ...(provider.hookAliases ?? [])]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
].toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export function resolvePreparedSyntheticAuth(params: {
|
||||
config: OpenClawConfig;
|
||||
provider: string;
|
||||
providers: readonly ProviderPlugin[];
|
||||
}): { apiKey?: string } | undefined {
|
||||
const normalizedProvider = normalizeProviderId(params.provider);
|
||||
const providerPlugin = params.providers.find((candidate) =>
|
||||
[candidate.id, ...(candidate.aliases ?? []), ...(candidate.hookAliases ?? [])].some(
|
||||
(ref) => normalizeProviderId(ref) === normalizedProvider,
|
||||
),
|
||||
);
|
||||
return (
|
||||
providerPlugin?.resolveSyntheticAuth?.({
|
||||
config: params.config,
|
||||
provider: params.provider,
|
||||
providerConfig: Object.entries(params.config.models?.providers ?? {}).find(
|
||||
([providerId]) => normalizeProviderId(providerId) === normalizedProvider,
|
||||
)?.[1],
|
||||
}) ?? undefined
|
||||
);
|
||||
}
|
||||
@@ -105,6 +105,7 @@ vi.mock("../agents/embedded-agent.runtime.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: loadModelCatalogMock,
|
||||
}));
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ vi.mock("../agents/embedded-agent.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/model-catalog.runtime.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: async () => [],
|
||||
loadPreparedModelCatalog: (...args: unknown[]) =>
|
||||
replyRuntimeMockState.mocks.loadModelCatalog(...args),
|
||||
}));
|
||||
|
||||
@@ -112,6 +112,7 @@ function setFastModelsCliBackendDeps(): void {
|
||||
}
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: modelCatalogMocks.loadModelCatalog,
|
||||
loadPreparedModelCatalogSnapshot: async (...args: unknown[]) => {
|
||||
const entries = await modelCatalogMocks.loadModelCatalog(...args);
|
||||
|
||||
@@ -356,6 +356,7 @@ vi.mock("../../agents/prepared-model-catalog.js", () => {
|
||||
]);
|
||||
return {
|
||||
loadPreparedModelCatalog: loadModelCatalog,
|
||||
loadProviderScopedThinkingCatalog: loadModelCatalog,
|
||||
loadPreparedModelCatalogSnapshot: async () => {
|
||||
const entries = await loadModelCatalog();
|
||||
return { entries, routeVariants: entries };
|
||||
|
||||
@@ -18,6 +18,7 @@ const mocks = vi.hoisted(() => ({
|
||||
registerGetReplyRuntimeOverrides(mocks);
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadResolvedPublishedModelCatalogOwner: mocks.loadResolvedPublishedModelCatalogOwner,
|
||||
}));
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ vi.mock("./commands-status.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: mocks.loadModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ const catalogRuntimeMocks = vi.hoisted(() => {
|
||||
|
||||
vi.mock("../../agents/model-catalog.runtime.js", () => ({
|
||||
loadManifestModelCatalog: vi.fn(() => []),
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: catalogRuntimeMocks.loadModelCatalog,
|
||||
loadPreparedModelCatalogSnapshot: catalogRuntimeMocks.loadModelCatalogSnapshot,
|
||||
}));
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { ModelAliasIndex } from "./model-selection-directive.js";
|
||||
const loadPreparedModelCatalog = vi.hoisted(() => vi.fn(async () => modelCatalog));
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ vi.mock("../../agents/session-write-lock.js", async () => {
|
||||
});
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: vi.fn(async () => [
|
||||
{ provider: "minimax", id: "m2.7", name: "M2.7" },
|
||||
{ provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" },
|
||||
|
||||
@@ -284,6 +284,7 @@ vi.mock("../agents/agent-scope.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog:
|
||||
mocks.loadModelCatalog as typeof import("../agents/prepared-model-catalog.js").loadPreparedModelCatalog,
|
||||
}));
|
||||
|
||||
@@ -67,6 +67,7 @@ vi.mock("../agents/model-catalog.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: vi.fn(),
|
||||
loadPreparedModelCatalogSnapshot: vi.fn(async () => ({
|
||||
entries: [],
|
||||
|
||||
@@ -21,6 +21,7 @@ const modelCatalogRouteVariants = vi.hoisted(() => ({
|
||||
value: undefined as readonly ModelCatalogEntry[] | undefined,
|
||||
}));
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogSnapshot: async (...args: unknown[]) => {
|
||||
const entries = await loadModelCatalog(...args);
|
||||
return { entries, routeVariants: modelCatalogRouteVariants.value ?? entries };
|
||||
|
||||
@@ -136,6 +136,7 @@ vi.mock("../agents/model-auth.js", async (importOriginal) => {
|
||||
});
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: loadModelCatalog,
|
||||
loadPreparedModelCatalogOwnerSnapshot: async (params: { agentDir?: string; config?: object }) => {
|
||||
const entries = await loadModelCatalog(params);
|
||||
|
||||
@@ -26,6 +26,7 @@ vi.mock("../../plugins/current-plugin-metadata-snapshot.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogSnapshot: mocks.loadPreparedModelCatalogSnapshot,
|
||||
}));
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const resolveAuthProfileEligibilityMock = vi.fn<
|
||||
const resolveSecretRefStringMock = vi.fn(async () => "resolved-secret");
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: loadModelCatalogMock,
|
||||
}));
|
||||
vi.mock("../../agents/model-auth.js", () => ({
|
||||
|
||||
@@ -20,6 +20,7 @@ vi.mock("../../agents/model-suppression.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogSnapshot: mocks.loadModelCatalogSnapshot,
|
||||
}));
|
||||
|
||||
|
||||
@@ -301,6 +301,7 @@ vi.mock("../../cli/update-cli/plugin-payload-validation.js", () => ({
|
||||
runPluginPayloadSmokeCheckForManifestRecords: mocks.runPluginPayloadSmokeCheckForManifestRecords,
|
||||
}));
|
||||
vi.mock("../../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogSnapshot: async (...args: unknown[]) => {
|
||||
const entries = await mocks.loadModelCatalog(...args);
|
||||
return { entries, routeVariants: mocks.modelCatalogRouteVariants ?? entries };
|
||||
|
||||
@@ -21,6 +21,9 @@ vi.mock("../agents/prepared-model-catalog.js", async () => {
|
||||
entries: (await loadPreparedModelCatalog(params)) ?? [],
|
||||
routeVariants: [],
|
||||
})),
|
||||
loadProviderScopedThinkingCatalog: vi.fn(
|
||||
async (params) => (await loadPreparedModelCatalog(params)) ?? [],
|
||||
),
|
||||
loadPublishedPreparedModelCatalog: loadPreparedModelCatalog,
|
||||
publishedModelCatalogOwnerMatchesAgent: (owner: { agentId: string }, agentId: string) =>
|
||||
owner.agentId === agentId.trim().toLowerCase(),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Cron turns must hydrate runtime-only model thinking through the provider-scoped helper,
|
||||
// never through a full live catalog build.
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const scopedThinkingCatalogMock = vi.fn(
|
||||
async (..._args: unknown[]): Promise<Array<Record<string, unknown>>> => [],
|
||||
);
|
||||
|
||||
vi.mock("./run-model-selection.runtime.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./run-model-selection.runtime.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadProviderScopedThinkingCatalog: (...args: unknown[]) => scopedThinkingCatalogMock(...args),
|
||||
};
|
||||
});
|
||||
|
||||
const owner = {
|
||||
agentId: "main",
|
||||
agentDir: "/tmp/cron-agent",
|
||||
workspaceDir: "/tmp/cron-workspace",
|
||||
config: {},
|
||||
modelCatalog: { entries: [], routeVariants: [] },
|
||||
} as never;
|
||||
|
||||
describe("resolveCronThinkingSelection scoped hydration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
scopedThinkingCatalogMock.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("hydrates a runtime-only model through the provider-scoped helper", async () => {
|
||||
scopedThinkingCatalogMock.mockResolvedValue([
|
||||
{ provider: "ollama", id: "minimax-m3:cloud", reasoning: true },
|
||||
]);
|
||||
const { resolveCronThinkingSelection } = await import("./model-selection.js");
|
||||
const selection = await resolveCronThinkingSelection({
|
||||
cfg: {},
|
||||
owner,
|
||||
provider: "ollama",
|
||||
model: "minimax-m3:cloud",
|
||||
jobThinking: "medium",
|
||||
});
|
||||
expect(selection.requestedThinkLevel).toBe("medium");
|
||||
expect(selection.catalog).toEqual([
|
||||
expect.objectContaining({ provider: "ollama", id: "minimax-m3:cloud", reasoning: true }),
|
||||
]);
|
||||
expect(scopedThinkingCatalogMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "ollama",
|
||||
model: "minimax-m3:cloud",
|
||||
agentId: "main",
|
||||
agentDir: "/tmp/cron-agent",
|
||||
workspaceDir: "/tmp/cron-workspace",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the owner catalog and skips hydration when thinking is off", async () => {
|
||||
const { resolveCronThinkingSelection } = await import("./model-selection.js");
|
||||
const selection = await resolveCronThinkingSelection({
|
||||
cfg: {},
|
||||
owner,
|
||||
provider: "ollama",
|
||||
model: "minimax-m3:cloud",
|
||||
jobThinking: "off",
|
||||
});
|
||||
expect(selection.requestedThinkLevel).toBe("off");
|
||||
expect(scopedThinkingCatalogMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
DEFAULT_PROVIDER,
|
||||
getModelRefStatus,
|
||||
loadResolvedPublishedModelCatalogOwner,
|
||||
loadPreparedModelCatalogSnapshot,
|
||||
loadProviderScopedThinkingCatalog,
|
||||
normalizeModelSelection,
|
||||
publishedModelCatalogOwnerMatchesAgent,
|
||||
resolveAgentConfig,
|
||||
@@ -125,15 +125,16 @@ async function resolveCronThinkingCatalog(params: {
|
||||
) {
|
||||
return catalog;
|
||||
}
|
||||
// Thinking capability is a per-model fact; never materialize the full live catalog on cron turns.
|
||||
return normalizeThinkingCatalogProviders(
|
||||
(
|
||||
await loadPreparedModelCatalogSnapshot({
|
||||
config: params.owner.config,
|
||||
agentId: params.owner.agentId,
|
||||
agentDir: params.owner.agentDir,
|
||||
workspaceDir: params.owner.workspaceDir,
|
||||
})
|
||||
).entries,
|
||||
await loadProviderScopedThinkingCatalog({
|
||||
config: params.owner.config,
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
agentId: params.owner.agentId,
|
||||
agentDir: params.owner.agentDir,
|
||||
workspaceDir: params.owner.workspaceDir,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export { resolveSubagentModelConfigSelectionResult } from "../../agents/agent-sc
|
||||
export { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
|
||||
export { publishedModelCatalogOwnerMatchesAgent } from "../../agents/prepared-model-catalog-owner.js";
|
||||
export {
|
||||
loadPreparedModelCatalogSnapshot,
|
||||
loadProviderScopedThinkingCatalog,
|
||||
loadResolvedPublishedModelCatalogOwner,
|
||||
} from "../../agents/prepared-model-catalog.js";
|
||||
export type { ResolvedPublishedModelCatalogOwner } from "../../agents/prepared-model-catalog.types.js";
|
||||
|
||||
@@ -238,6 +238,7 @@ vi.mock("./run-model-selection.runtime.js", () => ({
|
||||
entries: await loadModelCatalogMock(params),
|
||||
routeVariants: [],
|
||||
}),
|
||||
loadProviderScopedThinkingCatalog: async (params: unknown) => await loadModelCatalogMock(params),
|
||||
loadResolvedPublishedModelCatalogOwner: loadModelCatalogOwnerMock,
|
||||
publishedModelCatalogOwnerMatchesAgent: (owner: { agentId: string }, agentId: string) =>
|
||||
owner.agentId === agentId.trim().toLowerCase(),
|
||||
|
||||
@@ -21,6 +21,7 @@ vi.mock("../agents/model-catalog.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: mocks.loadModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ vi.mock("../agents/model-catalog.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: mocks.loadModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const mocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: mocks.loadModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -394,6 +394,7 @@ vi.mock("../agents/model-catalog.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: mocks.loadModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const mocks = vi.hoisted(() => ({ loadPreparedModelCatalogOwnerSnapshot: vi.fn() }));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogOwnerSnapshot: mocks.loadPreparedModelCatalogOwnerSnapshot,
|
||||
}));
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ vi.mock("../infra/update-startup.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: hoisted.loadModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ vi.mock("../agents/model-catalog.js", async () => {
|
||||
});
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: loadModelCatalog,
|
||||
}));
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const mocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
getPreparedModelCatalogSnapshot: (...args: unknown[]) => mocks.getSnapshot(...args),
|
||||
loadPreparedModelCatalog: (...args: unknown[]) => mocks.loadCatalog(...args),
|
||||
}));
|
||||
|
||||
@@ -1038,10 +1038,15 @@ export function resolveProviderSyntheticAuthWithPlugin(params: {
|
||||
}
|
||||
}
|
||||
if (providerRefs.length === 1) {
|
||||
// Last-resort match for custom provider ids with no resolvable owning plugin (e.g. Ollama
|
||||
// aliases). Entry modules only: a full plugin-runtime sweep here costs seconds per ref on
|
||||
// source checkouts and belongs to explicit control-plane loads.
|
||||
return resolvePluginDiscoveryProvidersRuntime({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
discoveryEntriesOnly: true,
|
||||
includeSyntheticAuthProviders: true,
|
||||
})
|
||||
.find((provider) => matchesAnyProviderPluginRef(provider, providerRefs))
|
||||
?.resolveSyntheticAuth?.(params.context);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
resolveEffectivePluginActivationState,
|
||||
resolveMemorySlotDecision,
|
||||
} from "../../plugins/config-policy.js";
|
||||
import { registerPluginMetadataProcessMemoLifecycleClear } from "../../plugins/plugin-metadata-lifecycle.js";
|
||||
import { resolvePluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.js";
|
||||
import { hasKind } from "../../plugins/slots.js";
|
||||
import { isPathInsideWithRealpath } from "../../security/scan-paths.js";
|
||||
@@ -20,6 +21,20 @@ const log = createSubsystemLogger("skills");
|
||||
|
||||
type PluginSkillLinkType = "dir" | "junction";
|
||||
|
||||
// Plugin metadata is process-stable while the gateway runs, but this resolver sits on the
|
||||
// per-turn skills-refresh path. The single-slot memo keeps repeat turns from re-walking and
|
||||
// re-publishing every plugin skill dir; lifecycle clears evict it on plugin reload/install.
|
||||
let pluginSkillDirsMemo: {
|
||||
workspaceDir: string;
|
||||
config: OpenClawConfig | undefined;
|
||||
snapshot: unknown;
|
||||
dirs: string[];
|
||||
} | null = null;
|
||||
|
||||
registerPluginMetadataProcessMemoLifecycleClear(() => {
|
||||
pluginSkillDirsMemo = null;
|
||||
});
|
||||
|
||||
export function resolvePluginSkillDirs(params: {
|
||||
workspaceDir: string | undefined;
|
||||
config?: OpenClawConfig;
|
||||
@@ -40,6 +55,16 @@ export function resolvePluginSkillDirs(params: {
|
||||
env: process.env,
|
||||
allowWorkspaceScopedCurrent: true,
|
||||
});
|
||||
const canMemoize = params.pluginSkillsDir === undefined;
|
||||
if (
|
||||
canMemoize &&
|
||||
pluginSkillDirsMemo &&
|
||||
pluginSkillDirsMemo.workspaceDir === workspaceDir &&
|
||||
pluginSkillDirsMemo.config === params.config &&
|
||||
pluginSkillDirsMemo.snapshot === metadataSnapshot
|
||||
) {
|
||||
return pluginSkillDirsMemo.dirs;
|
||||
}
|
||||
const registry = metadataSnapshot.manifestRegistry;
|
||||
if (registry.plugins.length === 0) {
|
||||
publishPluginSkills([], {
|
||||
@@ -117,6 +142,14 @@ export function resolvePluginSkillDirs(params: {
|
||||
pluginSkillsDir: params.pluginSkillsDir,
|
||||
});
|
||||
|
||||
if (canMemoize) {
|
||||
pluginSkillDirsMemo = {
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
snapshot: metadataSnapshot,
|
||||
dirs: resolved,
|
||||
};
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ vi.mock("../plugins/providers.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
// These tests exercise the TUI boundary, not filesystem-backed catalog discovery.
|
||||
getPreparedModelCatalogSnapshot: vi.fn(() => undefined),
|
||||
loadPreparedModelCatalog: vi.fn(async () => []),
|
||||
|
||||
@@ -269,6 +269,7 @@ vi.mock("../commands/auth-choice.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalogSnapshot: async (...args: unknown[]) => {
|
||||
const entries = await loadModelCatalog(...args);
|
||||
return { entries, routeVariants: entries };
|
||||
|
||||
@@ -28,6 +28,7 @@ const preparedVisionCatalog = vi.hoisted(() => [
|
||||
]);
|
||||
|
||||
vi.mock("../../../../src/agents/prepared-model-catalog.js", () => ({
|
||||
loadProviderScopedThinkingCatalog: vi.fn(async () => []),
|
||||
loadPreparedModelCatalog: vi.fn(async () => preparedVisionCatalog),
|
||||
}));
|
||||
|
||||
|
||||
@@ -145,6 +145,8 @@ vi.doMock("../../../src/agents/model-catalog.runtime.js", () => ({
|
||||
const entries = await modelCatalogMocks.loadPreparedModelCatalog(...args);
|
||||
return { entries, routeVariants: entries, authoritative: true };
|
||||
},
|
||||
loadProviderScopedThinkingCatalog: async (...args: unknown[]) =>
|
||||
await modelCatalogMocks.loadPreparedModelCatalog(...args),
|
||||
}));
|
||||
|
||||
vi.doMock("../../../src/plugins/provider-runtime.runtime.js", () => ({
|
||||
|
||||
Reference in New Issue
Block a user