From f727fbc775dd9df19f7746339908dc2b14c35119 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 10:59:27 +0100 Subject: [PATCH] fix(agents): skip model normalization in context warmup --- CHANGELOG.md | 1 + src/agents/context.lookup.test.ts | 28 +++++++++++++++++++++++++++- src/agents/context.test.ts | 16 ++++++++++++++++ src/agents/context.ts | 25 +++++++++++++++++++------ 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6588294388b..56afa3f1895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- TUI/chat: skip full provider model normalization during context-window warmup while preserving provider-owned context metadata, avoiding cold-start stalls with large model registries. Thanks @547895019. - Memory Wiki: accept relative Markdown links that include the `.md` suffix during broken-wikilink validation, avoiding false positives for native render-mode links. Thanks @Kenneth8128. - Plugins/CLI: cache plugin CLI registration entries per command program so completion state generation does not repeat the full plugin sweep in one invocation. Thanks @ScientificProgrammer. - Plugins: reuse gateway-bindable plugin loader cache entries for later default-mode loads without serving default-built registries to gateway-bound requests, reducing repeated plugin registration during dispatch. Refs #61756. Thanks @DmitryPogodaev. diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index 7bf65e39e2b..10b8b486f20 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -1,6 +1,11 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -type DiscoveredModel = { id: string; contextWindow?: number; contextTokens?: number }; +type DiscoveredModel = { + id: string; + provider?: string; + contextWindow?: number; + contextTokens?: number; +}; type ContextModule = typeof import("./context.js"); const contextTestState = vi.hoisted(() => { @@ -280,6 +285,27 @@ describe("lookupContextTokens", () => { expect(lookupContextTokens("gemini-3.1-pro-preview")).toBe(128_000); }); + it("skips model normalization during warmup but preserves provider-owned context metadata", async () => { + mockDiscoveryDeps([ + { + id: "anthropic/claude-opus-4.7-20260219", + provider: "anthropic", + contextWindow: 200_000, + }, + ]); + + const { lookupContextTokens } = await importContextModule(); + lookupContextTokens("anthropic/claude-opus-4.7-20260219"); + await flushAsyncWarmup(); + + expect(contextTestState.discoverModels).toHaveBeenCalledWith( + expect.anything(), + "/tmp/openclaw-agent", + { normalizeModels: false }, + ); + expect(lookupContextTokens("anthropic/claude-opus-4.7-20260219")).toBe(1_048_576); + }); + it("resolveContextTokensForModel returns discovery value when provider-qualified entry exists in cache", async () => { // Registry returns provider-qualified entries (real-world scenario from #35976). // When no explicit config override exists, the bare cache lookup hits the diff --git a/src/agents/context.test.ts b/src/agents/context.test.ts index ba87aa79f93..25523a2e835 100644 --- a/src/agents/context.test.ts +++ b/src/agents/context.test.ts @@ -93,6 +93,22 @@ describe("applyDiscoveredContextWindows", () => { expect(cache.get("anthropic/claude-opus-4.7-20260219")).toBe(200_000); }); + it("upgrades provider-owned anthropic opus 4.7 discovery ids", () => { + const cache = new Map(); + applyDiscoveredContextWindows({ + cache, + models: [ + { + id: "anthropic/claude-opus-4.7-20260219", + provider: "anthropic", + contextWindow: 200_000, + }, + ], + }); + + expect(cache.get("anthropic/claude-opus-4.7-20260219")).toBe(ANTHROPIC_CONTEXT_1M_TOKENS); + }); + it("does not upgrade bare opus 4.7 discovery ids without verified ownership", () => { const cache = new Map(); applyDiscoveredContextWindows({ diff --git a/src/agents/context.ts b/src/agents/context.ts index f7ace678c81..25cbfe2c061 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -15,7 +15,12 @@ import { normalizeProviderId } from "./model-selection.js"; export { resetContextWindowCacheForTest } from "./context-runtime-state.js"; -type ModelEntry = { id: string; contextWindow?: number; contextTokens?: number }; +type ModelEntry = { + id: string; + provider?: string; + contextWindow?: number; + contextTokens?: number; +}; type ModelRegistryLike = { getAvailable?: () => ModelEntry[]; getAll: () => ModelEntry[]; @@ -53,7 +58,7 @@ export function applyDiscoveredContextWindows(params: { : typeof model.contextWindow === "number" ? Math.trunc(model.contextWindow) : undefined; - const contextTokens = shouldUseDiscoveredAnthropicOpus47ContextWindow(model.id) + const contextTokens = shouldUseDiscoveredAnthropicOpus47ContextWindow(model) ? ANTHROPIC_CONTEXT_1M_TOKENS : discoveredContextTokens; if (!contextTokens || contextTokens <= 0) { @@ -236,7 +241,9 @@ function ensureContextWindowCacheLoaded(): Promise { await import("./pi-model-discovery-runtime.js"); const agentDir = resolveOpenClawAgentDir(); const authStorage = discoverAuthStorage(agentDir); - const modelRegistry = discoverModels(authStorage, agentDir) as unknown as ModelRegistryLike; + const modelRegistry = discoverModels(authStorage, agentDir, { + normalizeModels: false, + }) as unknown as ModelRegistryLike; const models = typeof modelRegistry.getAvailable === "function" ? modelRegistry.getAvailable() @@ -418,17 +425,23 @@ function shouldUseAnthropicOpus47ContextWindow(params: { ); } -function shouldUseDiscoveredAnthropicOpus47ContextWindow(modelId: string): boolean { +function shouldUseDiscoveredAnthropicOpus47ContextWindow(model: ModelEntry): boolean { + const provider = + typeof model.provider === "string" ? normalizeProviderId(model.provider) : undefined; + const modelId = model.id; if (!isClaudeOpus47Model(modelId)) { return false; } + if (provider) { + return provider === "anthropic" || provider === "claude-cli"; + } const normalized = normalizeLowercaseStringOrEmpty(modelId); const slash = normalized.indexOf("/"); if (slash < 0) { return false; } - const provider = normalizeProviderId(normalized.slice(0, slash)); - return provider === "claude-cli"; + const inferredProvider = normalizeProviderId(normalized.slice(0, slash)); + return inferredProvider === "claude-cli"; } function resolveModelFamilyId(modelId: string): string {