fix(agents): skip model normalization in context warmup

This commit is contained in:
Peter Steinberger
2026-05-02 10:59:27 +01:00
parent 1634f91a35
commit f727fbc775
4 changed files with 63 additions and 7 deletions

View File

@@ -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.

View File

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

View File

@@ -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<string, number>();
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<string, number>();
applyDiscoveredContextWindows({

View File

@@ -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<void> {
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 {