refactor: use auth index for model list rows

This commit is contained in:
Shakker
2026-04-29 16:52:06 +01:00
parent a777b82da0
commit b418c08a22
7 changed files with 41 additions and 199 deletions

View File

@@ -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<Record<string, unknown>>)
: [],
);
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<Record<string, unknown>>)
: [],
);
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(),

View File

@@ -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<Api>[] = [];
@@ -126,7 +128,7 @@ export async function modelsListCommand(
const buildRowContext = (skipRuntimeModelSuppression: boolean) => ({
cfg,
agentDir,
authStore,
authIndex,
availableKeys,
configuredByKey,
discoveredKeys,

View File

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

View File

@@ -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<string>;
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) {

View File

@@ -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<typeof toModelRowBase>[0]): ModelRow {
return toModelRowBase({
...params,
hasAuthForProvider: ({ provider, cfg, authStore }) =>
hasAuthForProvider(provider, cfg, authStore),
});
return toModelRowBase(params);
}

View File

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

View File

@@ -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<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,7 +28,7 @@ type RowFilter = {
export type RowBuilderContext = {
cfg: OpenClawConfig;
agentDir: string;
authStore: AuthProfileStore;
authIndex: ModelListAuthIndex;
availableKeys?: Set<string>;
configuredByKey: ConfiguredByKey;
discoveredKeys: Set<string>;
@@ -45,9 +38,7 @@ export type RowBuilderContext = {
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");
@@ -59,21 +50,11 @@ function loadModelResolverModule(): Promise<ModelResolverModule> {
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;
@@ -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<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;
@@ -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,
}),
);
}