diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts index c50ec909563..04b53a4137f 100644 --- a/src/agents/pi-model-discovery.ts +++ b/src/agents/pi-model-discovery.ts @@ -15,7 +15,10 @@ import { } from "../plugins/provider-runtime.js"; import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js"; import { isRecord } from "../utils.js"; -import { ensureAuthProfileStore } from "./auth-profiles/store.js"; +import { + ensureAuthProfileStore, + loadAuthProfileStoreForSecretsRuntime, +} from "./auth-profiles/store.js"; import { resolveProviderEnvApiKeyCandidates } from "./model-auth-env-vars.js"; import { resolveEnvApiKey } from "./model-auth-env.js"; import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js"; @@ -277,8 +280,18 @@ export function addEnvBackedPiCredentials( return next; } -export function resolvePiCredentialsForDiscovery(agentDir: string): PiCredentialMap { - const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); +type DiscoverAuthStorageOptions = { + readOnly?: boolean; +}; + +export function resolvePiCredentialsForDiscovery( + agentDir: string, + options?: DiscoverAuthStorageOptions, +): PiCredentialMap { + const store = + options?.readOnly === true + ? loadAuthProfileStoreForSecretsRuntime(agentDir) + : ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); const credentials = addEnvBackedPiCredentials(resolvePiCredentialMapFromStore(store)); for (const provider of resolveRuntimeSyntheticAuthProviderRefs()) { if (credentials[provider]) { @@ -305,10 +318,15 @@ export function resolvePiCredentialsForDiscovery(agentDir: string): PiCredential } // Compatibility helpers for pi-coding-agent 0.50+ (discover* helpers removed). -export function discoverAuthStorage(agentDir: string): PiAuthStorage { - const credentials = resolvePiCredentialsForDiscovery(agentDir); +export function discoverAuthStorage( + agentDir: string, + options?: DiscoverAuthStorageOptions, +): PiAuthStorage { + const credentials = resolvePiCredentialsForDiscovery(agentDir, options); const authPath = path.join(agentDir, "auth.json"); - scrubLegacyStaticAuthJsonEntriesForDiscovery(authPath); + if (options?.readOnly !== true) { + scrubLegacyStaticAuthJsonEntriesForDiscovery(authPath); + } return createAuthStorage(PiAuthStorageClass, authPath, credentials); } diff --git a/src/commands/models/list.list-command.ts b/src/commands/models/list.list-command.ts index b6afc796360..3ffee8e69c7 100644 --- a/src/commands/models/list.list-command.ts +++ b/src/commands/models/list.list-command.ts @@ -6,6 +6,7 @@ import { resolveConfiguredEntries } from "./list.configured.js"; import { formatErrorWithStack } from "./list.errors.js"; import { appendCatalogSupplementRows, + appendConfiguredProviderRows, appendConfiguredRows, appendDiscoveredRows, appendProviderCatalogRows, @@ -47,9 +48,8 @@ export async function modelsListCommand( if (providerFilter === null) { return; } - const { ensureAuthProfileStore, ensureOpenClawModelsJson, resolveOpenClawAgentDir } = - await import("./list.runtime.js"); - const { sourceConfig, resolvedConfig: cfg } = await loadModelsConfigWithSource({ + const { ensureAuthProfileStore, resolveOpenClawAgentDir } = await import("./list.runtime.js"); + const { resolvedConfig: cfg } = await loadModelsConfigWithSource({ commandName: "models list", runtime, }); @@ -62,11 +62,8 @@ export async function modelsListCommand( let availabilityErrorMessage: string | undefined; const useProviderCatalogFastPath = Boolean(opts.all && providerFilter === "codex"); try { - // Keep command behavior explicit: sync models.json from the source config - // before building the read-only model registry view. if (!useProviderCatalogFastPath) { - await ensureOpenClawModelsJson(sourceConfig ?? cfg); - const loaded = await loadListModelRegistry(cfg, { sourceConfig, providerFilter }); + const loaded = await loadListModelRegistry(cfg, { providerFilter }); modelRegistry = loaded.registry; discoveredKeys = loaded.discoveredKeys; availableKeys = loaded.availableKeys; @@ -107,6 +104,14 @@ export async function modelsListCommand( context: rowContext, }); + if (providerFilter) { + appendConfiguredProviderRows({ + rows, + context: rowContext, + seenKeys, + }); + } + if (modelRegistry) { await appendCatalogSupplementRows({ rows, diff --git a/src/commands/models/list.model-row.ts b/src/commands/models/list.model-row.ts index e06d1cf2411..52e67d2f6bc 100644 --- a/src/commands/models/list.model-row.ts +++ b/src/commands/models/list.model-row.ts @@ -1,10 +1,18 @@ -import type { Api, Model } from "@mariozechner/pi-ai"; 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"; +export type ListRowModel = { + id: string; + name: string; + provider: string; + input: Array<"text" | "image">; + baseUrl?: string; + contextWindow?: number | null; +}; + export type ModelAuthAvailabilityResolver = (params: { provider: string; cfg: OpenClawConfig; @@ -18,7 +26,7 @@ function authStoreHasProviderProfile(authStore: AuthProfileStore, provider: stri } export function toModelRow(params: { - model?: Model; + model?: ListRowModel; key: string; tags: string[]; aliases?: string[]; @@ -52,7 +60,7 @@ export function toModelRow(params: { } const input = model.input.join("+") || "text"; - const local = isLocalBaseUrl(model.baseUrl); + const local = isLocalBaseUrl(model.baseUrl ?? ""); const modelIsAvailable = availableKeys?.has(modelKey(model.provider, model.id)) ?? false; // Prefer model-level registry availability when present. // Fall back to provider-level auth heuristics only if registry availability isn't available, diff --git a/src/commands/models/list.registry.ts b/src/commands/models/list.registry.ts index 40b64cf46bd..29b4e67ddd5 100644 --- a/src/commands/models/list.registry.ts +++ b/src/commands/models/list.registry.ts @@ -108,12 +108,9 @@ function loadAvailableModels(registry: ModelRegistry, cfg: OpenClawConfig): Mode } } -export async function loadModelRegistry( - cfg: OpenClawConfig, - opts?: { sourceConfig?: OpenClawConfig; providerFilter?: string }, -) { +export async function loadModelRegistry(cfg: OpenClawConfig, opts?: { providerFilter?: string }) { const agentDir = resolveOpenClawAgentDir(); - const authStorage = discoverAuthStorage(agentDir); + const authStorage = discoverAuthStorage(agentDir, { readOnly: true }); const registry = discoverModels(authStorage, agentDir, { providerFilter: opts?.providerFilter, }); diff --git a/src/commands/models/list.rows.ts b/src/commands/models/list.rows.ts index 89aaa4fbd79..25d87e35fc6 100644 --- a/src/commands/models/list.rows.ts +++ b/src/commands/models/list.rows.ts @@ -1,9 +1,12 @@ 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 { 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 { ListRowModel } from "./list.model-row.js"; import { loadModelRegistry, toModelRow } from "./list.registry.js"; import { loadModelCatalog, @@ -42,7 +45,7 @@ function matchesRowFilter(filter: RowFilter, model: { provider: string; baseUrl? } function buildRow(params: { - model: Model; + model: ListRowModel; key: string; context: RowBuilderContext; allowProviderAvailabilityFallback?: boolean; @@ -75,9 +78,35 @@ function shouldSuppressListModel(params: { }); } +function resolveConfiguredModelInput(params: { + model: Partial; +}): Array<"text" | "image"> { + const input = Array.isArray(params.model.input) + ? params.model.input.filter( + (item): item is "text" | "image" => item === "text" || item === "image", + ) + : []; + return input.length > 0 ? input : ["text"]; +} + +function toConfiguredProviderListModel(params: { + provider: string; + providerConfig: Partial; + model: Partial & Pick; +}): ListRowModel { + return { + provider: params.provider, + id: params.model.id, + name: params.model.name ?? params.model.id, + baseUrl: params.model.baseUrl ?? params.providerConfig.baseUrl, + input: resolveConfiguredModelInput({ model: params.model }), + contextWindow: params.model.contextWindow ?? DEFAULT_CONTEXT_TOKENS, + }; +} + export async function loadListModelRegistry( cfg: OpenClawConfig, - opts?: { sourceConfig?: OpenClawConfig; providerFilter?: string }, + opts?: { providerFilter?: string }, ) { const loaded = await loadModelRegistry(cfg, opts); return { @@ -121,6 +150,43 @@ export function appendDiscoveredRows(params: { return seenKeys; } +export function appendConfiguredProviderRows(params: { + rows: ModelRow[]; + context: RowBuilderContext; + seenKeys: Set; +}): void { + for (const [provider, providerConfig] of Object.entries( + params.context.cfg.models?.providers ?? {}, + )) { + for (const configuredModel of providerConfig.models ?? []) { + const key = modelKey(provider, configuredModel.id); + if (params.seenKeys.has(key)) { + continue; + } + const model = toConfiguredProviderListModel({ + provider, + providerConfig, + model: configuredModel, + }); + if (!matchesRowFilter(params.context.filter, model)) { + continue; + } + if (shouldSuppressListModel({ model, context: params.context })) { + continue; + } + params.rows.push( + buildRow({ + model, + key, + context: params.context, + allowProviderAvailabilityFallback: !params.context.discoveredKeys.has(key), + }), + ); + params.seenKeys.add(key); + } + } +} + export async function appendCatalogSupplementRows(params: { rows: ModelRow[]; modelRegistry: ModelRegistry; diff --git a/src/commands/models/list.runtime.ts b/src/commands/models/list.runtime.ts index ad446fa4b78..7d0613de45b 100644 --- a/src/commands/models/list.runtime.ts +++ b/src/commands/models/list.runtime.ts @@ -1,5 +1,4 @@ -export { ensureAuthProfileStoreWithoutExternalProfiles as ensureAuthProfileStore } from "../../agents/auth-profiles/store.js"; -export { ensureOpenClawModelsJson } from "../../agents/models-config.js"; +export { loadAuthProfileStoreWithoutExternalProfiles as ensureAuthProfileStore } from "../../agents/auth-profiles/store.js"; export { resolveOpenClawAgentDir } from "../../agents/agent-paths.js"; export { listProfilesForProvider } from "../../agents/auth-profiles.js"; export {