diff --git a/src/commands/models/list.list-command.forward-compat.test.ts b/src/commands/models/list.list-command.forward-compat.test.ts index 0e539e0689c..55c12fa3317 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -147,6 +147,7 @@ function installModelsListCommandForwardCompatMocks() { vi.doMock("./list.provider-catalog.js", () => ({ hasProviderStaticCatalogForFilter: mocks.hasProviderStaticCatalogForFilter, + loadProviderCatalogModelsForList: mocks.loadProviderCatalogModelsForList, })); vi.doMock("./list.manifest-catalog.js", () => ({ @@ -190,14 +191,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 +246,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 +270,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 () => { @@ -345,17 +356,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 () => { @@ -568,19 +574,15 @@ 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 () => { @@ -748,7 +750,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: [ { diff --git a/src/commands/models/list.list-command.ts b/src/commands/models/list.list-command.ts index be3f411c0d5..52cba5f94e5 100644 --- a/src/commands/models/list.list-command.ts +++ b/src/commands/models/list.list-command.ts @@ -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 | undefined; +let rowSourcesModulePromise: Promise | undefined; +let providerCatalogModulePromise: Promise | undefined; + +function loadRegistryLoadModule(): Promise { + registryLoadModulePromise ??= import("./list.registry-load.js"); + return registryLoadModulePromise; +} + +function loadRowSourcesModule(): Promise { + rowSourcesModulePromise ??= import("./list.row-sources.js"); + return rowSourcesModulePromise; +} + +function loadProviderCatalogModule(): Promise { + 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), }); } diff --git a/src/commands/models/list.registry-load.ts b/src/commands/models/list.registry-load.ts index e4114be6a04..d7e683c3c38 100644 --- a/src/commands/models/list.registry-load.ts +++ b/src/commands/models/list.registry-load.ts @@ -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"; diff --git a/src/commands/models/list.registry.ts b/src/commands/models/list.registry.ts index 29b4e67ddd5..972b8ab43ac 100644 --- a/src/commands/models/list.registry.ts +++ b/src/commands/models/list.registry.ts @@ -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"; diff --git a/src/commands/models/list.row-sources.ts b/src/commands/models/list.row-sources.ts index bb868a5706d..ec5dc6363f0 100644 --- a/src/commands/models/list.row-sources.ts +++ b/src/commands/models/list.row-sources.ts @@ -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(); - 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 { + await appendConfiguredRows(params); } diff --git a/src/commands/models/list.rows.test.ts b/src/commands/models/list.rows.test.ts index e2f77819e47..663596dc9f6 100644 --- a/src/commands/models/list.rows.test.ts +++ b/src/commands/models/list.rows.test.ts @@ -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), diff --git a/src/commands/models/list.rows.ts b/src/commands/models/list.rows.ts index 18d6a0097a5..df6b9bd5dc5 100644 --- a/src/commands/models/list.rows.ts +++ b/src/commands/models/list.rows.ts @@ -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; +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 | undefined; +let modelResolverModulePromise: Promise | undefined; +let profileListModulePromise: Promise | undefined; +let providerCatalogModulePromise: Promise | undefined; +let syntheticAuthModulePromise: Promise | undefined; + +function loadModelCatalogModule(): Promise { + modelCatalogModulePromise ??= import("../../agents/model-catalog.js"); + return modelCatalogModulePromise; +} + +function loadModelResolverModule(): Promise { + modelResolverModulePromise ??= import("../../agents/pi-embedded-runner/model.js"); + return modelResolverModulePromise; +} + +function loadProfileListModule(): Promise { + profileListModulePromise ??= import("../../agents/auth-profiles/profile-list.js"); + return profileListModulePromise; +} + +function loadProviderCatalogModule(): Promise { + providerCatalogModulePromise ??= import("./list.provider-catalog.js"); + return providerCatalogModulePromise; +} + +function loadSyntheticAuthModule(): Promise { + 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 { + 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 { 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; allowProviderAvailabilityFallback?: boolean; -}): boolean { +}): Promise { 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[]; modelRegistry?: ModelRegistry; context: RowBuilderContext; -}): Set { +}): Promise> { const seenKeys = new Set(); + 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; -}): void { +}): Promise { 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; catalogRows: readonly NormalizedModelCatalogRow[]; -}): number { +}): Promise { 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; manifestRows: readonly NormalizedModelCatalogRow[]; -}): number { +}): Promise { return appendModelCatalogRows({ ...params, catalogRows: params.manifestRows, @@ -268,6 +373,10 @@ export async function appendCatalogSupplementRows(params: { context: RowBuilderContext; seenKeys: Set; }): Promise { + 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 { 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 { + 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, }), ); }