mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:50:45 +00:00
Merge remote-tracking branch 'origin/main' into release/2026.4.25
This commit is contained in:
@@ -160,9 +160,13 @@ export OTEL_SERVICE_NAME="openclaw-gateway"
|
||||
./scripts/docker/setup.sh
|
||||
```
|
||||
|
||||
The official OpenClaw Docker release image includes `diagnostics-otel`
|
||||
dependencies. To enable export, allow and enable the `diagnostics-otel` plugin
|
||||
in config, then set `diagnostics.otel.enabled=true` or use the config example in
|
||||
The official OpenClaw Docker release image includes the bundled
|
||||
`diagnostics-otel` plugin source. Depending on the image and cache state, the
|
||||
Gateway may still stage plugin-local OpenTelemetry runtime dependencies the
|
||||
first time the plugin is enabled, so allow that first boot to reach the package
|
||||
registry or prewarm the image in your release lane. To enable export, allow and
|
||||
enable the `diagnostics-otel` plugin in config, then set
|
||||
`diagnostics.otel.enabled=true` or use the config example in
|
||||
[OpenTelemetry export](/gateway/opentelemetry). Collector auth headers are
|
||||
configured through `diagnostics.otel.headers`, not through Docker environment
|
||||
variables.
|
||||
|
||||
@@ -170,6 +170,10 @@ vi.mock("../commands/models/list.js", () => ({
|
||||
modelsStatusCommand:
|
||||
mocks.modelsStatusCommand as typeof import("../commands/models/list.js").modelsStatusCommand,
|
||||
}));
|
||||
vi.mock("../commands/models/list.status-command.js", () => ({
|
||||
modelsStatusCommand:
|
||||
mocks.modelsStatusCommand as typeof import("../commands/models/list.status-command.js").modelsStatusCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/call.js", () => ({
|
||||
callGateway: mocks.callGateway as typeof import("../gateway/call.js").callGateway,
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { updateAuthProfileStoreWithLock } from "../agents/auth-profiles/store.js";
|
||||
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
|
||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||
import { modelsStatusCommand } from "../commands/models/list.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
@@ -683,6 +682,7 @@ async function buildModelProviders() {
|
||||
|
||||
async function runModelAuthStatus() {
|
||||
const captured: string[] = [];
|
||||
const { modelsStatusCommand } = await import("../commands/models/list.status-command.js");
|
||||
await modelsStatusCommand(
|
||||
{ json: true },
|
||||
{
|
||||
|
||||
@@ -49,6 +49,12 @@ vi.mock("../commands/models/list.js", () => ({
|
||||
modelsListCommand: mocks.noopAsync,
|
||||
modelsStatusCommand: mocks.modelsStatusCommand,
|
||||
}));
|
||||
vi.mock("../commands/models/list.list-command.js", () => ({
|
||||
modelsListCommand: mocks.noopAsync,
|
||||
}));
|
||||
vi.mock("../commands/models/list.status-command.js", () => ({
|
||||
modelsStatusCommand: mocks.modelsStatusCommand,
|
||||
}));
|
||||
vi.mock("../commands/models/auth.js", () => ({
|
||||
modelsAuthAddCommand: mocks.modelsAuthAddCommand,
|
||||
modelsAuthLoginCommand: mocks.modelsAuthLoginCommand,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Command } from "commander";
|
||||
import { modelsListCommand, modelsStatusCommand } from "../commands/models/list.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
@@ -35,6 +34,7 @@ export function registerModelsCli(program: Command) {
|
||||
.option("--plain", "Plain line output", false)
|
||||
.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsListCommand } = await import("../commands/models/list.list-command.js");
|
||||
await modelsListCommand(opts, defaultRuntime);
|
||||
});
|
||||
});
|
||||
@@ -71,6 +71,7 @@ export function registerModelsCli(program: Command) {
|
||||
const agent =
|
||||
resolveOptionFromCommand<string>(command, "agent") ?? (opts.agent as string | undefined);
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsStatusCommand } = await import("../commands/models/list.status-command.js");
|
||||
await modelsStatusCommand(
|
||||
{
|
||||
json: Boolean(opts.json),
|
||||
@@ -270,6 +271,7 @@ export function registerModelsCli(program: Command) {
|
||||
|
||||
models.action(async (opts) => {
|
||||
await runModelsCommand(async () => {
|
||||
const { modelsStatusCommand } = await import("../commands/models/list.status-command.js");
|
||||
await modelsStatusCommand(
|
||||
{
|
||||
json: Boolean(opts?.statusJson),
|
||||
|
||||
@@ -56,6 +56,7 @@ export const loadPluginManifestRegistry: UnknownMock = vi.fn();
|
||||
export const buildPluginSnapshotReport: UnknownMock = vi.fn();
|
||||
export const buildPluginRegistrySnapshotReport: UnknownMock = vi.fn();
|
||||
export const buildPluginInspectReport: UnknownMock = vi.fn();
|
||||
export const buildAllPluginInspectReports: UnknownMock = vi.fn();
|
||||
export const buildPluginDiagnosticsReport: UnknownMock = vi.fn();
|
||||
export const buildPluginCompatibilityNotices: UnknownMock = vi.fn();
|
||||
export const inspectPluginRegistry: AsyncUnknownMock = vi.fn();
|
||||
@@ -248,6 +249,16 @@ vi.mock("../plugins/status.js", () => ({
|
||||
buildPluginInspectReport,
|
||||
...args,
|
||||
)) as (typeof import("../plugins/status.js"))["buildPluginInspectReport"],
|
||||
buildAllPluginInspectReports: ((
|
||||
...args: Parameters<(typeof import("../plugins/status.js"))["buildAllPluginInspectReports"]>
|
||||
) =>
|
||||
invokeMock<
|
||||
Parameters<(typeof import("../plugins/status.js"))["buildAllPluginInspectReports"]>,
|
||||
ReturnType<(typeof import("../plugins/status.js"))["buildAllPluginInspectReports"]>
|
||||
>(
|
||||
buildAllPluginInspectReports,
|
||||
...args,
|
||||
)) as (typeof import("../plugins/status.js"))["buildAllPluginInspectReports"],
|
||||
buildPluginDiagnosticsReport: ((
|
||||
...args: Parameters<(typeof import("../plugins/status.js"))["buildPluginDiagnosticsReport"]>
|
||||
) =>
|
||||
|
||||
@@ -5,51 +5,14 @@ import { loadConfig, readConfigFileSnapshot, replaceConfigFile } from "../config
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||
import {
|
||||
loadInstalledPluginIndexInstallRecords,
|
||||
removePluginInstallRecordFromRecords,
|
||||
withoutPluginInstallRecords,
|
||||
withPluginInstallRecords,
|
||||
} from "../plugins/installed-plugin-index-records.js";
|
||||
import { listMarketplacePlugins } from "../plugins/marketplace.js";
|
||||
import { inspectPluginRegistry, refreshPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import { formatPluginSourceForTable, resolvePluginSourceRoots } from "../plugins/source-display.js";
|
||||
import {
|
||||
buildAllPluginInspectReports,
|
||||
buildPluginDiagnosticsReport,
|
||||
buildPluginCompatibilityNotices,
|
||||
buildPluginInspectReport,
|
||||
buildPluginRegistrySnapshotReport,
|
||||
formatPluginCompatibilityNotice,
|
||||
} from "../plugins/status.js";
|
||||
import type { PluginLogger } from "../plugins/types.js";
|
||||
import {
|
||||
applyPluginUninstallDirectoryRemoval,
|
||||
formatUninstallActionLabels,
|
||||
formatUninstallSlotResetPreview,
|
||||
planPluginUninstall,
|
||||
resolveUninstallChannelConfigKeys,
|
||||
UNINSTALL_ACTION_LABELS,
|
||||
} from "../plugins/uninstall.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { formatDocsLink } from "../terminal/links.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
import {
|
||||
applySlotSelectionForPlugin,
|
||||
createPluginInstallLogger,
|
||||
logSlotWarnings,
|
||||
} from "./plugins-command-helpers.js";
|
||||
import { setPluginEnabledInConfig } from "./plugins-config.js";
|
||||
import { runPluginInstallCommand } from "./plugins-install-command.js";
|
||||
import { commitPluginInstallRecordsWithConfig } from "./plugins-install-record-commit.js";
|
||||
import { formatPluginLine } from "./plugins-list-format.js";
|
||||
import { refreshPluginRegistryAfterConfigMutation } from "./plugins-registry-refresh.js";
|
||||
import { resolvePluginUninstallId } from "./plugins-uninstall-selection.js";
|
||||
import { runPluginUpdateCommand } from "./plugins-update-command.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
|
||||
export type PluginsListOptions = {
|
||||
json?: boolean;
|
||||
@@ -182,7 +145,8 @@ export function registerPluginsCli(program: Command) {
|
||||
.option("--json", "Print JSON")
|
||||
.option("--enabled", "Only show enabled plugins", false)
|
||||
.option("--verbose", "Show detailed entries", false)
|
||||
.action((opts: PluginsListOptions) => {
|
||||
.action(async (opts: PluginsListOptions) => {
|
||||
const { buildPluginRegistrySnapshotReport } = await import("../plugins/status.js");
|
||||
const cfg = loadConfig();
|
||||
const report = buildPluginRegistrySnapshotReport({
|
||||
config: cfg,
|
||||
@@ -290,6 +254,14 @@ export function registerPluginsCli(program: Command) {
|
||||
.option("--all", "Inspect all plugins")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (id: string | undefined, opts: PluginInspectOptions) => {
|
||||
const {
|
||||
buildAllPluginInspectReports,
|
||||
buildPluginDiagnosticsReport,
|
||||
buildPluginInspectReport,
|
||||
formatPluginCompatibilityNotice,
|
||||
} = await import("../plugins/status.js");
|
||||
const { loadInstalledPluginIndexInstallRecords } =
|
||||
await import("../plugins/installed-plugin-index-records.js");
|
||||
const cfg = loadConfig();
|
||||
const installRecords = await loadInstalledPluginIndexInstallRecords();
|
||||
const report = buildPluginDiagnosticsReport({
|
||||
@@ -523,6 +495,11 @@ export function registerPluginsCli(program: Command) {
|
||||
.description("Enable a plugin in config")
|
||||
.argument("<id>", "Plugin id")
|
||||
.action(async (id: string) => {
|
||||
const { enablePluginInConfig } = await import("../plugins/enable.js");
|
||||
const { applySlotSelectionForPlugin, logSlotWarnings } =
|
||||
await import("./plugins-command-helpers.js");
|
||||
const { refreshPluginRegistryAfterConfigMutation } =
|
||||
await import("./plugins-registry-refresh.js");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
|
||||
const enableResult = enablePluginInConfig(cfg, id);
|
||||
@@ -557,6 +534,9 @@ export function registerPluginsCli(program: Command) {
|
||||
.description("Disable a plugin in config")
|
||||
.argument("<id>", "Plugin id")
|
||||
.action(async (id: string) => {
|
||||
const { setPluginEnabledInConfig } = await import("./plugins-config.js");
|
||||
const { refreshPluginRegistryAfterConfigMutation } =
|
||||
await import("./plugins-registry-refresh.js");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
|
||||
const next = setPluginEnabledInConfig(cfg, id, false);
|
||||
@@ -583,6 +563,27 @@ export function registerPluginsCli(program: Command) {
|
||||
.option("--force", "Skip confirmation prompt", false)
|
||||
.option("--dry-run", "Show what would be removed without making changes", false)
|
||||
.action(async (id: string, opts: PluginUninstallOptions) => {
|
||||
const {
|
||||
loadInstalledPluginIndexInstallRecords,
|
||||
removePluginInstallRecordFromRecords,
|
||||
withoutPluginInstallRecords,
|
||||
withPluginInstallRecords,
|
||||
} = await import("../plugins/installed-plugin-index-records.js");
|
||||
const { buildPluginDiagnosticsReport } = await import("../plugins/status.js");
|
||||
const {
|
||||
formatUninstallActionLabels,
|
||||
formatUninstallSlotResetPreview,
|
||||
resolveUninstallChannelConfigKeys,
|
||||
resolveUninstallDirectoryTarget,
|
||||
UNINSTALL_ACTION_LABELS,
|
||||
uninstallPlugin,
|
||||
} = await import("../plugins/uninstall.js");
|
||||
const { commitPluginInstallRecordsWithConfig } =
|
||||
await import("./plugins-install-record-commit.js");
|
||||
const { refreshPluginRegistryAfterConfigMutation } =
|
||||
await import("./plugins-registry-refresh.js");
|
||||
const { resolvePluginUninstallId } = await import("./plugins-uninstall-selection.js");
|
||||
const { promptYesNo } = await import("./prompt.js");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const sourceConfig = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
|
||||
const installRecords = await loadInstalledPluginIndexInstallRecords();
|
||||
@@ -744,6 +745,7 @@ export function registerPluginsCli(program: Command) {
|
||||
marketplace?: string;
|
||||
},
|
||||
) => {
|
||||
const { runPluginInstallCommand } = await import("./plugins-install-command.js");
|
||||
await runPluginInstallCommand({ raw, opts });
|
||||
},
|
||||
);
|
||||
@@ -760,6 +762,7 @@ export function registerPluginsCli(program: Command) {
|
||||
false,
|
||||
)
|
||||
.action(async (id: string | undefined, opts: PluginUpdateOptions) => {
|
||||
const { runPluginUpdateCommand } = await import("./plugins-update-command.js");
|
||||
await runPluginUpdateCommand({ id, opts });
|
||||
});
|
||||
|
||||
@@ -769,6 +772,8 @@ export function registerPluginsCli(program: Command) {
|
||||
.option("--json", "Print JSON")
|
||||
.option("--refresh", "Rebuild the persisted registry from current plugin manifests", false)
|
||||
.action(async (opts: PluginRegistryOptions) => {
|
||||
const { inspectPluginRegistry, refreshPluginRegistry } =
|
||||
await import("../plugins/plugin-registry.js");
|
||||
const cfg = loadConfig();
|
||||
|
||||
if (opts.refresh) {
|
||||
@@ -825,7 +830,12 @@ export function registerPluginsCli(program: Command) {
|
||||
plugins
|
||||
.command("doctor")
|
||||
.description("Report plugin load issues")
|
||||
.action(() => {
|
||||
.action(async () => {
|
||||
const {
|
||||
buildPluginCompatibilityNotices,
|
||||
buildPluginDiagnosticsReport,
|
||||
formatPluginCompatibilityNotice,
|
||||
} = await import("../plugins/status.js");
|
||||
const report = buildPluginDiagnosticsReport();
|
||||
const errors = report.plugins.filter((p) => p.status === "error");
|
||||
const diags = report.diagnostics.filter((d) => d.level === "error");
|
||||
@@ -880,6 +890,8 @@ export function registerPluginsCli(program: Command) {
|
||||
.argument("<source>", "Local marketplace path/repo or git/GitHub source")
|
||||
.option("--json", "Print JSON")
|
||||
.action(async (source: string, opts: PluginMarketplaceListOptions) => {
|
||||
const { listMarketplacePlugins } = await import("../plugins/marketplace.js");
|
||||
const { createPluginInstallLogger } = await import("./plugins-command-helpers.js");
|
||||
const result = await listMarketplacePlugins({
|
||||
marketplace: source,
|
||||
logger: createPluginInstallLogger(),
|
||||
|
||||
@@ -17,7 +17,8 @@ type RouteArgParser<TArgs> = (argv: string[]) => TArgs | null;
|
||||
|
||||
type ParsedRouteArgs<TParse extends RouteArgParser<unknown>> = Exclude<ReturnType<TParse>, null>;
|
||||
type ConfigCliModule = typeof import("../config-cli.js");
|
||||
type ModelsListModule = typeof import("../../commands/models/list.js");
|
||||
type ModelsListCommandModule = typeof import("../../commands/models/list.list-command.js");
|
||||
type ModelsStatusCommandModule = typeof import("../../commands/models/list.status-command.js");
|
||||
|
||||
export type RoutedCommandDefinition<TParse extends RouteArgParser<unknown>> = {
|
||||
parseArgs: TParse;
|
||||
@@ -36,16 +37,22 @@ function defineRoutedCommand<TParse extends RouteArgParser<unknown>>(
|
||||
}
|
||||
|
||||
let configCliPromise: Promise<ConfigCliModule> | undefined;
|
||||
let modelsListPromise: Promise<ModelsListModule> | undefined;
|
||||
let modelsListCommandPromise: Promise<ModelsListCommandModule> | undefined;
|
||||
let modelsStatusCommandPromise: Promise<ModelsStatusCommandModule> | undefined;
|
||||
|
||||
function loadConfigCli(): Promise<ConfigCliModule> {
|
||||
configCliPromise ??= import("../config-cli.js");
|
||||
return configCliPromise;
|
||||
}
|
||||
|
||||
function loadModelsList(): Promise<ModelsListModule> {
|
||||
modelsListPromise ??= import("../../commands/models/list.js");
|
||||
return modelsListPromise;
|
||||
function loadModelsListCommand(): Promise<ModelsListCommandModule> {
|
||||
modelsListCommandPromise ??= import("../../commands/models/list.list-command.js");
|
||||
return modelsListCommandPromise;
|
||||
}
|
||||
|
||||
function loadModelsStatusCommand(): Promise<ModelsStatusCommandModule> {
|
||||
modelsStatusCommandPromise ??= import("../../commands/models/list.status-command.js");
|
||||
return modelsStatusCommandPromise;
|
||||
}
|
||||
|
||||
export const routedCommandDefinitions = {
|
||||
@@ -114,14 +121,14 @@ export const routedCommandDefinitions = {
|
||||
"models-list": defineRoutedCommand({
|
||||
parseArgs: parseModelsListRouteArgs,
|
||||
runParsedArgs: async (args) => {
|
||||
const { modelsListCommand } = await loadModelsList();
|
||||
const { modelsListCommand } = await loadModelsListCommand();
|
||||
await modelsListCommand(args, defaultRuntime);
|
||||
},
|
||||
}),
|
||||
"models-status": defineRoutedCommand({
|
||||
parseArgs: parseModelsStatusRouteArgs,
|
||||
runParsedArgs: async (args) => {
|
||||
const { modelsStatusCommand } = await loadModelsList();
|
||||
const { modelsStatusCommand } = await loadModelsStatusCommand();
|
||||
await modelsStatusCommand(args, defaultRuntime);
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -23,6 +23,12 @@ vi.mock("../../commands/models/list.js", () => ({
|
||||
modelsListCommand: modelsListCommandMock,
|
||||
modelsStatusCommand: modelsStatusCommandMock,
|
||||
}));
|
||||
vi.mock("../../commands/models/list.list-command.js", () => ({
|
||||
modelsListCommand: modelsListCommandMock,
|
||||
}));
|
||||
vi.mock("../../commands/models/list.status-command.js", () => ({
|
||||
modelsStatusCommand: modelsStatusCommandMock,
|
||||
}));
|
||||
|
||||
vi.mock("../daemon-cli/status.js", () => ({
|
||||
runDaemonStatus: runDaemonStatusMock,
|
||||
|
||||
@@ -63,6 +63,7 @@ const mocks = vi.hoisted(() => {
|
||||
loadModelCatalog: vi.fn(),
|
||||
loadProviderCatalogModelsForList: vi.fn(),
|
||||
loadStaticManifestCatalogRowsForList: vi.fn(),
|
||||
loadProviderIndexCatalogRowsForList: vi.fn(),
|
||||
hasProviderStaticCatalogForFilter: vi.fn(),
|
||||
resolveConfiguredEntries: vi.fn(),
|
||||
printModelTable: vi.fn(),
|
||||
@@ -91,6 +92,7 @@ function resetMocks() {
|
||||
mocks.loadModelCatalog.mockResolvedValue([]);
|
||||
mocks.loadProviderCatalogModelsForList.mockResolvedValue([]);
|
||||
mocks.loadStaticManifestCatalogRowsForList.mockReturnValue([]);
|
||||
mocks.loadProviderIndexCatalogRowsForList.mockReturnValue([]);
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValue(false);
|
||||
mocks.resolveConfiguredEntries.mockReturnValue({
|
||||
entries: [
|
||||
@@ -147,12 +149,17 @@ function installModelsListCommandForwardCompatMocks() {
|
||||
|
||||
vi.doMock("./list.provider-catalog.js", () => ({
|
||||
hasProviderStaticCatalogForFilter: mocks.hasProviderStaticCatalogForFilter,
|
||||
loadProviderCatalogModelsForList: mocks.loadProviderCatalogModelsForList,
|
||||
}));
|
||||
|
||||
vi.doMock("./list.manifest-catalog.js", () => ({
|
||||
loadStaticManifestCatalogRowsForList: mocks.loadStaticManifestCatalogRowsForList,
|
||||
}));
|
||||
|
||||
vi.doMock("./list.provider-index-catalog.js", () => ({
|
||||
loadProviderIndexCatalogRowsForList: mocks.loadProviderIndexCatalogRowsForList,
|
||||
}));
|
||||
|
||||
vi.doMock("./list.registry-load.js", () => ({
|
||||
loadListModelRegistry: async (
|
||||
cfg: unknown,
|
||||
@@ -190,14 +197,27 @@ function installModelsListCommandForwardCompatMocks() {
|
||||
},
|
||||
}));
|
||||
|
||||
vi.doMock("./list.runtime.js", () => ({
|
||||
ensureOpenClawModelsJson: mocks.ensureOpenClawModelsJson,
|
||||
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
|
||||
vi.doMock("../../agents/auth-profiles/store.js", () => ({
|
||||
loadAuthProfileStoreWithoutExternalProfiles: mocks.ensureAuthProfileStore,
|
||||
}));
|
||||
|
||||
vi.doMock("../../agents/agent-paths.js", () => ({
|
||||
resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir,
|
||||
}));
|
||||
|
||||
vi.doMock("../../agents/auth-profiles/profile-list.js", () => ({
|
||||
listProfilesForProvider: mocks.listProfilesForProvider,
|
||||
}));
|
||||
|
||||
vi.doMock("../../agents/model-catalog.js", () => ({
|
||||
loadModelCatalog: mocks.loadModelCatalog,
|
||||
loadProviderCatalogModelsForList: mocks.loadProviderCatalogModelsForList,
|
||||
}));
|
||||
|
||||
vi.doMock("../../agents/pi-embedded-runner/model.js", () => ({
|
||||
resolveModelWithRegistry: mocks.resolveModelWithRegistry,
|
||||
}));
|
||||
|
||||
vi.doMock("../../agents/model-auth.js", () => ({
|
||||
resolveEnvApiKey: vi.fn().mockReturnValue(undefined),
|
||||
resolveAwsSdkEnvVarName: vi.fn().mockReturnValue(undefined),
|
||||
hasUsableCustomProviderApiKey: vi.fn().mockReturnValue(false),
|
||||
@@ -232,7 +252,7 @@ async function buildAllOpenAiCodexRows(opts: { supplementCatalog?: boolean } = {
|
||||
),
|
||||
filter: { provider: "openai-codex" },
|
||||
};
|
||||
const seenKeys = listRowsModule.appendDiscoveredRows({
|
||||
const seenKeys = await listRowsModule.appendDiscoveredRows({
|
||||
rows: rows as never,
|
||||
models: loaded.models as never,
|
||||
modelRegistry: loaded.registry as never,
|
||||
@@ -256,17 +276,14 @@ beforeEach(() => {
|
||||
|
||||
describe("modelsListCommand forward-compat", () => {
|
||||
describe("configured rows", () => {
|
||||
it("passes provider filters into registry loading before row assembly", async () => {
|
||||
it("keeps configured provider filters on the registry-free row path", async () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true, provider: "moonshot" }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).toHaveBeenCalledWith(
|
||||
mocks.resolvedConfig,
|
||||
expect.objectContaining({
|
||||
providerFilter: "moonshot",
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.printModelTable).not.toHaveBeenCalled();
|
||||
expect(runtime.log).toHaveBeenCalledWith("No models found.");
|
||||
});
|
||||
|
||||
it("does not mark configured codex model as missing when forward-compat can build a fallback", async () => {
|
||||
@@ -298,7 +315,6 @@ describe("modelsListCommand forward-compat", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.resolveModelWithRegistry.mockReturnValueOnce({ ...OPENAI_CODEX_MINI_MODEL });
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true }, runtime as never);
|
||||
@@ -327,7 +343,6 @@ describe("modelsListCommand forward-compat", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
mocks.resolveModelWithRegistry.mockReturnValueOnce({ ...OPENAI_CODEX_PRO_MODEL });
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true }, runtime as never);
|
||||
@@ -345,17 +360,12 @@ describe("modelsListCommand forward-compat", () => {
|
||||
expect(codexPro?.tags).not.toContain("missing");
|
||||
});
|
||||
|
||||
it("loads model registry without source config persistence input", async () => {
|
||||
it("does not load the model registry for configured-mode listing", async () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ json: true }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).toHaveBeenCalledWith(
|
||||
mocks.resolvedConfig,
|
||||
expect.not.objectContaining({
|
||||
sourceConfig: expect.anything(),
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps configured local openai gpt-5.4 entries visible in --local output", async () => {
|
||||
@@ -416,11 +426,6 @@ describe("modelsListCommand forward-compat", () => {
|
||||
it("does not require the all-model registry result for configured-mode listing", async () => {
|
||||
const previousExitCode = process.exitCode;
|
||||
process.exitCode = undefined;
|
||||
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||
models: [],
|
||||
availableKeys: new Set<string>(),
|
||||
registry: undefined,
|
||||
});
|
||||
const runtime = createRuntime();
|
||||
let observedExitCode: number | undefined;
|
||||
|
||||
@@ -433,6 +438,7 @@ describe("modelsListCommand forward-compat", () => {
|
||||
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(observedExitCode).toBeUndefined();
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.printModelTable).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -509,12 +515,27 @@ describe("modelsListCommand forward-compat", () => {
|
||||
|
||||
it("uses provider index preview rows when an installable provider is not installed", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(false);
|
||||
mocks.loadProviderIndexCatalogRowsForList.mockReturnValueOnce([
|
||||
{
|
||||
provider: "moonshot",
|
||||
id: "kimi-k2.6",
|
||||
ref: "moonshot/kimi-k2.6",
|
||||
mergeKey: "moonshot::kimi-k2.6",
|
||||
name: "Kimi K2.6",
|
||||
source: "provider-index",
|
||||
input: ["text", "image"],
|
||||
reasoning: false,
|
||||
status: "available",
|
||||
baseUrl: "https://api.moonshot.ai/v1",
|
||||
contextWindow: 262_144,
|
||||
},
|
||||
]);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ all: true, provider: "moonshot", json: true }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(mocks.hasProviderStaticCatalogForFilter).not.toHaveBeenCalled();
|
||||
expect(mocks.loadProviderCatalogModelsForList).not.toHaveBeenCalled();
|
||||
expect(lastPrintedRows<{ key: string; available: boolean }>()).toEqual([
|
||||
expect.objectContaining({
|
||||
@@ -568,23 +589,20 @@ describe("modelsListCommand forward-compat", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the registry path for provider filters without static catalog coverage", async () => {
|
||||
it("does not fall back to the registry for provider filters without catalog coverage", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(false);
|
||||
const runtime = createRuntime();
|
||||
|
||||
await modelsListCommand({ all: true, provider: "openrouter", json: true }, runtime as never);
|
||||
|
||||
expect(mocks.loadModelRegistry).toHaveBeenCalledWith(
|
||||
mocks.resolvedConfig,
|
||||
expect.objectContaining({
|
||||
providerFilter: "openrouter",
|
||||
}),
|
||||
);
|
||||
expect(mocks.loadModelRegistry).not.toHaveBeenCalled();
|
||||
expect(runtime.log).toHaveBeenCalledWith("No models found.");
|
||||
});
|
||||
|
||||
it("includes provider-owned supplemental catalog rows with provider filters", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true);
|
||||
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||
models: [],
|
||||
availableKeys: new Set(["opencode-go/deepseek-v4-pro"]),
|
||||
@@ -679,6 +697,7 @@ describe("modelsListCommand forward-compat", () => {
|
||||
|
||||
it("uses provider runtime metadata for discovered codex gpt-5.5 rows", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true);
|
||||
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||
models: [
|
||||
{
|
||||
@@ -748,7 +767,7 @@ describe("modelsListCommand forward-compat", () => {
|
||||
it("suppresses direct openai gpt-5.3-codex-spark rows in --all output", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
const rows: unknown[] = [];
|
||||
listRowsModule.appendDiscoveredRows({
|
||||
await listRowsModule.appendDiscoveredRows({
|
||||
rows: rows as never,
|
||||
models: [
|
||||
{
|
||||
@@ -796,6 +815,7 @@ describe("modelsListCommand forward-compat", () => {
|
||||
describe("provider filter canonicalization", () => {
|
||||
it("matches alias-valued discovered providers against canonical provider filters", async () => {
|
||||
mocks.resolveConfiguredEntries.mockReturnValueOnce({ entries: [] });
|
||||
mocks.hasProviderStaticCatalogForFilter.mockResolvedValueOnce(true);
|
||||
mocks.loadModelRegistry.mockResolvedValueOnce({
|
||||
models: [
|
||||
{
|
||||
|
||||
@@ -5,13 +5,6 @@ import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
|
||||
import { resolveConfiguredEntries } from "./list.configured.js";
|
||||
import { formatErrorWithStack } from "./list.errors.js";
|
||||
import { hasProviderStaticCatalogForFilter } from "./list.provider-catalog.js";
|
||||
import { loadConfiguredListModelRegistry, loadListModelRegistry } from "./list.registry-load.js";
|
||||
import {
|
||||
appendAllModelRowSources,
|
||||
appendConfiguredModelRowSources,
|
||||
modelRowSourcesRequireRegistry,
|
||||
} from "./list.row-sources.js";
|
||||
import { printModelTable } from "./list.table.js";
|
||||
import type { ModelRow } from "./list.types.js";
|
||||
import { loadModelsConfigWithSource } from "./load-config.js";
|
||||
@@ -19,6 +12,45 @@ import { DEFAULT_PROVIDER, ensureFlagCompatibility } from "./shared.js";
|
||||
|
||||
const DISPLAY_MODEL_PARSE_OPTIONS = { allowPluginNormalization: false } as const;
|
||||
|
||||
type RegistryLoadModule = typeof import("./list.registry-load.js");
|
||||
type RowSourcesModule = typeof import("./list.row-sources.js");
|
||||
type ProviderCatalogModule = typeof import("./list.provider-catalog.js");
|
||||
|
||||
let registryLoadModulePromise: Promise<RegistryLoadModule> | undefined;
|
||||
let rowSourcesModulePromise: Promise<RowSourcesModule> | undefined;
|
||||
let providerCatalogModulePromise: Promise<ProviderCatalogModule> | undefined;
|
||||
|
||||
function loadRegistryLoadModule(): Promise<RegistryLoadModule> {
|
||||
registryLoadModulePromise ??= import("./list.registry-load.js");
|
||||
return registryLoadModulePromise;
|
||||
}
|
||||
|
||||
function loadRowSourcesModule(): Promise<RowSourcesModule> {
|
||||
rowSourcesModulePromise ??= import("./list.row-sources.js");
|
||||
return rowSourcesModulePromise;
|
||||
}
|
||||
|
||||
function loadProviderCatalogModule(): Promise<ProviderCatalogModule> {
|
||||
providerCatalogModulePromise ??= import("./list.provider-catalog.js");
|
||||
return providerCatalogModulePromise;
|
||||
}
|
||||
|
||||
function modelRowSourcesRequireRegistry(params: {
|
||||
all?: boolean;
|
||||
providerFilter?: string;
|
||||
useManifestCatalogFastPath: boolean;
|
||||
useProviderCatalogFastPath: boolean;
|
||||
useProviderIndexCatalogFastPath: boolean;
|
||||
}): boolean {
|
||||
if (!params.all) {
|
||||
return false;
|
||||
}
|
||||
if (params.providerFilter) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function modelsListCommand(
|
||||
opts: {
|
||||
all?: boolean;
|
||||
@@ -48,12 +80,16 @@ export async function modelsListCommand(
|
||||
if (providerFilter === null) {
|
||||
return;
|
||||
}
|
||||
const { ensureAuthProfileStore, resolveOpenClawAgentDir } = await import("./list.runtime.js");
|
||||
const [{ loadAuthProfileStoreWithoutExternalProfiles }, { resolveOpenClawAgentDir }] =
|
||||
await Promise.all([
|
||||
import("../../agents/auth-profiles/store.js"),
|
||||
import("../../agents/agent-paths.js"),
|
||||
]);
|
||||
const { resolvedConfig: cfg } = await loadModelsConfigWithSource({
|
||||
commandName: "models list",
|
||||
runtime,
|
||||
});
|
||||
const authStore = ensureAuthProfileStore();
|
||||
const authStore = loadAuthProfileStoreWithoutExternalProfiles();
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
|
||||
let modelRegistry: ModelRegistry | undefined;
|
||||
@@ -69,16 +105,24 @@ export async function modelsListCommand(
|
||||
manifestCatalogRows = loadStaticManifestCatalogRowsForList({ cfg, providerFilter });
|
||||
}
|
||||
const useManifestCatalogFastPath = manifestCatalogRows.length > 0;
|
||||
const useProviderCatalogFastPath =
|
||||
!useManifestCatalogFastPath && opts.all && providerFilter
|
||||
? await hasProviderStaticCatalogForFilter({ cfg, providerFilter })
|
||||
: false;
|
||||
if (!useManifestCatalogFastPath && !useProviderCatalogFastPath && opts.all && providerFilter) {
|
||||
if (!useManifestCatalogFastPath && opts.all && providerFilter) {
|
||||
const { loadProviderIndexCatalogRowsForList } =
|
||||
await import("./list.provider-index-catalog.js");
|
||||
providerIndexCatalogRows = loadProviderIndexCatalogRowsForList({ cfg, providerFilter });
|
||||
}
|
||||
const useProviderIndexCatalogFastPath = providerIndexCatalogRows.length > 0;
|
||||
const useProviderCatalogFastPath = await (async () => {
|
||||
if (
|
||||
useManifestCatalogFastPath ||
|
||||
useProviderIndexCatalogFastPath ||
|
||||
!opts.all ||
|
||||
!providerFilter
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const { hasProviderStaticCatalogForFilter } = await loadProviderCatalogModule();
|
||||
return hasProviderStaticCatalogForFilter({ cfg, providerFilter });
|
||||
})();
|
||||
const shouldLoadRegistry = modelRowSourcesRequireRegistry({
|
||||
all: opts.all,
|
||||
providerFilter,
|
||||
@@ -87,6 +131,7 @@ export async function modelsListCommand(
|
||||
useProviderIndexCatalogFastPath,
|
||||
});
|
||||
const loadRegistryState = async () => {
|
||||
const { loadListModelRegistry } = await loadRegistryLoadModule();
|
||||
const loaded = await loadListModelRegistry(cfg, { providerFilter });
|
||||
modelRegistry = loaded.registry;
|
||||
discoveredKeys = loaded.discoveredKeys;
|
||||
@@ -96,7 +141,8 @@ export async function modelsListCommand(
|
||||
try {
|
||||
if (shouldLoadRegistry) {
|
||||
await loadRegistryState();
|
||||
} else if (!opts.all) {
|
||||
} else if (!opts.all && opts.local) {
|
||||
const { loadConfiguredListModelRegistry } = await loadRegistryLoadModule();
|
||||
const loaded = loadConfiguredListModelRegistry(cfg, entries, { providerFilter });
|
||||
modelRegistry = loaded.registry;
|
||||
discoveredKeys = loaded.discoveredKeys;
|
||||
@@ -123,6 +169,7 @@ export async function modelsListCommand(
|
||||
const rows: ModelRow[] = [];
|
||||
|
||||
if (opts.all) {
|
||||
const { appendAllModelRowSources } = await loadRowSourcesModule();
|
||||
let rowContext = buildRowContext(
|
||||
useManifestCatalogFastPath || useProviderCatalogFastPath || useProviderIndexCatalogFastPath,
|
||||
);
|
||||
@@ -158,17 +205,12 @@ export async function modelsListCommand(
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const registry = modelRegistry;
|
||||
if (!registry) {
|
||||
runtime.error("Model registry unavailable.");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
appendConfiguredModelRowSources({
|
||||
const { appendConfiguredModelRowSources } = await loadRowSourcesModule();
|
||||
await appendConfiguredModelRowSources({
|
||||
rows,
|
||||
entries,
|
||||
modelRegistry: registry,
|
||||
context: buildRowContext(false),
|
||||
modelRegistry,
|
||||
context: buildRowContext(!modelRegistry),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "./list.provider-catalog.js";
|
||||
|
||||
const providerDiscoveryMocks = vi.hoisted(() => ({
|
||||
loadPluginRegistrySnapshot: vi.fn(),
|
||||
loadPluginRegistrySnapshotWithMetadata: vi.fn(),
|
||||
resolvePluginContributionOwners: vi.fn(),
|
||||
resolveProviderOwners: vi.fn(),
|
||||
resolveBundledProviderCompatPluginIds: vi.fn(),
|
||||
@@ -17,7 +17,8 @@ const providerDiscoveryMocks = vi.hoisted(() => ({
|
||||
|
||||
vi.mock("../../plugins/plugin-registry.js", () => ({
|
||||
loadPluginManifestRegistryForPluginRegistry: () => ({ diagnostics: [], plugins: [] }),
|
||||
loadPluginRegistrySnapshot: providerDiscoveryMocks.loadPluginRegistrySnapshot,
|
||||
loadPluginRegistrySnapshotWithMetadata:
|
||||
providerDiscoveryMocks.loadPluginRegistrySnapshotWithMetadata,
|
||||
resolvePluginContributionOwners: providerDiscoveryMocks.resolvePluginContributionOwners,
|
||||
resolveProviderOwners: providerDiscoveryMocks.resolveProviderOwners,
|
||||
}));
|
||||
@@ -115,8 +116,11 @@ const defaultProviders = [chutesProvider, moonshotProvider, openaiProvider];
|
||||
describe("loadProviderCatalogModelsForList", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
providerDiscoveryMocks.loadPluginRegistrySnapshot.mockReturnValue({
|
||||
plugins: [],
|
||||
providerDiscoveryMocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "persisted",
|
||||
snapshot: {
|
||||
plugins: [],
|
||||
},
|
||||
diagnostics: [],
|
||||
});
|
||||
providerDiscoveryMocks.resolveProviderOwners.mockImplementation(
|
||||
@@ -197,9 +201,10 @@ describe("loadProviderCatalogModelsForList", () => {
|
||||
}),
|
||||
).resolves.toEqual(["moonshot"]);
|
||||
|
||||
expect(providerDiscoveryMocks.loadPluginRegistrySnapshot).toHaveBeenCalledWith({
|
||||
expect(providerDiscoveryMocks.loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledWith({
|
||||
config: baseParams.cfg,
|
||||
env: baseParams.env,
|
||||
cache: true,
|
||||
});
|
||||
expect(providerDiscoveryMocks.resolveOwningPluginIdsForProvider).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import {
|
||||
loadPluginRegistrySnapshot,
|
||||
loadPluginRegistrySnapshotWithMetadata,
|
||||
resolvePluginContributionOwners,
|
||||
resolveProviderOwners,
|
||||
type PluginRegistrySnapshot,
|
||||
@@ -70,10 +70,15 @@ function resolveInstalledIndexPluginIdsForProviderFilter(params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
providerFilter: string;
|
||||
}): string[] | undefined {
|
||||
const index = loadPluginRegistrySnapshot({
|
||||
const snapshot = loadPluginRegistrySnapshotWithMetadata({
|
||||
config: params.cfg,
|
||||
env: params.env,
|
||||
cache: true,
|
||||
});
|
||||
if (snapshot.source !== "persisted" && snapshot.source !== "provided") {
|
||||
return [];
|
||||
}
|
||||
const index = snapshot.snapshot;
|
||||
const pluginIds = [
|
||||
...collectMatchingContributionOwners(index, "providers", params.providerFilter, params.cfg),
|
||||
...collectMatchingContributionOwners(index, "cliBackends", params.providerFilter, params.cfg),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js";
|
||||
import { shouldSuppressBuiltInModel } from "../../agents/model-suppression.js";
|
||||
import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { loadModelRegistry } from "./list.registry.js";
|
||||
import { discoverAuthStorage, discoverModels, resolveOpenClawAgentDir } from "./list.runtime.js";
|
||||
import type { ConfiguredEntry } from "./list.types.js";
|
||||
import { modelKey } from "./shared.js";
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveOpenClawAgentDir } from "../../agents/agent-paths.js";
|
||||
import { listProfilesForProvider } from "../../agents/auth-profiles/profile-list.js";
|
||||
import type { AuthProfileStore } from "../../agents/auth-profiles/types.js";
|
||||
import {
|
||||
hasUsableCustomProviderApiKey,
|
||||
resolveAwsSdkEnvVarName,
|
||||
resolveEnvApiKey,
|
||||
} from "../../agents/model-auth.js";
|
||||
import { shouldSuppressBuiltInModel } from "../../agents/model-suppression.js";
|
||||
import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resolveRuntimeSyntheticAuthProviderRefs } from "../../plugins/synthetic-auth.runtime.js";
|
||||
import {
|
||||
@@ -10,15 +18,6 @@ import {
|
||||
shouldFallbackToAuthHeuristics,
|
||||
} from "./list.errors.js";
|
||||
import { toModelRow as toModelRowBase } from "./list.model-row.js";
|
||||
import {
|
||||
discoverAuthStorage,
|
||||
discoverModels,
|
||||
hasUsableCustomProviderApiKey,
|
||||
listProfilesForProvider,
|
||||
resolveAwsSdkEnvVarName,
|
||||
resolveEnvApiKey,
|
||||
resolveOpenClawAgentDir,
|
||||
} from "./list.runtime.js";
|
||||
import type { ModelRow } from "./list.types.js";
|
||||
import { modelKey } from "./shared.js";
|
||||
|
||||
|
||||
@@ -37,12 +37,7 @@ export function modelRowSourcesRequireRegistry(params: {
|
||||
if (!params.all) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.providerFilter &&
|
||||
(params.useManifestCatalogFastPath ||
|
||||
params.useProviderCatalogFastPath ||
|
||||
params.useProviderIndexCatalogFastPath)
|
||||
) {
|
||||
if (params.providerFilter) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -58,14 +53,14 @@ export async function appendAllModelRowSources(
|
||||
params.useProviderIndexCatalogFastPath)
|
||||
) {
|
||||
let seenKeys = new Set<string>();
|
||||
appendConfiguredProviderRows({
|
||||
await appendConfiguredProviderRows({
|
||||
rows: params.rows,
|
||||
context: params.context,
|
||||
seenKeys,
|
||||
});
|
||||
let catalogRows = 0;
|
||||
if (params.useManifestCatalogFastPath) {
|
||||
catalogRows = appendManifestCatalogRows({
|
||||
catalogRows = await appendManifestCatalogRows({
|
||||
rows: params.rows,
|
||||
context: params.context,
|
||||
seenKeys,
|
||||
@@ -81,7 +76,7 @@ export async function appendAllModelRowSources(
|
||||
});
|
||||
}
|
||||
if (catalogRows === 0 && params.useProviderIndexCatalogFastPath) {
|
||||
catalogRows = appendModelCatalogRows({
|
||||
catalogRows = await appendModelCatalogRows({
|
||||
rows: params.rows,
|
||||
context: params.context,
|
||||
seenKeys,
|
||||
@@ -92,7 +87,7 @@ export async function appendAllModelRowSources(
|
||||
if (!params.modelRegistry) {
|
||||
return { requiresRegistryFallback: true };
|
||||
}
|
||||
appendDiscoveredRows({
|
||||
await appendDiscoveredRows({
|
||||
rows: params.rows,
|
||||
models: params.modelRegistry.getAll(),
|
||||
modelRegistry: params.modelRegistry,
|
||||
@@ -102,14 +97,14 @@ export async function appendAllModelRowSources(
|
||||
return { requiresRegistryFallback: false };
|
||||
}
|
||||
|
||||
const seenKeys = appendDiscoveredRows({
|
||||
const seenKeys = await appendDiscoveredRows({
|
||||
rows: params.rows,
|
||||
models: params.modelRegistry?.getAll() ?? [],
|
||||
modelRegistry: params.modelRegistry,
|
||||
context: params.context,
|
||||
});
|
||||
|
||||
appendConfiguredProviderRows({
|
||||
await appendConfiguredProviderRows({
|
||||
rows: params.rows,
|
||||
context: params.context,
|
||||
seenKeys,
|
||||
@@ -133,11 +128,11 @@ export async function appendAllModelRowSources(
|
||||
return { requiresRegistryFallback: false };
|
||||
}
|
||||
|
||||
export function appendConfiguredModelRowSources(params: {
|
||||
export async function appendConfiguredModelRowSources(params: {
|
||||
rows: ModelRow[];
|
||||
entries: ConfiguredEntry[];
|
||||
modelRegistry: ModelRegistry;
|
||||
modelRegistry?: ModelRegistry;
|
||||
context: RowBuilderContext;
|
||||
}): void {
|
||||
appendConfiguredRows(params);
|
||||
}): Promise<void> {
|
||||
await appendConfiguredRows(params);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,15 @@ vi.mock("../../agents/model-suppression.js", () => ({
|
||||
shouldSuppressBuiltInModel: mocks.shouldSuppressBuiltInModel,
|
||||
}));
|
||||
|
||||
vi.mock("./list.runtime.js", () => ({
|
||||
vi.mock("./list.provider-catalog.js", () => ({
|
||||
loadProviderCatalogModelsForList: mocks.loadProviderCatalogModelsForList,
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/auth-profiles/profile-list.js", () => ({
|
||||
listProfilesForProvider: mocks.listProfilesForProvider,
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/model-auth.js", () => ({
|
||||
resolveAwsSdkEnvVarName: vi.fn().mockReturnValue(undefined),
|
||||
resolveEnvApiKey: vi.fn().mockReturnValue(null),
|
||||
hasUsableCustomProviderApiKey: vi.fn().mockReturnValue(false),
|
||||
|
||||
@@ -2,22 +2,27 @@ import type { Api, Model } from "@mariozechner/pi-ai";
|
||||
import type { ModelRegistry } from "@mariozechner/pi-coding-agent";
|
||||
import type { AuthProfileStore } from "../../agents/auth-profiles/types.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import {
|
||||
hasUsableCustomProviderApiKey,
|
||||
resolveAwsSdkEnvVarName,
|
||||
resolveEnvApiKey,
|
||||
} from "../../agents/model-auth.js";
|
||||
import { shouldSuppressBuiltInModel } from "../../agents/model-suppression.js";
|
||||
import { normalizeProviderId } from "../../agents/provider-id.js";
|
||||
import type { ModelDefinitionConfig, ModelProviderConfig } from "../../config/types.models.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { NormalizedModelCatalogRow } from "../../model-catalog/index.js";
|
||||
import type { ListRowModel } from "./list.model-row.js";
|
||||
import { toModelRow } from "./list.registry.js";
|
||||
import {
|
||||
loadModelCatalog,
|
||||
loadProviderCatalogModelsForList,
|
||||
resolveModelWithRegistry,
|
||||
} from "./list.runtime.js";
|
||||
import { toModelRow } from "./list.model-row.js";
|
||||
import type { ConfiguredEntry, ModelRow } from "./list.types.js";
|
||||
import { isLocalBaseUrl, modelKey } from "./shared.js";
|
||||
|
||||
type ConfiguredByKey = Map<string, ConfiguredEntry>;
|
||||
type ModelCatalogModule = typeof import("../../agents/model-catalog.js");
|
||||
type ModelResolverModule = typeof import("../../agents/pi-embedded-runner/model.js");
|
||||
type ProfileListModule = typeof import("../../agents/auth-profiles/profile-list.js");
|
||||
type ProviderCatalogModule = typeof import("./list.provider-catalog.js");
|
||||
type SyntheticAuthModule = typeof import("../../plugins/synthetic-auth.runtime.js");
|
||||
|
||||
type RowFilter = {
|
||||
provider?: string;
|
||||
@@ -35,6 +40,37 @@ export type RowBuilderContext = {
|
||||
skipRuntimeModelSuppression?: boolean;
|
||||
};
|
||||
|
||||
let modelCatalogModulePromise: Promise<ModelCatalogModule> | undefined;
|
||||
let modelResolverModulePromise: Promise<ModelResolverModule> | undefined;
|
||||
let profileListModulePromise: Promise<ProfileListModule> | undefined;
|
||||
let providerCatalogModulePromise: Promise<ProviderCatalogModule> | undefined;
|
||||
let syntheticAuthModulePromise: Promise<SyntheticAuthModule> | undefined;
|
||||
|
||||
function loadModelCatalogModule(): Promise<ModelCatalogModule> {
|
||||
modelCatalogModulePromise ??= import("../../agents/model-catalog.js");
|
||||
return modelCatalogModulePromise;
|
||||
}
|
||||
|
||||
function loadModelResolverModule(): Promise<ModelResolverModule> {
|
||||
modelResolverModulePromise ??= import("../../agents/pi-embedded-runner/model.js");
|
||||
return modelResolverModulePromise;
|
||||
}
|
||||
|
||||
function loadProfileListModule(): Promise<ProfileListModule> {
|
||||
profileListModulePromise ??= import("../../agents/auth-profiles/profile-list.js");
|
||||
return profileListModulePromise;
|
||||
}
|
||||
|
||||
function loadProviderCatalogModule(): Promise<ProviderCatalogModule> {
|
||||
providerCatalogModulePromise ??= import("./list.provider-catalog.js");
|
||||
return providerCatalogModulePromise;
|
||||
}
|
||||
|
||||
function loadSyntheticAuthModule(): Promise<SyntheticAuthModule> {
|
||||
syntheticAuthModulePromise ??= import("../../plugins/synthetic-auth.runtime.js");
|
||||
return syntheticAuthModulePromise;
|
||||
}
|
||||
|
||||
function matchesRowFilter(filter: RowFilter, model: { provider: string; baseUrl?: string }) {
|
||||
if (filter.provider && normalizeProviderId(model.provider) !== filter.provider) {
|
||||
return false;
|
||||
@@ -45,13 +81,44 @@ function matchesRowFilter(filter: RowFilter, model: { provider: string; baseUrl?
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildRow(params: {
|
||||
async function hasAuthForProvider(params: {
|
||||
provider: string;
|
||||
cfg: OpenClawConfig;
|
||||
authStore: AuthProfileStore;
|
||||
}): Promise<boolean> {
|
||||
const { listProfilesForProvider } = await loadProfileListModule();
|
||||
if (listProfilesForProvider(params.authStore, params.provider).length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (params.provider === "amazon-bedrock" && resolveAwsSdkEnvVarName()) {
|
||||
return true;
|
||||
}
|
||||
if (resolveEnvApiKey(params.provider)) {
|
||||
return true;
|
||||
}
|
||||
if (hasUsableCustomProviderApiKey(params.cfg, params.provider)) {
|
||||
return true;
|
||||
}
|
||||
const { resolveRuntimeSyntheticAuthProviderRefs } = await loadSyntheticAuthModule();
|
||||
return resolveRuntimeSyntheticAuthProviderRefs().includes(params.provider);
|
||||
}
|
||||
|
||||
async function buildRow(params: {
|
||||
model: ListRowModel;
|
||||
key: string;
|
||||
context: RowBuilderContext;
|
||||
allowProviderAvailabilityFallback?: boolean;
|
||||
}): ModelRow {
|
||||
}): Promise<ModelRow> {
|
||||
const configured = params.context.configuredByKey.get(params.key);
|
||||
const shouldResolveProviderAuth =
|
||||
params.context.availableKeys === undefined || params.allowProviderAvailabilityFallback === true;
|
||||
const hasProviderAuth = shouldResolveProviderAuth
|
||||
? await hasAuthForProvider({
|
||||
provider: params.model.provider,
|
||||
cfg: params.context.cfg,
|
||||
authStore: params.context.authStore,
|
||||
})
|
||||
: false;
|
||||
return toModelRow({
|
||||
model: params.model,
|
||||
key: params.key,
|
||||
@@ -61,6 +128,7 @@ function buildRow(params: {
|
||||
cfg: params.context.cfg,
|
||||
authStore: params.context.authStore,
|
||||
allowProviderAvailabilityFallback: params.allowProviderAvailabilityFallback ?? false,
|
||||
hasAuthForProvider: shouldResolveProviderAuth ? () => hasProviderAuth : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,14 +147,14 @@ function shouldSuppressListModel(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function appendVisibleRow(params: {
|
||||
async function appendVisibleRow(params: {
|
||||
rows: ModelRow[];
|
||||
model: ListRowModel;
|
||||
key: string;
|
||||
context: RowBuilderContext;
|
||||
seenKeys?: Set<string>;
|
||||
allowProviderAvailabilityFallback?: boolean;
|
||||
}): boolean {
|
||||
}): Promise<boolean> {
|
||||
if (params.seenKeys?.has(params.key)) {
|
||||
return false;
|
||||
}
|
||||
@@ -97,7 +165,7 @@ function appendVisibleRow(params: {
|
||||
return false;
|
||||
}
|
||||
params.rows.push(
|
||||
buildRow({
|
||||
await buildRow({
|
||||
model: params.model,
|
||||
key: params.key,
|
||||
context: params.context,
|
||||
@@ -153,13 +221,49 @@ function shouldListConfiguredProviderModel(params: {
|
||||
return params.providerConfig.api !== undefined || params.model.api !== undefined;
|
||||
}
|
||||
|
||||
export function appendDiscoveredRows(params: {
|
||||
function findConfiguredProviderModel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
}): ListRowModel | undefined {
|
||||
const providerConfig = params.cfg.models?.providers?.[params.provider];
|
||||
const configuredModel = providerConfig?.models?.find((model) => model.id === params.modelId);
|
||||
if (!providerConfig || !configuredModel) {
|
||||
return undefined;
|
||||
}
|
||||
return toConfiguredProviderListModel({
|
||||
provider: params.provider,
|
||||
providerConfig,
|
||||
model: configuredModel,
|
||||
});
|
||||
}
|
||||
|
||||
function toFallbackConfiguredListModel(entry: ConfiguredEntry, cfg: OpenClawConfig): ListRowModel {
|
||||
return (
|
||||
findConfiguredProviderModel({
|
||||
cfg,
|
||||
provider: entry.ref.provider,
|
||||
modelId: entry.ref.model,
|
||||
}) ?? {
|
||||
provider: entry.ref.provider,
|
||||
id: entry.ref.model,
|
||||
name: entry.ref.model,
|
||||
input: ["text"],
|
||||
contextWindow: DEFAULT_CONTEXT_TOKENS,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function appendDiscoveredRows(params: {
|
||||
rows: ModelRow[];
|
||||
models: Model<Api>[];
|
||||
modelRegistry?: ModelRegistry;
|
||||
context: RowBuilderContext;
|
||||
}): Set<string> {
|
||||
}): Promise<Set<string>> {
|
||||
const seenKeys = new Set<string>();
|
||||
const modelResolver = params.modelRegistry
|
||||
? (await loadModelResolverModule()).resolveModelWithRegistry
|
||||
: undefined;
|
||||
const sorted = [...params.models].toSorted((a, b) => {
|
||||
const providerCompare = a.provider.localeCompare(b.provider);
|
||||
if (providerCompare !== 0) {
|
||||
@@ -170,20 +274,21 @@ export function appendDiscoveredRows(params: {
|
||||
|
||||
for (const model of sorted) {
|
||||
const key = modelKey(model.provider, model.id);
|
||||
const resolvedModel = params.modelRegistry
|
||||
? resolveModelWithRegistry({
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
modelRegistry: params.modelRegistry,
|
||||
cfg: params.context.cfg,
|
||||
agentDir: params.context.agentDir,
|
||||
})
|
||||
: undefined;
|
||||
const resolvedModel =
|
||||
params.modelRegistry && modelResolver
|
||||
? modelResolver({
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
modelRegistry: params.modelRegistry,
|
||||
cfg: params.context.cfg,
|
||||
agentDir: params.context.agentDir,
|
||||
})
|
||||
: undefined;
|
||||
const rowModel =
|
||||
resolvedModel && modelKey(resolvedModel.provider, resolvedModel.id) === key
|
||||
? resolvedModel
|
||||
: model;
|
||||
appendVisibleRow({
|
||||
await appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model: rowModel,
|
||||
key,
|
||||
@@ -195,11 +300,11 @@ export function appendDiscoveredRows(params: {
|
||||
return seenKeys;
|
||||
}
|
||||
|
||||
export function appendConfiguredProviderRows(params: {
|
||||
export async function appendConfiguredProviderRows(params: {
|
||||
rows: ModelRow[];
|
||||
context: RowBuilderContext;
|
||||
seenKeys: Set<string>;
|
||||
}): void {
|
||||
}): Promise<void> {
|
||||
for (const [provider, providerConfig] of Object.entries(
|
||||
params.context.cfg.models?.providers ?? {},
|
||||
)) {
|
||||
@@ -213,7 +318,7 @@ export function appendConfiguredProviderRows(params: {
|
||||
providerConfig,
|
||||
model: configuredModel,
|
||||
});
|
||||
appendVisibleRow({
|
||||
await appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model,
|
||||
key,
|
||||
@@ -225,17 +330,17 @@ export function appendConfiguredProviderRows(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export function appendModelCatalogRows(params: {
|
||||
export async function appendModelCatalogRows(params: {
|
||||
rows: ModelRow[];
|
||||
context: RowBuilderContext;
|
||||
seenKeys: Set<string>;
|
||||
catalogRows: readonly NormalizedModelCatalogRow[];
|
||||
}): number {
|
||||
}): Promise<number> {
|
||||
let appended = 0;
|
||||
for (const catalogRow of params.catalogRows) {
|
||||
const key = modelKey(catalogRow.provider, catalogRow.id);
|
||||
if (
|
||||
appendVisibleRow({
|
||||
await appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model: toManifestCatalogListModel(catalogRow),
|
||||
key,
|
||||
@@ -255,7 +360,7 @@ export function appendManifestCatalogRows(params: {
|
||||
context: RowBuilderContext;
|
||||
seenKeys: Set<string>;
|
||||
manifestRows: readonly NormalizedModelCatalogRow[];
|
||||
}): number {
|
||||
}): Promise<number> {
|
||||
return appendModelCatalogRows({
|
||||
...params,
|
||||
catalogRows: params.manifestRows,
|
||||
@@ -268,6 +373,10 @@ export async function appendCatalogSupplementRows(params: {
|
||||
context: RowBuilderContext;
|
||||
seenKeys: Set<string>;
|
||||
}): Promise<void> {
|
||||
const [{ loadModelCatalog }, { resolveModelWithRegistry }] = await Promise.all([
|
||||
loadModelCatalogModule(),
|
||||
loadModelResolverModule(),
|
||||
]);
|
||||
const catalog = await loadModelCatalog({ config: params.context.cfg, readOnly: true });
|
||||
for (const entry of catalog) {
|
||||
if (
|
||||
@@ -286,7 +395,7 @@ export async function appendCatalogSupplementRows(params: {
|
||||
if (!model) {
|
||||
continue;
|
||||
}
|
||||
appendVisibleRow({
|
||||
await appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model,
|
||||
key,
|
||||
@@ -314,6 +423,7 @@ export async function appendProviderCatalogRows(params: {
|
||||
staticOnly?: boolean;
|
||||
}): Promise<number> {
|
||||
let appended = 0;
|
||||
const { loadProviderCatalogModelsForList } = await loadProviderCatalogModule();
|
||||
for (const model of await loadProviderCatalogModelsForList({
|
||||
cfg: params.context.cfg,
|
||||
agentDir: params.context.agentDir,
|
||||
@@ -322,7 +432,7 @@ export async function appendProviderCatalogRows(params: {
|
||||
})) {
|
||||
const key = modelKey(model.provider, model.id);
|
||||
if (
|
||||
appendVisibleRow({
|
||||
await appendVisibleRow({
|
||||
rows: params.rows,
|
||||
model,
|
||||
key,
|
||||
@@ -337,12 +447,15 @@ export async function appendProviderCatalogRows(params: {
|
||||
return appended;
|
||||
}
|
||||
|
||||
export function appendConfiguredRows(params: {
|
||||
export async function appendConfiguredRows(params: {
|
||||
rows: ModelRow[];
|
||||
entries: ConfiguredEntry[];
|
||||
modelRegistry: ModelRegistry;
|
||||
modelRegistry?: ModelRegistry;
|
||||
context: RowBuilderContext;
|
||||
}) {
|
||||
}): Promise<void> {
|
||||
const resolveModelWithRegistry = params.modelRegistry
|
||||
? (await loadModelResolverModule()).resolveModelWithRegistry
|
||||
: undefined;
|
||||
for (const entry of params.entries) {
|
||||
if (
|
||||
params.context.filter.provider &&
|
||||
@@ -350,12 +463,15 @@ export function appendConfiguredRows(params: {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const model = resolveModelWithRegistry({
|
||||
provider: entry.ref.provider,
|
||||
modelId: entry.ref.model,
|
||||
modelRegistry: params.modelRegistry,
|
||||
cfg: params.context.cfg,
|
||||
});
|
||||
const model =
|
||||
params.modelRegistry && resolveModelWithRegistry
|
||||
? resolveModelWithRegistry({
|
||||
provider: entry.ref.provider,
|
||||
modelId: entry.ref.model,
|
||||
modelRegistry: params.modelRegistry,
|
||||
cfg: params.context.cfg,
|
||||
})
|
||||
: toFallbackConfiguredListModel(entry, params.context.cfg);
|
||||
if (params.context.filter.local && model && !isLocalBaseUrl(model.baseUrl ?? "")) {
|
||||
continue;
|
||||
}
|
||||
@@ -365,6 +481,17 @@ export function appendConfiguredRows(params: {
|
||||
if (model && shouldSuppressListModel({ model, context: params.context })) {
|
||||
continue;
|
||||
}
|
||||
const shouldResolveProviderAuth =
|
||||
model &&
|
||||
(params.context.availableKeys === undefined ||
|
||||
!params.context.discoveredKeys.has(modelKey(model.provider, model.id)));
|
||||
const hasProviderAuth = shouldResolveProviderAuth
|
||||
? await hasAuthForProvider({
|
||||
provider: model.provider,
|
||||
cfg: params.context.cfg,
|
||||
authStore: params.context.authStore,
|
||||
})
|
||||
: false;
|
||||
params.rows.push(
|
||||
toModelRow({
|
||||
model,
|
||||
@@ -377,6 +504,7 @@ export function appendConfiguredRows(params: {
|
||||
allowProviderAvailabilityFallback: model
|
||||
? !params.context.discoveredKeys.has(modelKey(model.provider, model.id))
|
||||
: false,
|
||||
hasAuthForProvider: shouldResolveProviderAuth ? () => hasProviderAuth : undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,26 +2,20 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getPluginRegistryState = vi.hoisted(() => vi.fn());
|
||||
const pluginRegistryMocks = vi.hoisted(() => ({
|
||||
loadPluginManifestRegistryForInstalledIndex: vi.fn(),
|
||||
loadPluginRegistrySnapshot: vi.fn((_params?: unknown) => ({ plugins: [] })),
|
||||
loadPluginRegistrySnapshotWithMetadata: vi.fn((_params?: unknown) => ({
|
||||
source: "persisted",
|
||||
snapshot: { plugins: [] },
|
||||
diagnostics: [],
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("./runtime-state.js", () => ({
|
||||
getPluginRegistryState,
|
||||
}));
|
||||
|
||||
vi.mock("./manifest-registry-installed.js", () => ({
|
||||
loadPluginManifestRegistryForInstalledIndex:
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex,
|
||||
}));
|
||||
|
||||
vi.mock("./plugin-registry.js", () => ({
|
||||
loadPluginRegistrySnapshot: pluginRegistryMocks.loadPluginRegistrySnapshot,
|
||||
loadPluginManifestRegistryForPluginRegistry: () =>
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex({
|
||||
index: pluginRegistryMocks.loadPluginRegistrySnapshot({ cache: true }),
|
||||
includeDisabled: true,
|
||||
}),
|
||||
loadPluginRegistrySnapshotWithMetadata:
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata,
|
||||
}));
|
||||
|
||||
import { resolveRuntimeSyntheticAuthProviderRefs } from "./synthetic-auth.runtime.js";
|
||||
@@ -29,19 +23,24 @@ import { resolveRuntimeSyntheticAuthProviderRefs } from "./synthetic-auth.runtim
|
||||
describe("synthetic auth runtime refs", () => {
|
||||
beforeEach(() => {
|
||||
getPluginRegistryState.mockReset();
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex
|
||||
.mockReset()
|
||||
.mockReturnValue({ plugins: [] });
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshot.mockReset().mockReturnValue({ plugins: [] });
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata.mockReset().mockReturnValue({
|
||||
source: "persisted",
|
||||
snapshot: { plugins: [] },
|
||||
diagnostics: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("uses manifest-owned synthetic auth refs before the runtime registry exists", () => {
|
||||
pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({
|
||||
plugins: [
|
||||
{ syntheticAuthRefs: [" local-provider ", "local-provider", "local-cli"] },
|
||||
{ syntheticAuthRefs: ["remote-provider"] },
|
||||
{ syntheticAuthRefs: [] },
|
||||
],
|
||||
it("uses persisted registry synthetic auth refs before the runtime registry exists", () => {
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "persisted",
|
||||
snapshot: {
|
||||
plugins: [
|
||||
{ syntheticAuthRefs: [" local-provider ", "local-provider", "local-cli"] },
|
||||
{ syntheticAuthRefs: ["remote-provider"] },
|
||||
{ syntheticAuthRefs: [] },
|
||||
],
|
||||
},
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
expect(resolveRuntimeSyntheticAuthProviderRefs()).toEqual([
|
||||
@@ -49,13 +48,27 @@ describe("synthetic auth runtime refs", () => {
|
||||
"local-cli",
|
||||
"remote-provider",
|
||||
]);
|
||||
expect(pluginRegistryMocks.loadPluginRegistrySnapshot).toHaveBeenCalledWith({ cache: true });
|
||||
expect(pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith({
|
||||
index: expect.anything(),
|
||||
includeDisabled: true,
|
||||
expect(pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledWith({
|
||||
cache: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not derive the registry just to resolve synthetic auth refs", () => {
|
||||
pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
|
||||
source: "derived",
|
||||
snapshot: {
|
||||
plugins: [
|
||||
{ syntheticAuthRefs: [" local-provider ", "local-provider", "local-cli"] },
|
||||
{ syntheticAuthRefs: ["remote-provider"] },
|
||||
{ syntheticAuthRefs: [] },
|
||||
],
|
||||
},
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
expect(resolveRuntimeSyntheticAuthProviderRefs()).toEqual([]);
|
||||
});
|
||||
|
||||
it("prefers the active runtime registry when plugins are already loaded", () => {
|
||||
getPluginRegistryState.mockReturnValue({
|
||||
activeRegistry: {
|
||||
@@ -84,7 +97,6 @@ describe("synthetic auth runtime refs", () => {
|
||||
});
|
||||
|
||||
expect(resolveRuntimeSyntheticAuthProviderRefs()).toEqual(["runtime-provider", "runtime-cli"]);
|
||||
expect(pluginRegistryMocks.loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled();
|
||||
expect(pluginRegistryMocks.loadPluginRegistrySnapshot).not.toHaveBeenCalled();
|
||||
expect(pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { normalizeProviderId } from "../agents/provider-id.js";
|
||||
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
|
||||
import { loadPluginRegistrySnapshotWithMetadata } from "./plugin-registry.js";
|
||||
import { getPluginRegistryState } from "./runtime-state.js";
|
||||
|
||||
function uniqueProviderRefs(values: readonly string[]): string[] {
|
||||
@@ -18,10 +18,12 @@ function uniqueProviderRefs(values: readonly string[]): string[] {
|
||||
}
|
||||
|
||||
function resolveManifestSyntheticAuthProviderRefs(): string[] {
|
||||
const result = loadPluginRegistrySnapshotWithMetadata({ cache: true });
|
||||
if (result.source !== "persisted" && result.source !== "provided") {
|
||||
return [];
|
||||
}
|
||||
return uniqueProviderRefs(
|
||||
loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true }).plugins.flatMap(
|
||||
(plugin) => plugin.syntheticAuthRefs ?? [],
|
||||
),
|
||||
result.snapshot.plugins.flatMap((plugin) => plugin.syntheticAuthRefs ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user