CLI: dedupe config validate errors and expose allowed values

This commit is contained in:
Gustavo Madeira Santana
2026-03-02 20:05:12 -05:00
parent a44843507f
commit f26853f14c
41 changed files with 1393 additions and 134 deletions

View File

@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
describe("lookupContextTokens", () => {
beforeEach(() => {
vi.resetModules();
});
it("returns configured model context window on first lookup", async () => {
vi.doMock("../config/config.js", () => ({
loadConfig: () => ({
models: {
providers: {
openrouter: {
models: [{ id: "openrouter/claude-sonnet", contextWindow: 321_000 }],
},
},
},
}),
}));
vi.doMock("./models-config.js", () => ({
ensureOpenClawModelsJson: vi.fn(async () => {}),
}));
vi.doMock("./agent-paths.js", () => ({
resolveOpenClawAgentDir: () => "/tmp/openclaw-agent",
}));
vi.doMock("./pi-model-discovery.js", () => ({
discoverAuthStorage: vi.fn(() => ({})),
discoverModels: vi.fn(() => ({
getAll: () => [],
})),
}));
const { lookupContextTokens } = await import("./context.js");
expect(lookupContextTokens("openrouter/claude-sonnet")).toBe(321_000);
});
it("does not skip eager warmup when --profile is followed by -- terminator", async () => {
const loadConfigMock = vi.fn(() => ({ models: {} }));
vi.doMock("../config/config.js", () => ({
loadConfig: loadConfigMock,
}));
vi.doMock("./models-config.js", () => ({
ensureOpenClawModelsJson: vi.fn(async () => {}),
}));
vi.doMock("./agent-paths.js", () => ({
resolveOpenClawAgentDir: () => "/tmp/openclaw-agent",
}));
vi.doMock("./pi-model-discovery.js", () => ({
discoverAuthStorage: vi.fn(() => ({})),
discoverModels: vi.fn(() => ({
getAll: () => [],
})),
}));
const argvSnapshot = process.argv;
process.argv = ["node", "openclaw", "--profile", "--", "config", "validate"];
try {
await import("./context.js");
expect(loadConfigMock).toHaveBeenCalledTimes(1);
} finally {
process.argv = argvSnapshot;
}
});
});

View File

@@ -3,6 +3,7 @@
import { loadConfig } from "../config/config.js";
import type { OpenClawConfig } from "../config/config.js";
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
import { resolveOpenClawAgentDir } from "./agent-paths.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
@@ -66,55 +67,114 @@ export function applyConfiguredContextWindows(params: {
}
const MODEL_CACHE = new Map<string, number>();
const loadPromise = (async () => {
let cfg: ReturnType<typeof loadConfig> | undefined;
let loadPromise: Promise<void> | null = null;
let configuredWindowsPrimed = false;
function getCommandPathFromArgv(argv: string[]): string[] {
const args = argv.slice(2);
const tokens: string[] = [];
for (let i = 0; i < args.length; i += 1) {
const arg = args[i];
if (!arg || arg === FLAG_TERMINATOR) {
break;
}
const consumed = consumeRootOptionToken(args, i);
if (consumed > 0) {
i += consumed - 1;
continue;
}
if (arg.startsWith("-")) {
continue;
}
tokens.push(arg);
if (tokens.length >= 2) {
break;
}
}
return tokens;
}
function shouldSkipEagerContextWindowWarmup(argv: string[] = process.argv): boolean {
const [primary, secondary] = getCommandPathFromArgv(argv);
return primary === "config" && secondary === "validate";
}
function primeConfiguredContextWindows(): OpenClawConfig | undefined {
if (configuredWindowsPrimed) {
return undefined;
}
configuredWindowsPrimed = true;
try {
cfg = loadConfig();
const cfg = loadConfig();
applyConfiguredContextWindows({
cache: MODEL_CACHE,
modelsConfig: cfg.models as ModelsConfig | undefined,
});
return cfg;
} catch {
// If config can't be loaded, leave cache empty.
return;
return undefined;
}
}
try {
await ensureOpenClawModelsJson(cfg);
} catch {
// Continue with best-effort discovery/overrides.
function ensureContextWindowCacheLoaded(): Promise<void> {
const cfg = primeConfiguredContextWindows();
if (loadPromise) {
return loadPromise;
}
loadPromise = (async () => {
if (!cfg) {
return;
}
try {
const { discoverAuthStorage, discoverModels } = await import("./pi-model-discovery.js");
const agentDir = resolveOpenClawAgentDir();
const authStorage = discoverAuthStorage(agentDir);
const modelRegistry = discoverModels(authStorage, agentDir) as unknown as ModelRegistryLike;
const models =
typeof modelRegistry.getAvailable === "function"
? modelRegistry.getAvailable()
: modelRegistry.getAll();
applyDiscoveredContextWindows({
try {
await ensureOpenClawModelsJson(cfg);
} catch {
// Continue with best-effort discovery/overrides.
}
try {
const { discoverAuthStorage, discoverModels } = await import("./pi-model-discovery.js");
const agentDir = resolveOpenClawAgentDir();
const authStorage = discoverAuthStorage(agentDir);
const modelRegistry = discoverModels(authStorage, agentDir) as unknown as ModelRegistryLike;
const models =
typeof modelRegistry.getAvailable === "function"
? modelRegistry.getAvailable()
: modelRegistry.getAll();
applyDiscoveredContextWindows({
cache: MODEL_CACHE,
models,
});
} catch {
// If model discovery fails, continue with config overrides only.
}
applyConfiguredContextWindows({
cache: MODEL_CACHE,
models,
modelsConfig: cfg.models as ModelsConfig | undefined,
});
} catch {
// If model discovery fails, continue with config overrides only.
}
applyConfiguredContextWindows({
cache: MODEL_CACHE,
modelsConfig: cfg.models as ModelsConfig | undefined,
})().catch(() => {
// Keep lookup best-effort.
});
})().catch(() => {
// Keep lookup best-effort.
});
return loadPromise;
}
export function lookupContextTokens(modelId?: string): number | undefined {
if (!modelId) {
return undefined;
}
// Best-effort: kick off loading, but don't block.
void loadPromise;
void ensureContextWindowCacheLoaded();
return MODEL_CACHE.get(modelId);
}
if (!shouldSkipEagerContextWindowWarmup()) {
// Keep prior behavior where model limits begin loading during startup.
// This avoids a cold-start miss on the first context token lookup.
void ensureContextWindowCacheLoaded();
}
function resolveConfiguredModelParams(
cfg: OpenClawConfig | undefined,
provider: string,