fix(models): sync auth-profiles before availability checks

This commit is contained in:
Sebastian
2026-02-16 21:00:25 -05:00
parent fbda9a93fd
commit 4ca75bed56
6 changed files with 178 additions and 1 deletions

View File

@@ -0,0 +1,101 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { saveAuthProfileStore } from "../agents/auth-profiles.js";
import { clearConfigCache } from "../config/config.js";
import { modelsListCommand } from "./models/list.list-command.js";
const ENV_KEYS = [
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
"OPENCLAW_CONFIG_PATH",
"OPENROUTER_API_KEY",
] as const;
function captureEnv() {
return Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
}
function restoreEnv(snapshot: Record<string, string | undefined>) {
for (const key of ENV_KEYS) {
const value = snapshot[key];
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
async function pathExists(pathname: string): Promise<boolean> {
try {
await fs.stat(pathname);
return true;
} catch {
return false;
}
}
describe("models list auth-profile sync", () => {
it("marks models available when auth exists only in auth-profiles.json", async () => {
const env = captureEnv();
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-models-list-auth-sync-"));
try {
const stateDir = path.join(root, "state");
const agentDir = path.join(stateDir, "agents", "main", "agent");
const configPath = path.join(stateDir, "openclaw.json");
await fs.mkdir(agentDir, { recursive: true });
await fs.writeFile(configPath, "{}\n", "utf8");
process.env.OPENCLAW_STATE_DIR = stateDir;
process.env.OPENCLAW_AGENT_DIR = agentDir;
process.env.PI_CODING_AGENT_DIR = agentDir;
process.env.OPENCLAW_CONFIG_PATH = configPath;
delete process.env.OPENROUTER_API_KEY;
saveAuthProfileStore(
{
version: 1,
profiles: {
"openrouter:default": {
type: "api_key",
provider: "openrouter",
key: "sk-or-v1-regression-test",
},
},
},
agentDir,
);
const authPath = path.join(agentDir, "auth.json");
expect(await pathExists(authPath)).toBe(false);
clearConfigCache();
const runtime = {
log: vi.fn(),
error: vi.fn(),
};
await modelsListCommand({ all: true, json: true }, runtime as never);
expect(runtime.error).not.toHaveBeenCalled();
expect(runtime.log).toHaveBeenCalledTimes(1);
const payload = JSON.parse(String(runtime.log.mock.calls[0]?.[0])) as {
models?: Array<{ key?: string; available?: boolean }>;
};
const openrouter = payload.models?.find((model) =>
String(model.key ?? "").startsWith("openrouter/"),
);
expect(openrouter).toBeDefined();
expect(openrouter?.available).toBe(true);
expect(await pathExists(authPath)).toBe(true);
} finally {
clearConfigCache();
restoreEnv(env);
await fs.rm(root, { recursive: true, force: true });
}
});
});

View File

@@ -4,6 +4,9 @@ let modelsListCommand: typeof import("./models/list.list-command.js").modelsList
const loadConfig = vi.fn();
const ensureOpenClawModelsJson = vi.fn().mockResolvedValue(undefined);
const ensurePiAuthJsonFromAuthProfiles = vi
.fn()
.mockResolvedValue({ wrote: false, authPath: "/tmp/openclaw-agent/auth.json" });
const resolveOpenClawAgentDir = vi.fn().mockReturnValue("/tmp/openclaw-agent");
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
const listProfilesForProvider = vi.fn().mockReturnValue([]);
@@ -33,6 +36,10 @@ vi.mock("../agents/models-config.js", () => ({
ensureOpenClawModelsJson,
}));
vi.mock("../agents/pi-auth-json.js", () => ({
ensurePiAuthJsonFromAuthProfiles,
}));
vi.mock("../agents/agent-paths.js", () => ({
resolveOpenClawAgentDir,
}));
@@ -100,6 +107,7 @@ beforeEach(() => {
modelRegistryState.getAllError = undefined;
modelRegistryState.getAvailableError = undefined;
listProfilesForProvider.mockReturnValue([]);
ensurePiAuthJsonFromAuthProfiles.mockClear();
});
afterEach(() => {
@@ -267,6 +275,15 @@ describe("models list/status", () => {
({ modelsListCommand } = await import("./models/list.list-command.js"));
});
it("models list syncs auth-profiles into auth.json before availability checks", async () => {
setDefaultZaiRegistry();
const runtime = makeRuntime();
await modelsListCommand({ all: true, json: true }, runtime);
expect(ensurePiAuthJsonFromAuthProfiles).toHaveBeenCalledWith("/tmp/openclaw-agent");
});
it("models list outputs canonical zai key for configured z.ai model", async () => {
setDefaultZaiRegistry();
const runtime = makeRuntime();

View File

@@ -12,6 +12,7 @@ import {
resolveForwardCompatModel,
} from "../../agents/model-forward-compat.js";
import { ensureOpenClawModelsJson } from "../../agents/models-config.js";
import { ensurePiAuthJsonFromAuthProfiles } from "../../agents/pi-auth-json.js";
import type { ModelRegistry } from "../../agents/pi-model-discovery.js";
import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
import type { OpenClawConfig } from "../../config/config.js";
@@ -101,6 +102,7 @@ function loadAvailableModels(registry: ModelRegistry): Model<Api>[] {
export async function loadModelRegistry(cfg: OpenClawConfig) {
await ensureOpenClawModelsJson(cfg);
const agentDir = resolveOpenClawAgentDir();
await ensurePiAuthJsonFromAuthProfiles(agentDir);
const authStorage = discoverAuthStorage(agentDir);
const registry = discoverModels(authStorage, agentDir);
const appended = appendAntigravityForwardCompatModels(registry.getAll(), registry);