mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 07:07:27 +00:00
feat(agents): support Anthropic 1M context beta header
This commit is contained in:
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Changes
|
### 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.
|
- 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.
|
- 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.
|
- Agents/Subagents: add an accepted response note for `sessions_spawn` explaining polling subagents are disabled for one-off calls. Thanks @tyler6204.
|
||||||
|
|||||||
@@ -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
|
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)).
|
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
|
## Option B: Claude setup-token
|
||||||
|
|
||||||
**Best for:** using your Claude subscription.
|
**Best for:** using your Claude subscription.
|
||||||
|
|||||||
@@ -108,6 +108,23 @@ agents:
|
|||||||
every: "55m"
|
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
|
## Tips for reducing token pressure
|
||||||
|
|
||||||
- Use `/compact` to summarize long sessions.
|
- Use `/compact` to summarize long sessions.
|
||||||
|
|||||||
@@ -112,6 +112,122 @@ describe("applyExtraParamsToAgent", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds Anthropic 1M beta header when context1m is enabled for Opus/Sonnet", () => {
|
||||||
|
const calls: Array<SimpleStreamOptions | undefined> = [];
|
||||||
|
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||||
|
calls.push(options);
|
||||||
|
return {} as ReturnType<StreamFn>;
|
||||||
|
};
|
||||||
|
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<SimpleStreamOptions | undefined> = [];
|
||||||
|
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||||
|
calls.push(options);
|
||||||
|
return {} as ReturnType<StreamFn>;
|
||||||
|
};
|
||||||
|
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<SimpleStreamOptions | undefined> = [];
|
||||||
|
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||||
|
calls.push(options);
|
||||||
|
return {} as ReturnType<StreamFn>;
|
||||||
|
};
|
||||||
|
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", () => {
|
it("forces store=true for direct OpenAI Responses payloads", () => {
|
||||||
const payload = runStoreMutationCase({
|
const payload = runStoreMutationCase({
|
||||||
applyProvider: "openai",
|
applyProvider: "openai",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const OPENROUTER_APP_HEADERS: Record<string, string> = {
|
|||||||
"HTTP-Referer": "https://openclaw.ai",
|
"HTTP-Referer": "https://openclaw.ai",
|
||||||
"X-Title": "OpenClaw",
|
"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.
|
// NOTE: We only force `store=true` for *direct* OpenAI Responses.
|
||||||
// Codex responses (chatgpt.com/backend-api/codex/responses) require `store=false`.
|
// Codex responses (chatgpt.com/backend-api/codex/responses) require `store=false`.
|
||||||
const OPENAI_RESPONSES_APIS = new Set(["openai-responses"]);
|
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<string, unknown> | undefined,
|
||||||
|
provider: string,
|
||||||
|
modelId: string,
|
||||||
|
): string[] | undefined {
|
||||||
|
if (provider !== "anthropic") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const betas = new Set<string>();
|
||||||
|
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<string, string> | undefined,
|
||||||
|
betas: string[],
|
||||||
|
): Record<string, string> {
|
||||||
|
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.
|
* Create a streamFn wrapper that adds OpenRouter app attribution headers.
|
||||||
* These headers allow OpenClaw to appear on OpenRouter's leaderboard.
|
* These headers allow OpenClaw to appear on OpenRouter's leaderboard.
|
||||||
@@ -237,6 +311,14 @@ export function applyExtraParamsToAgent(
|
|||||||
agent.streamFn = wrappedStreamFn;
|
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") {
|
if (provider === "openrouter") {
|
||||||
log.debug(`applying OpenRouter app attribution headers for ${provider}/${modelId}`);
|
log.debug(`applying OpenRouter app attribution headers for ${provider}/${modelId}`);
|
||||||
agent.streamFn = createOpenRouterHeadersWrapper(agent.streamFn);
|
agent.streamFn = createOpenRouterHeadersWrapper(agent.streamFn);
|
||||||
|
|||||||
Reference in New Issue
Block a user