From b418c08a2297bc9647ac3e1500b4662400376915 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 29 Apr 2026 16:52:06 +0100 Subject: [PATCH] refactor: use auth index for model list rows --- .../list.list-command.forward-compat.test.ts | 45 ++++++------ src/commands/models/list.list-command.ts | 4 +- src/commands/models/list.model-row.test.ts | 15 +--- src/commands/models/list.model-row.ts | 30 +------- src/commands/models/list.registry.ts | 40 +---------- src/commands/models/list.rows.test.ts | 35 ++------- src/commands/models/list.rows.ts | 71 +++---------------- 7 files changed, 41 insertions(+), 199 deletions(-) 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 394c08cb1f9..05717fa9d8b 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -55,9 +55,8 @@ const mocks = vi.hoisted(() => { hasProviderStaticCatalogForFilter: vi.fn(), resolveConfiguredEntries: vi.fn(), printModelTable: vi.fn(), - listProfilesForProvider: vi.fn(), resolveModelWithRegistry: vi.fn(), - resolveRuntimeSyntheticAuthProviderRefs: vi.fn(), + readPersistedInstalledPluginIndexSync: vi.fn(), }; }); @@ -94,9 +93,8 @@ function resetMocks() { ], }); mocks.printModelTable.mockReset(); - mocks.listProfilesForProvider.mockReturnValue([]); mocks.resolveModelWithRegistry.mockReturnValue({ ...OPENAI_CODEX_MODEL }); - mocks.resolveRuntimeSyntheticAuthProviderRefs.mockReturnValue([]); + mocks.readPersistedInstalledPluginIndexSync.mockReturnValue(null); } function createRuntime() { @@ -201,10 +199,6 @@ function installModelsListCommandForwardCompatMocks() { resolveOpenClawAgentDir: mocks.resolveOpenClawAgentDir, })); - vi.doMock("../../agents/auth-profiles/profile-list.js", () => ({ - listProfilesForProvider: mocks.listProfilesForProvider, - })); - vi.doMock("../../agents/model-catalog.js", () => ({ loadModelCatalog: mocks.loadModelCatalog, })); @@ -214,13 +208,11 @@ function installModelsListCommandForwardCompatMocks() { })); vi.doMock("../../agents/model-auth.js", () => ({ - resolveEnvApiKey: vi.fn().mockReturnValue(undefined), - resolveAwsSdkEnvVarName: vi.fn().mockReturnValue(undefined), hasUsableCustomProviderApiKey: vi.fn().mockReturnValue(false), })); - vi.doMock("../../plugins/synthetic-auth.runtime.js", () => ({ - resolveRuntimeSyntheticAuthProviderRefs: mocks.resolveRuntimeSyntheticAuthProviderRefs, + vi.doMock("../../plugins/installed-plugin-index-store.js", () => ({ + readPersistedInstalledPluginIndexSync: mocks.readPersistedInstalledPluginIndexSync, })); } @@ -238,7 +230,7 @@ async function buildAllOpenAiCodexRows(opts: { supplementCatalog?: boolean } = { const context = { cfg: mocks.resolvedConfig, agentDir: "/tmp/openclaw-agent", - authStore: mocks.ensureAuthProfileStore(), + authIndex: { hasProviderAuth: (provider: string) => provider === "openai-codex" }, availableKeys: loaded.availableKeys, configuredByKey: new Map(), discoveredKeys: new Set( @@ -453,11 +445,17 @@ describe("modelsListCommand forward-compat", () => { describe("availability fallback", () => { it("marks synthetic codex gpt-5.4 rows as available when provider auth exists", async () => { - mocks.listProfilesForProvider.mockImplementation((_: unknown, provider: string) => - provider === "openai-codex" - ? ([{ id: "profile-1" }] as Array>) - : [], - ); + mocks.ensureAuthProfileStore.mockReturnValueOnce({ + version: 1, + profiles: { + "openai-codex:default": { + type: "token", + provider: "openai-codex", + token: "codex-app-server", + }, + }, + order: {}, + }); const runtime = createRuntime(); await modelsListCommand({ json: true }, runtime as never); @@ -508,7 +506,9 @@ describe("modelsListCommand forward-compat", () => { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }, ]); - mocks.resolveRuntimeSyntheticAuthProviderRefs.mockReturnValueOnce(["codex"]); + mocks.readPersistedInstalledPluginIndexSync.mockReturnValue({ + plugins: [{ syntheticAuthRefs: ["codex"] }], + }); const runtime = createRuntime(); await modelsListCommand({ all: true, provider: "codex", json: true }, runtime as never); @@ -865,11 +865,6 @@ describe("modelsListCommand forward-compat", () => { contextWindow: 400000, }, ]); - mocks.listProfilesForProvider.mockImplementation((_: unknown, provider: string) => - provider === "openai-codex" - ? ([{ id: "profile-1" }] as Array>) - : [], - ); mocks.resolveModelWithRegistry.mockImplementation( ({ provider, modelId }: { provider: string; modelId: string }) => { if (provider !== "openai-codex") { @@ -997,7 +992,7 @@ describe("modelsListCommand forward-compat", () => { ] as never, context: { cfg: mocks.resolvedConfig, - authStore: mocks.ensureAuthProfileStore(), + authIndex: { hasProviderAuth: () => false }, availableKeys: new Set(["openai-codex/gpt-5.4"]), configuredByKey: new Map(), discoveredKeys: new Set(), diff --git a/src/commands/models/list.list-command.ts b/src/commands/models/list.list-command.ts index 8b74566f052..04c2bdee827 100644 --- a/src/commands/models/list.list-command.ts +++ b/src/commands/models/list.list-command.ts @@ -3,6 +3,7 @@ import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; import { parseModelRef } from "../../agents/model-selection.js"; import type { RuntimeEnv } from "../../runtime.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { createModelListAuthIndex } from "./list.auth-index.js"; import { resolveConfiguredEntries } from "./list.configured.js"; import { formatErrorWithStack } from "./list.errors.js"; import { printModelTable } from "./list.table.js"; @@ -75,6 +76,7 @@ export async function modelsListCommand( }); const authStore = loadAuthProfileStoreWithoutExternalProfiles(); const agentDir = resolveOpenClawAgentDir(); + const authIndex = createModelListAuthIndex({ cfg, authStore }); let modelRegistry: ModelRegistry | undefined; let registryModels: Model[] = []; @@ -126,7 +128,7 @@ export async function modelsListCommand( const buildRowContext = (skipRuntimeModelSuppression: boolean) => ({ cfg, agentDir, - authStore, + authIndex, availableKeys, configuredByKey, discoveredKeys, diff --git a/src/commands/models/list.model-row.test.ts b/src/commands/models/list.model-row.test.ts index df093030a2d..b436c4f9d53 100644 --- a/src/commands/models/list.model-row.test.ts +++ b/src/commands/models/list.model-row.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from "vitest"; -import type { AuthProfileStore } from "../../agents/auth-profiles/types.js"; import { toModelRow } from "./list.model-row.js"; const OPENROUTER_MODEL = { @@ -33,23 +32,11 @@ describe("toModelRow", () => { }); it("marks models available from auth profiles without loading model discovery", () => { - const authStore: AuthProfileStore = { - version: 1, - profiles: { - "openrouter:default": { - type: "api_key", - provider: "openrouter", - key: "sk-or-v1-regression-test", - }, - }, - }; - const row = toModelRow({ model: OPENROUTER_MODEL as never, key: "openrouter/openai/gpt-5.4", tags: [], - cfg: {}, - authStore, + hasAuthForProvider: (provider) => provider === "openrouter", }); expect(row.available).toBe(true); diff --git a/src/commands/models/list.model-row.ts b/src/commands/models/list.model-row.ts index a0a9b557574..46079c69f7b 100644 --- a/src/commands/models/list.model-row.ts +++ b/src/commands/models/list.model-row.ts @@ -1,6 +1,4 @@ -import type { AuthProfileStore } from "../../agents/auth-profiles/types.js"; import { modelKey } from "../../agents/model-ref-shared.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { isLocalBaseUrl } from "./list.local-url.js"; import type { ModelRow } from "./list.types.js"; @@ -14,17 +12,7 @@ export type ListRowModel = { contextTokens?: number | null; }; -export type ModelAuthAvailabilityResolver = (params: { - provider: string; - cfg: OpenClawConfig; - authStore: AuthProfileStore; -}) => boolean; - -function authStoreHasProviderProfile(authStore: AuthProfileStore, provider: string): boolean { - return Object.values(authStore.profiles ?? {}).some( - (credential) => credential.provider === provider, - ); -} +export type ModelAuthAvailabilityResolver = (provider: string) => boolean; export function toModelRow(params: { model?: ListRowModel; @@ -32,8 +20,6 @@ export function toModelRow(params: { tags: string[]; aliases?: string[]; availableKeys?: Set; - cfg?: OpenClawConfig; - authStore?: AuthProfileStore; allowProviderAvailabilityFallback?: boolean; hasAuthForProvider?: ModelAuthAvailabilityResolver; }): ModelRow { @@ -43,8 +29,6 @@ export function toModelRow(params: { tags, aliases = [], availableKeys, - cfg, - authStore, allowProviderAvailabilityFallback = false, } = params; if (!model) { @@ -69,17 +53,7 @@ export function toModelRow(params: { const available = availableKeys !== undefined && !allowProviderAvailabilityFallback ? modelIsAvailable - : modelIsAvailable || - (cfg && authStore - ? ( - params.hasAuthForProvider ?? - ((input) => authStoreHasProviderProfile(input.authStore, input.provider)) - )({ - provider: model.provider, - cfg, - authStore, - }) - : false); + : modelIsAvailable || (params.hasAuthForProvider?.(model.provider) ?? false); const aliasTags = aliases.length > 0 ? [`alias:${aliases.join(",")}`] : []; const mergedTags = new Set(tags); if (aliasTags.length > 0) { diff --git a/src/commands/models/list.registry.ts b/src/commands/models/list.registry.ts index 53bb4672c9b..8e75eff4c85 100644 --- a/src/commands/models/list.registry.ts +++ b/src/commands/models/list.registry.ts @@ -1,20 +1,12 @@ 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, shouldSuppressBuiltInModelFromManifest, } 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 { formatErrorWithStack, MODEL_AVAILABILITY_UNAVAILABLE_CODE, @@ -24,32 +16,6 @@ import { toModelRow as toModelRowBase } from "./list.model-row.js"; import type { ModelRow } from "./list.types.js"; import { modelKey } from "./shared.js"; -const hasAuthForProvider = ( - provider: string, - cfg?: OpenClawConfig, - authStore?: AuthProfileStore, -) => { - if (!cfg || !authStore) { - return false; - } - if (listProfilesForProvider(authStore, provider).length > 0) { - return true; - } - if (provider === "amazon-bedrock" && resolveAwsSdkEnvVarName()) { - return true; - } - if (resolveEnvApiKey(provider)) { - return true; - } - if (hasUsableCustomProviderApiKey(cfg, provider)) { - return true; - } - if (resolveRuntimeSyntheticAuthProviderRefs().includes(provider)) { - return true; - } - return false; -}; - function createAvailabilityUnavailableError(message: string): Error { const err = new Error(message); (err as { code?: string }).code = MODEL_AVAILABILITY_UNAVAILABLE_CODE; @@ -171,9 +137,5 @@ export async function loadModelRegistry( } export function toModelRow(params: Parameters[0]): ModelRow { - return toModelRowBase({ - ...params, - hasAuthForProvider: ({ provider, cfg, authStore }) => - hasAuthForProvider(provider, cfg, authStore), - }); + return toModelRowBase(params); } diff --git a/src/commands/models/list.rows.test.ts b/src/commands/models/list.rows.test.ts index afc2bc43a0a..b3eabf0cd63 100644 --- a/src/commands/models/list.rows.test.ts +++ b/src/commands/models/list.rows.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it, vi } from "vitest"; -import type { AuthProfileStore } from "../../agents/auth-profiles/types.js"; import type { ModelRow } from "./list.types.js"; const mocks = vi.hoisted(() => ({ @@ -17,7 +16,6 @@ const mocks = vi.hoisted(() => ({ input: ["text"], }, ]), - listProfilesForProvider: vi.fn().mockReturnValue(["codex:synthetic"]), })); vi.mock("../../agents/model-suppression.js", () => ({ @@ -29,36 +27,15 @@ 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), -})); - -vi.mock("../../plugins/synthetic-auth.runtime.js", () => ({ - resolveRuntimeSyntheticAuthProviderRefs: vi.fn().mockReturnValue([]), -})); - import { appendProviderCatalogRows } from "./list.rows.js"; +const authIndex = { + hasProviderAuth: (provider: string) => provider === "codex", +}; + describe("appendProviderCatalogRows", () => { it("can skip runtime model-suppression hooks for provider-catalog fast paths", async () => { const rows: ModelRow[] = []; - const authStore: AuthProfileStore = { - version: 1, - profiles: { - "codex:synthetic": { - type: "token", - provider: "codex", - token: "codex-app-server", - }, - }, - order: {}, - }; await appendProviderCatalogRows({ rows, @@ -69,7 +46,7 @@ describe("appendProviderCatalogRows", () => { models: { providers: {} }, }, agentDir: "/tmp/openclaw-agent", - authStore, + authIndex, configuredByKey: new Map(), discoveredKeys: new Set(), filter: { provider: "codex", local: false }, @@ -118,7 +95,7 @@ describe("appendProviderCatalogRows", () => { models: { providers: {} }, }, agentDir: "/tmp/openclaw-agent", - authStore: { version: 1, profiles: {}, order: {} }, + authIndex: { hasProviderAuth: () => false }, configuredByKey: new Map(), discoveredKeys: new Set(), filter: { provider: "openai", local: false }, diff --git a/src/commands/models/list.rows.ts b/src/commands/models/list.rows.ts index 4a432f80030..ade8af65f7e 100644 --- a/src/commands/models/list.rows.ts +++ b/src/commands/models/list.rows.ts @@ -1,12 +1,6 @@ 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, shouldSuppressBuiltInModelFromManifest, @@ -15,6 +9,7 @@ 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 { ModelListAuthIndex } from "./list.auth-index.js"; import type { ListRowModel } from "./list.model-row.js"; import { toModelRow } from "./list.model-row.js"; import type { ConfiguredEntry, ModelRow } from "./list.types.js"; @@ -23,9 +18,7 @@ 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,7 +28,7 @@ type RowFilter = { export type RowBuilderContext = { cfg: OpenClawConfig; agentDir: string; - authStore: AuthProfileStore; + authIndex: ModelListAuthIndex; availableKeys?: Set; configuredByKey: ConfiguredByKey; discoveredKeys: Set; @@ -45,9 +38,7 @@ export type RowBuilderContext = { 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"); @@ -59,21 +50,11 @@ function loadModelResolverModule(): Promise { 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; @@ -84,28 +65,6 @@ function matchesRowFilter(filter: RowFilter, model: { provider: string; baseUrl? return true; } -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; @@ -115,23 +74,16 @@ async function buildRow(params: { 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, tags: configured ? Array.from(configured.tags) : [], aliases: configured?.aliases ?? [], availableKeys: params.context.availableKeys, - cfg: params.context.cfg, - authStore: params.context.authStore, allowProviderAvailabilityFallback: params.allowProviderAvailabilityFallback ?? false, - hasAuthForProvider: shouldResolveProviderAuth ? () => hasProviderAuth : undefined, + hasAuthForProvider: shouldResolveProviderAuth + ? params.context.authIndex.hasProviderAuth + : undefined, }); } @@ -503,13 +455,6 @@ export async function appendConfiguredRows(params: { 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, @@ -517,12 +462,12 @@ export async function appendConfiguredRows(params: { tags: Array.from(entry.tags), aliases: entry.aliases, availableKeys: params.context.availableKeys, - cfg: params.context.cfg, - authStore: params.context.authStore, allowProviderAvailabilityFallback: model ? !params.context.discoveredKeys.has(modelKey(model.provider, model.id)) : false, - hasAuthForProvider: shouldResolveProviderAuth ? () => hasProviderAuth : undefined, + hasAuthForProvider: shouldResolveProviderAuth + ? params.context.authIndex.hasProviderAuth + : undefined, }), ); }