fix(status): normalize qualified context window keys openclaw#36389 thanks @haoruilee

This commit is contained in:
Josh Lehman 2026-03-10 14:21:55 -07:00 committed by Cursor Agent
parent 4bc4e66057
commit f2df5d60e6
4 changed files with 51 additions and 4 deletions

View File

@ -139,6 +139,9 @@ Docs: https://docs.openclaw.ai
- Telegram/direct delivery: bridge direct delivery sends to internal `message:sent` hooks so internal hook listeners observe successful Telegram deliveries. (#40185) Thanks @vincentkoc.
- Dependencies: refresh workspace dependencies except the pinned Carbon package, and harden ACP session-config writes against non-string SDK values so newer ACP clients fail fast instead of tripping type/runtime mismatches.
- Telegram/polling restarts: clear bounded cleanup timeout handles after `runner.stop()` and `bot.stop()` settle so stall recovery no longer leaves stray 15-second timers behind on clean shutdown. (#43188) thanks @kyohwang.
- Gateway/config errors: surface up to three validation issues in top-level `config.set`, `config.patch`, and `config.apply` error messages while preserving structured issue details. (#42664) Thanks @huntharo.
- Hooks/plugin context parity followup: pass `trigger` and `channelId` through embedded `llm_input`, `agent_end`, and `llm_output` hook contexts so plugins receive the same agent metadata across hook phases. (#42362) Thanks @zhoulf1006.
- Status/context windows: normalize provider-qualified override cache keys so `/status` resolves the active provider's configured context window even when `models.providers` keys use mixed case or surrounding whitespace. (#36389) Thanks @haoruilee.
## 2026.3.8

View File

@ -138,4 +138,24 @@ describe("lookupContextTokens", () => {
});
expect(result).toBe(1_048_576);
});
it("resolveContextTokensForModel honors configured overrides when provider keys use mixed case", async () => {
mockDiscoveryDeps(
[{ id: "openrouter/anthropic/claude-sonnet-4-5", contextWindow: 1_048_576 }],
{
" OpenRouter ": {
models: [{ id: "anthropic/claude-sonnet-4-5", contextWindow: 200_000 }],
},
},
);
const { resolveContextTokensForModel } = await import("./context.js");
await new Promise((r) => setTimeout(r, 0));
const result = resolveContextTokensForModel({
provider: "openrouter",
model: "anthropic/claude-sonnet-4-5",
});
expect(result).toBe(200_000);
});
});

View File

@ -107,6 +107,25 @@ describe("applyConfiguredContextWindows", () => {
expect(cache.get("openrouter/anthropic/claude-sonnet-4-5")).toBe(200_000);
});
it("normalizes provider keys before writing provider-qualified override entries", () => {
const cache = new Map<string, number>();
cache.set("openrouter/anthropic/claude-sonnet-4-5", 1_048_576);
applyConfiguredContextWindows({
cache,
modelsConfig: {
providers: {
" OpenRouter ": {
models: [{ id: "anthropic/claude-sonnet-4-5", contextWindow: 200_000 }],
},
},
},
});
expect(cache.get("anthropic/claude-sonnet-4-5")).toBe(200_000);
expect(cache.get("openrouter/anthropic/claude-sonnet-4-5")).toBe(200_000);
expect(cache.has(" OpenRouter /anthropic/claude-sonnet-4-5")).toBe(false);
});
it("adds config-only model context windows and ignores invalid entries", () => {
const cache = new Map<string, number>();
applyConfiguredContextWindows({

View File

@ -6,6 +6,7 @@ import type { OpenClawConfig } from "../config/config.js";
import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js";
import { consumeRootOptionToken, FLAG_TERMINATOR } from "../infra/cli-root-options.js";
import { resolveOpenClawAgentDir } from "./agent-paths.js";
import { normalizeProviderId } from "./model-selection.js";
import { ensureOpenClawModelsJson } from "./models-config.js";
type ModelEntry = { id: string; contextWindow?: number };
@ -27,6 +28,10 @@ const CONFIG_LOAD_RETRY_POLICY: BackoffPolicy = {
jitter: 0,
};
function resolveQualifiedContextWindowKey(providerId: string, modelId: string): string {
return `${normalizeProviderId(providerId)}/${modelId}`;
}
export function applyDiscoveredContextWindows(params: {
cache: Map<string, number>;
models: ModelEntry[];
@ -78,7 +83,7 @@ export function applyConfiguredContextWindows(params: {
// discovered values. This covers both bare IDs (e.g. "claude-opus-4" →
// "anthropic/claude-opus-4") and slash-containing IDs common in OpenRouter
// (e.g. "anthropic/claude-sonnet-4-5" → "openrouter/anthropic/claude-sonnet-4-5").
params.cache.set(`${providerId}/${modelId}`, contextWindow);
params.cache.set(resolveQualifiedContextWindowKey(providerId, modelId), contextWindow);
}
}
}
@ -232,13 +237,13 @@ function resolveProviderModelRef(params: {
}
const providerRaw = params.provider?.trim();
if (providerRaw) {
return { provider: providerRaw.toLowerCase(), model: modelRaw };
return { provider: normalizeProviderId(providerRaw), model: modelRaw };
}
const slash = modelRaw.indexOf("/");
if (slash <= 0) {
return undefined;
}
const provider = modelRaw.slice(0, slash).trim().toLowerCase();
const provider = normalizeProviderId(modelRaw.slice(0, slash));
const model = modelRaw.slice(slash + 1).trim();
if (!provider || !model) {
return undefined;
@ -282,7 +287,7 @@ export function resolveContextTokensForModel(params: {
// When provider is known, prefer the provider-qualified key so the correct
// entry is found even when the same bare model id is catalogued under
// multiple providers with different context limits.
const qualifiedKey = ref ? `${ref.provider}/${ref.model}` : undefined;
const qualifiedKey = ref ? resolveQualifiedContextWindowKey(ref.provider, ref.model) : undefined;
return (
(qualifiedKey ? lookupContextTokens(qualifiedKey) : undefined) ??
lookupContextTokens(params.model) ??