feat(agents): support Anthropic 1M context beta header

This commit is contained in:
Peter Steinberger
2026-02-18 03:28:56 +01:00
parent d1c00dbb7c
commit c90b09cb02
5 changed files with 238 additions and 0 deletions

View File

@@ -8,6 +8,8 @@ const OPENROUTER_APP_HEADERS: Record<string, string> = {
"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<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.
* 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);