diff --git a/CHANGELOG.md b/CHANGELOG.md index b1e6f4e2a20..d25ecb1caba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index 82e9bb077dc..9d65b63199a 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -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); + }); }); diff --git a/src/agents/context.test.ts b/src/agents/context.test.ts index 728d7de236d..9b5cbbb0bfa 100644 --- a/src/agents/context.test.ts +++ b/src/agents/context.test.ts @@ -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(); + 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(); applyConfiguredContextWindows({ diff --git a/src/agents/context.ts b/src/agents/context.ts index 20b3f871b45..4a5d5264042 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -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; 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) ??