fix: make models list read-only

This commit is contained in:
Shakker
2026-04-24 00:30:46 +01:00
committed by Shakker
parent 93e95a2057
commit c7af4dcb31
6 changed files with 118 additions and 25 deletions

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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<Api>;
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,

View File

@@ -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,
});

View File

@@ -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<Api>;
model: ListRowModel;
key: string;
context: RowBuilderContext;
allowProviderAvailabilityFallback?: boolean;
@@ -75,9 +78,35 @@ function shouldSuppressListModel(params: {
});
}
function resolveConfiguredModelInput(params: {
model: Partial<ModelDefinitionConfig>;
}): 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<ModelProviderConfig>;
model: Partial<ModelDefinitionConfig> & Pick<ModelDefinitionConfig, "id">;
}): 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<string>;
}): 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;

View File

@@ -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 {