mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix(models): sync auth-profiles before availability checks
This commit is contained in:
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Tools: scope the `message` tool schema to the active channel so Telegram uses `buttons` and Discord uses `components`. (#18215) Thanks @obviyus.
|
||||
- Agents/Models: probe the primary model when its auth-profile cooldown is near expiry (with per-provider throttling), so runs recover from temporary rate limits without staying on fallback models until restart. (#17478) Thanks @PlayerGhost.
|
||||
- Agents/Failover: classify provider abort stop-reason errors (`Unhandled stop reason: abort`, `stop reason: abort`, `reason: abort`) as timeout-class failures so configured model fallback chains trigger instead of surfacing raw abort failures. (#18618) Thanks @sauerdaniel.
|
||||
- Models/CLI: sync auth-profiles credentials into agent `auth.json` before registry availability checks so `openclaw models list --all` reports auth correctly for API-key/token providers, normalize provider-id aliases when bridging credentials, and skip expired token mirrors. (#18610, #18615)
|
||||
- Agents/Context: raise default total bootstrap prompt cap from `24000` to `150000` chars (keeping `bootstrapMaxChars` at `20000`), include total-cap visibility in `/context`, and mark truncation from injected-vs-raw sizes so total-cap clipping is reflected accurately.
|
||||
- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96.
|
||||
- Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope.
|
||||
|
||||
@@ -157,6 +157,54 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => {
|
||||
expect(result.wrote).toBe(false);
|
||||
});
|
||||
|
||||
it("skips expired token credentials", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: "sk-ant-expired",
|
||||
expires: Date.now() - 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const result = await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
expect(result.wrote).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes provider ids when writing auth.json keys", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"z.ai:default": {
|
||||
type: "api_key",
|
||||
provider: "z.ai",
|
||||
key: "sk-zai",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const result = await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
expect(result.wrote).toBe(true);
|
||||
|
||||
const authPath = path.join(agentDir, "auth.json");
|
||||
const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as Record<string, unknown>;
|
||||
expect(auth["zai"]).toMatchObject({ type: "api_key", key: "sk-zai" });
|
||||
expect(auth["z.ai"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves existing auth.json entries not in auth-profiles", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
const authPath = path.join(agentDir, "auth.json");
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
import type { AuthProfileCredential } from "./auth-profiles/types.js";
|
||||
import { normalizeProviderId } from "./model-selection.js";
|
||||
|
||||
type AuthJsonCredential =
|
||||
| {
|
||||
@@ -50,6 +51,13 @@ function convertCredential(cred: AuthProfileCredential): AuthJsonCredential | nu
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
const expires =
|
||||
typeof (cred as { expires?: unknown }).expires === "number"
|
||||
? (cred as { expires: number }).expires
|
||||
: Number.NaN;
|
||||
if (Number.isFinite(expires) && expires > 0 && Date.now() >= expires) {
|
||||
return null;
|
||||
}
|
||||
return { type: "api_key", key: token };
|
||||
}
|
||||
|
||||
@@ -114,7 +122,7 @@ export async function ensurePiAuthJsonFromAuthProfiles(agentDir: string): Promis
|
||||
const providerCredentials = new Map<string, AuthJsonCredential>();
|
||||
|
||||
for (const [, cred] of Object.entries(store.profiles)) {
|
||||
const provider = cred.provider;
|
||||
const provider = normalizeProviderId(String(cred.provider ?? "")).trim();
|
||||
if (!provider || providerCredentials.has(provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
101
src/commands/models.list.auth-sync.test.ts
Normal file
101
src/commands/models.list.auth-sync.test.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user