From c90b09cb02b2a1387d4e23d73c9afe3a26c0bb87 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Feb 2026 03:28:56 +0100 Subject: [PATCH] feat(agents): support Anthropic 1M context beta header --- CHANGELOG.md | 1 + docs/providers/anthropic.md | 22 ++++ docs/reference/token-use.md | 17 +++ ...pi-embedded-runner-extraparams.e2e.test.ts | 116 ++++++++++++++++++ src/agents/pi-embedded-runner/extra-params.ts | 82 +++++++++++++ 5 files changed, 238 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2e4263935..dcdd9d714a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Agents/Anthropic: add opt-in 1M context beta header support for Opus/Sonnet via model `params.context1m: true` (maps to `anthropic-beta: context-1m-2025-08-07`). - Agents/Models: support Anthropic Sonnet 4.6 (`anthropic/claude-sonnet-4-6`) across aliases/defaults with forward-compat fallback when upstream catalogs still only expose Sonnet 4.5. - Commands/Subagents: add `/subagents spawn` for deterministic subagent activation from chat commands. (#18218) Thanks @JoshuaLelon. - Agents/Subagents: add an accepted response note for `sessions_spawn` explaining polling subagents are disabled for one-off calls. Thanks @tyler6204. diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index ff82280bef6..6f9759b3b2f 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -79,6 +79,28 @@ We recommend migrating to the new `cacheRetention` parameter. OpenClaw includes the `extended-cache-ttl-2025-04-11` beta flag for Anthropic API requests; keep it if you override provider headers (see [/gateway/configuration](/gateway/configuration)). +## 1M context window (Anthropic beta) + +Anthropic's 1M context window is beta-gated. In OpenClaw, enable it per model +with `params.context1m: true` for supported Opus/Sonnet models. + +```json5 +{ + agents: { + defaults: { + models: { + "anthropic/claude-opus-4-6": { + params: { context1m: true }, + }, + }, + }, + }, +} +``` + +OpenClaw maps this to `anthropic-beta: context-1m-2025-08-07` on Anthropic +requests. + ## Option B: Claude setup-token **Best for:** using your Claude subscription. diff --git a/docs/reference/token-use.md b/docs/reference/token-use.md index 96a096259ea..7f04e19650f 100644 --- a/docs/reference/token-use.md +++ b/docs/reference/token-use.md @@ -108,6 +108,23 @@ agents: every: "55m" ``` +### Example: enable Anthropic 1M context beta header + +Anthropic's 1M context window is currently beta-gated. OpenClaw can inject the +required `anthropic-beta` value when you enable `context1m` on supported Opus +or Sonnet models. + +```yaml +agents: + defaults: + models: + "anthropic/claude-opus-4-6": + params: + context1m: true +``` + +This maps to Anthropic's `context-1m-2025-08-07` beta header. + ## Tips for reducing token pressure - Use `/compact` to summarize long sessions. diff --git a/src/agents/pi-embedded-runner-extraparams.e2e.test.ts b/src/agents/pi-embedded-runner-extraparams.e2e.test.ts index 37a29e91b86..c1d76f35180 100644 --- a/src/agents/pi-embedded-runner-extraparams.e2e.test.ts +++ b/src/agents/pi-embedded-runner-extraparams.e2e.test.ts @@ -112,6 +112,122 @@ describe("applyExtraParamsToAgent", () => { }); }); + it("adds Anthropic 1M beta header when context1m is enabled for Opus/Sonnet", () => { + const calls: Array = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + calls.push(options); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + const cfg = { + agents: { + defaults: { + models: { + "anthropic/claude-opus-4-6": { + params: { + context1m: true, + }, + }, + }, + }, + }, + }; + + applyExtraParamsToAgent(agent, cfg, "anthropic", "claude-opus-4-6"); + + const model = { + api: "anthropic-messages", + provider: "anthropic", + id: "claude-opus-4-6", + } as Model<"anthropic-messages">; + const context: Context = { messages: [] }; + + void agent.streamFn?.(model, context, { headers: { "X-Custom": "1" } }); + + expect(calls).toHaveLength(1); + expect(calls[0]?.headers).toEqual({ + "X-Custom": "1", + "anthropic-beta": "context-1m-2025-08-07", + }); + }); + + it("merges existing anthropic-beta headers with configured betas", () => { + const calls: Array = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + calls.push(options); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + const cfg = { + agents: { + defaults: { + models: { + "anthropic/claude-sonnet-4-5": { + params: { + context1m: true, + anthropicBeta: ["files-api-2025-04-14"], + }, + }, + }, + }, + }, + }; + + applyExtraParamsToAgent(agent, cfg, "anthropic", "claude-sonnet-4-5"); + + const model = { + api: "anthropic-messages", + provider: "anthropic", + id: "claude-sonnet-4-5", + } as Model<"anthropic-messages">; + const context: Context = { messages: [] }; + + void agent.streamFn?.(model, context, { + headers: { "anthropic-beta": "prompt-caching-2024-07-31" }, + }); + + expect(calls).toHaveLength(1); + expect(calls[0]?.headers).toEqual({ + "anthropic-beta": "prompt-caching-2024-07-31,files-api-2025-04-14,context-1m-2025-08-07", + }); + }); + + it("ignores context1m for non-Opus/Sonnet Anthropic models", () => { + const calls: Array = []; + const baseStreamFn: StreamFn = (_model, _context, options) => { + calls.push(options); + return {} as ReturnType; + }; + const agent = { streamFn: baseStreamFn }; + const cfg = { + agents: { + defaults: { + models: { + "anthropic/claude-haiku-3-5": { + params: { + context1m: true, + }, + }, + }, + }, + }, + }; + + applyExtraParamsToAgent(agent, cfg, "anthropic", "claude-haiku-3-5"); + + const model = { + api: "anthropic-messages", + provider: "anthropic", + id: "claude-haiku-3-5", + } as Model<"anthropic-messages">; + const context: Context = { messages: [] }; + + void agent.streamFn?.(model, context, { headers: { "X-Custom": "1" } }); + + expect(calls).toHaveLength(1); + expect(calls[0]?.headers).toEqual({ "X-Custom": "1" }); + }); + it("forces store=true for direct OpenAI Responses payloads", () => { const payload = runStoreMutationCase({ applyProvider: "openai", diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/pi-embedded-runner/extra-params.ts index 70154e5b550..0a4efde24c9 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/pi-embedded-runner/extra-params.ts @@ -8,6 +8,8 @@ const OPENROUTER_APP_HEADERS: Record = { "HTTP-Referer": "https://openclaw.ai", "X-Title": "OpenClaw", }; +const ANTHROPIC_CONTEXT_1M_BETA = "context-1m-2025-08-07"; +const ANTHROPIC_1M_MODEL_PREFIXES = ["claude-opus-4", "claude-sonnet-4"] as const; // NOTE: We only force `store=true` for *direct* OpenAI Responses. // Codex responses (chatgpt.com/backend-api/codex/responses) require `store=false`. const OPENAI_RESPONSES_APIS = new Set(["openai-responses"]); @@ -156,6 +158,78 @@ function createOpenAIResponsesStoreWrapper(baseStreamFn: StreamFn | undefined): }; } +function isAnthropic1MModel(modelId: string): boolean { + const normalized = modelId.trim().toLowerCase(); + return ANTHROPIC_1M_MODEL_PREFIXES.some((prefix) => normalized.startsWith(prefix)); +} + +function parseHeaderList(value: unknown): string[] { + if (typeof value !== "string") { + return []; + } + return value + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +function resolveAnthropicBetas( + extraParams: Record | undefined, + provider: string, + modelId: string, +): string[] | undefined { + if (provider !== "anthropic") { + return undefined; + } + + const betas = new Set(); + const configured = extraParams?.anthropicBeta; + if (typeof configured === "string" && configured.trim()) { + betas.add(configured.trim()); + } else if (Array.isArray(configured)) { + for (const beta of configured) { + if (typeof beta === "string" && beta.trim()) { + betas.add(beta.trim()); + } + } + } + + if (extraParams?.context1m === true) { + if (isAnthropic1MModel(modelId)) { + betas.add(ANTHROPIC_CONTEXT_1M_BETA); + } else { + log.warn(`ignoring context1m for non-opus/sonnet model: ${provider}/${modelId}`); + } + } + + return betas.size > 0 ? [...betas] : undefined; +} + +function mergeAnthropicBetaHeader( + headers: Record | undefined, + betas: string[], +): Record { + const merged = { ...headers }; + const existingKey = Object.keys(merged).find((key) => key.toLowerCase() === "anthropic-beta"); + const existing = existingKey ? parseHeaderList(merged[existingKey]) : []; + const values = Array.from(new Set([...existing, ...betas])); + const key = existingKey ?? "anthropic-beta"; + merged[key] = values.join(","); + return merged; +} + +function createAnthropicBetaHeadersWrapper( + baseStreamFn: StreamFn | undefined, + betas: string[], +): StreamFn { + const underlying = baseStreamFn ?? streamSimple; + return (model, context, options) => + underlying(model, context, { + ...options, + headers: mergeAnthropicBetaHeader(options?.headers, betas), + }); +} + /** * Create a streamFn wrapper that adds OpenRouter app attribution headers. * These headers allow OpenClaw to appear on OpenRouter's leaderboard. @@ -237,6 +311,14 @@ export function applyExtraParamsToAgent( agent.streamFn = wrappedStreamFn; } + const anthropicBetas = resolveAnthropicBetas(merged, provider, modelId); + if (anthropicBetas?.length) { + log.debug( + `applying Anthropic beta header for ${provider}/${modelId}: ${anthropicBetas.join(",")}`, + ); + agent.streamFn = createAnthropicBetaHeadersWrapper(agent.streamFn, anthropicBetas); + } + if (provider === "openrouter") { log.debug(`applying OpenRouter app attribution headers for ${provider}/${modelId}`); agent.streamFn = createOpenRouterHeadersWrapper(agent.streamFn);