mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 04:16:25 +00:00
Providers: skip context1m beta for Anthropic OAuth tokens (#24620)
* Providers: skip context1m beta for Anthropic OAuth tokens * Tests: cover OAuth context1m beta skip behavior * Docs: note context1m OAuth incompatibility * Agents: add context1m-aware context token resolver * Agents: cover context1m context-token resolver * Commands: apply context1m-aware context tokens in session store * Commands: apply context1m-aware context tokens in status summary * Status: resolve context tokens with context1m model params * Status: test context1m status context display
This commit is contained in:
@@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Providers/Anthropic: skip `context-1m-*` beta injection for OAuth/subscription tokens (`sk-ant-oat-*`) while preserving OAuth-required betas, avoiding Anthropic 401 auth failures when `params.context1m` is enabled. (#10647, #20354) Thanks @ClumsyWizardHands and @dcruver.
|
||||
- Gateway/WS: close repeated post-handshake `unauthorized role:*` request floods per connection and sample duplicate rejection logs, preventing a single misbehaving client from degrading gateway responsiveness. (#20168) Thanks @acy103, @vibecodooor, and @vincentkoc.
|
||||
- Agents/Reasoning: when model-default thinking is active (for example `thinking=low`), keep auto-reasoning disabled unless explicitly enabled, preventing `Reasoning:` thinking-block leakage in channel replies. (#24335, #24290) thanks @Kay-051.
|
||||
- Agents/Reasoning: avoid classifying provider reasoning-required errors as context overflows so these failures no longer trigger compaction-style overflow recovery. (#24593) Thanks @vincentkoc.
|
||||
|
||||
@@ -101,6 +101,10 @@ with `params.context1m: true` for supported Opus/Sonnet models.
|
||||
OpenClaw maps this to `anthropic-beta: context-1m-2025-08-07` on Anthropic
|
||||
requests.
|
||||
|
||||
Note: Anthropic currently rejects `context-1m-*` beta requests when using
|
||||
OAuth/subscription tokens (`sk-ant-oat-*`). OpenClaw automatically skips the
|
||||
context1m beta header for OAuth auth and keeps the required OAuth betas.
|
||||
|
||||
## Option B: Claude setup-token
|
||||
|
||||
**Best for:** using your Claude subscription.
|
||||
|
||||
@@ -125,6 +125,10 @@ agents:
|
||||
|
||||
This maps to Anthropic's `context-1m-2025-08-07` beta header.
|
||||
|
||||
If you authenticate Anthropic with OAuth/subscription tokens (`sk-ant-oat-*`),
|
||||
OpenClaw skips the `context-1m-*` beta header because Anthropic currently
|
||||
rejects that combination with HTTP 401.
|
||||
|
||||
## Tips for reducing token pressure
|
||||
|
||||
- Use `/compact` to summarize long sessions.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyConfiguredContextWindows, applyDiscoveredContextWindows } from "./context.js";
|
||||
import {
|
||||
ANTHROPIC_CONTEXT_1M_TOKENS,
|
||||
applyConfiguredContextWindows,
|
||||
applyDiscoveredContextWindows,
|
||||
resolveContextTokensForModel,
|
||||
} from "./context.js";
|
||||
import { createSessionManagerRuntimeRegistry } from "./pi-extensions/session-manager-runtime-registry.js";
|
||||
|
||||
describe("applyDiscoveredContextWindows", () => {
|
||||
@@ -75,3 +80,47 @@ describe("createSessionManagerRuntimeRegistry", () => {
|
||||
expect(registry.get(123)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveContextTokensForModel", () => {
|
||||
it("returns 1M context when anthropic context1m is enabled for opus/sonnet", () => {
|
||||
const result = resolveContextTokensForModel({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-6": {
|
||||
params: { context1m: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-6",
|
||||
fallbackContextTokens: 200_000,
|
||||
});
|
||||
|
||||
expect(result).toBe(ANTHROPIC_CONTEXT_1M_TOKENS);
|
||||
});
|
||||
|
||||
it("does not force 1M context for non-opus/sonnet Anthropic models", () => {
|
||||
const result = resolveContextTokensForModel({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-haiku-3-5": {
|
||||
params: { context1m: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "anthropic",
|
||||
model: "claude-haiku-3-5",
|
||||
fallbackContextTokens: 200_000,
|
||||
});
|
||||
|
||||
expect(result).toBe(200_000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// the agent reports a model id. This includes custom models.json entries.
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveOpenClawAgentDir } from "./agent-paths.js";
|
||||
import { ensureOpenClawModelsJson } from "./models-config.js";
|
||||
|
||||
@@ -13,6 +14,10 @@ type ModelRegistryLike = {
|
||||
type ConfigModelEntry = { id?: string; contextWindow?: number };
|
||||
type ProviderConfigEntry = { models?: ConfigModelEntry[] };
|
||||
type ModelsConfig = { providers?: Record<string, ProviderConfigEntry | undefined> };
|
||||
type AgentModelEntry = { params?: Record<string, unknown> };
|
||||
|
||||
const ANTHROPIC_1M_MODEL_PREFIXES = ["claude-opus-4", "claude-sonnet-4"] as const;
|
||||
export const ANTHROPIC_CONTEXT_1M_TOKENS = 1_048_576;
|
||||
|
||||
export function applyDiscoveredContextWindows(params: {
|
||||
cache: Map<string, number>;
|
||||
@@ -109,3 +114,82 @@ export function lookupContextTokens(modelId?: string): number | undefined {
|
||||
void loadPromise;
|
||||
return MODEL_CACHE.get(modelId);
|
||||
}
|
||||
|
||||
function resolveConfiguredModelParams(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
provider: string,
|
||||
model: string,
|
||||
): Record<string, unknown> | undefined {
|
||||
const models = cfg?.agents?.defaults?.models;
|
||||
if (!models) {
|
||||
return undefined;
|
||||
}
|
||||
const key = `${provider}/${model}`.trim().toLowerCase();
|
||||
for (const [rawKey, entry] of Object.entries(models)) {
|
||||
if (rawKey.trim().toLowerCase() === key) {
|
||||
const params = (entry as AgentModelEntry | undefined)?.params;
|
||||
return params && typeof params === "object" ? params : undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveProviderModelRef(params: {
|
||||
provider?: string;
|
||||
model?: string;
|
||||
}): { provider: string; model: string } | undefined {
|
||||
const modelRaw = params.model?.trim();
|
||||
if (!modelRaw) {
|
||||
return undefined;
|
||||
}
|
||||
const providerRaw = params.provider?.trim();
|
||||
if (providerRaw) {
|
||||
return { provider: providerRaw.toLowerCase(), model: modelRaw };
|
||||
}
|
||||
const slash = modelRaw.indexOf("/");
|
||||
if (slash <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
const provider = modelRaw.slice(0, slash).trim().toLowerCase();
|
||||
const model = modelRaw.slice(slash + 1).trim();
|
||||
if (!provider || !model) {
|
||||
return undefined;
|
||||
}
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
function isAnthropic1MModel(provider: string, model: string): boolean {
|
||||
if (provider !== "anthropic") {
|
||||
return false;
|
||||
}
|
||||
const normalized = model.trim().toLowerCase();
|
||||
const modelId = normalized.includes("/")
|
||||
? (normalized.split("/").at(-1) ?? normalized)
|
||||
: normalized;
|
||||
return ANTHROPIC_1M_MODEL_PREFIXES.some((prefix) => modelId.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function resolveContextTokensForModel(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
contextTokensOverride?: number;
|
||||
fallbackContextTokens?: number;
|
||||
}): number | undefined {
|
||||
if (typeof params.contextTokensOverride === "number" && params.contextTokensOverride > 0) {
|
||||
return params.contextTokensOverride;
|
||||
}
|
||||
|
||||
const ref = resolveProviderModelRef({
|
||||
provider: params.provider,
|
||||
model: params.model,
|
||||
});
|
||||
if (ref) {
|
||||
const modelParams = resolveConfiguredModelParams(params.cfg, ref.provider, ref.model);
|
||||
if (modelParams?.context1m === true && isAnthropic1MModel(ref.provider, ref.model)) {
|
||||
return ANTHROPIC_CONTEXT_1M_TOKENS;
|
||||
}
|
||||
}
|
||||
|
||||
return lookupContextTokens(params.model) ?? params.fallbackContextTokens;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves oauth-2025-04-20 beta when context1m is enabled with an OAuth token", () => {
|
||||
it("skips context1m beta for OAuth tokens but preserves OAuth-required betas", () => {
|
||||
const calls: Array<SimpleStreamOptions | undefined> = [];
|
||||
const baseStreamFn: StreamFn = (_model, _context, options) => {
|
||||
calls.push(options);
|
||||
@@ -220,7 +220,7 @@ describe("applyExtraParamsToAgent", () => {
|
||||
// Must include the OAuth-required betas so they aren't stripped by pi-ai's mergeHeaders
|
||||
expect(betaHeader).toContain("oauth-2025-04-20");
|
||||
expect(betaHeader).toContain("claude-code-20250219");
|
||||
expect(betaHeader).toContain("context-1m-2025-08-07");
|
||||
expect(betaHeader).not.toContain("context-1m-2025-08-07");
|
||||
});
|
||||
|
||||
it("merges existing anthropic-beta headers with configured betas", () => {
|
||||
|
||||
@@ -276,13 +276,25 @@ function createAnthropicBetaHeadersWrapper(
|
||||
): StreamFn {
|
||||
const underlying = baseStreamFn ?? streamSimple;
|
||||
return (model, context, options) => {
|
||||
const isOauth = isAnthropicOAuthApiKey(options?.apiKey);
|
||||
const requestedContext1m = betas.includes(ANTHROPIC_CONTEXT_1M_BETA);
|
||||
const effectiveBetas =
|
||||
isOauth && requestedContext1m
|
||||
? betas.filter((beta) => beta !== ANTHROPIC_CONTEXT_1M_BETA)
|
||||
: betas;
|
||||
if (isOauth && requestedContext1m) {
|
||||
log.warn(
|
||||
`ignoring context1m for OAuth token auth on ${model.provider}/${model.id}; Anthropic rejects context-1m beta with OAuth auth`,
|
||||
);
|
||||
}
|
||||
|
||||
// Preserve the betas pi-ai's createClient would inject for the given token type.
|
||||
// Without this, our options.headers["anthropic-beta"] overwrites the pi-ai
|
||||
// defaultHeaders via Object.assign, stripping critical betas like oauth-2025-04-20.
|
||||
const piAiBetas = isAnthropicOAuthApiKey(options?.apiKey)
|
||||
const piAiBetas = isOauth
|
||||
? (PI_AI_OAUTH_ANTHROPIC_BETAS as readonly string[])
|
||||
: (PI_AI_DEFAULT_ANTHROPIC_BETAS as readonly string[]);
|
||||
const allBetas = [...new Set([...piAiBetas, ...betas])];
|
||||
const allBetas = [...new Set([...piAiBetas, ...effectiveBetas])];
|
||||
return underlying(model, context, {
|
||||
...options,
|
||||
headers: mergeAnthropicBetaHeader(options?.headers, allBetas),
|
||||
|
||||
@@ -120,6 +120,36 @@ describe("buildStatusMessage", () => {
|
||||
expect(normalized).toContain("channel override");
|
||||
});
|
||||
|
||||
it("shows 1M context window when anthropic context1m is enabled", () => {
|
||||
const text = buildStatusMessage({
|
||||
config: {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
models: {
|
||||
"anthropic/claude-opus-4-6": {
|
||||
params: { context1m: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
agent: {
|
||||
model: "anthropic/claude-opus-4-6",
|
||||
},
|
||||
sessionEntry: {
|
||||
sessionId: "ctx1m",
|
||||
updatedAt: 0,
|
||||
totalTokens: 200_000,
|
||||
},
|
||||
sessionKey: "agent:main:main",
|
||||
sessionScope: "per-sender",
|
||||
queue: { mode: "collect", depth: 0 },
|
||||
});
|
||||
|
||||
expect(normalizeTestText(text)).toContain("Context: 200k/1.0m");
|
||||
});
|
||||
|
||||
it("uses per-agent sandbox config when config and session key are provided", () => {
|
||||
const text = buildStatusMessage({
|
||||
config: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from "node:fs";
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import { resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveModelAuthMode } from "../agents/model-auth.js";
|
||||
import {
|
||||
@@ -398,12 +398,29 @@ const formatVoiceModeLine = (
|
||||
export function buildStatusMessage(args: StatusArgs): string {
|
||||
const now = args.now ?? Date.now();
|
||||
const entry = args.sessionEntry;
|
||||
const selectionConfig = {
|
||||
agents: {
|
||||
defaults: args.agent ?? {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const contextConfig = args.config
|
||||
? ({
|
||||
...args.config,
|
||||
agents: {
|
||||
...args.config.agents,
|
||||
defaults: {
|
||||
...args.config.agents?.defaults,
|
||||
...args.agent,
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig)
|
||||
: ({
|
||||
agents: {
|
||||
defaults: args.agent ?? {},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
const resolved = resolveConfiguredModelRef({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: args.agent ?? {},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
cfg: selectionConfig,
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
@@ -417,10 +434,13 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
let activeProvider = modelRefs.active.provider;
|
||||
let activeModel = modelRefs.active.model;
|
||||
let contextTokens =
|
||||
entry?.contextTokens ??
|
||||
args.agent?.contextTokens ??
|
||||
lookupContextTokens(activeModel) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
resolveContextTokensForModel({
|
||||
cfg: contextConfig,
|
||||
provider: activeProvider,
|
||||
model: activeModel,
|
||||
contextTokensOverride: entry?.contextTokens ?? args.agent?.contextTokens,
|
||||
fallbackContextTokens: DEFAULT_CONTEXT_TOKENS,
|
||||
}) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
let inputTokens = entry?.inputTokens;
|
||||
let outputTokens = entry?.outputTokens;
|
||||
@@ -457,7 +477,12 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
}
|
||||
}
|
||||
if (!contextTokens && logUsage.model) {
|
||||
contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens;
|
||||
contextTokens =
|
||||
resolveContextTokensForModel({
|
||||
cfg: contextConfig,
|
||||
model: logUsage.model,
|
||||
fallbackContextTokens: contextTokens ?? undefined,
|
||||
}) ?? contextTokens;
|
||||
}
|
||||
if (!inputTokens || inputTokens === 0) {
|
||||
inputTokens = logUsage.input;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { setCliSessionId } from "../../agents/cli-session.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { resolveContextTokensForModel } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { isCliProvider } from "../../agents/model-selection.js";
|
||||
import { deriveSessionTotalTokens, hasNonzeroUsage } from "../../agents/usage.js";
|
||||
@@ -42,7 +42,13 @@ export async function updateSessionStoreAfterAgentRun(params: {
|
||||
const modelUsed = result.meta.agentMeta?.model ?? fallbackModel ?? defaultModel;
|
||||
const providerUsed = result.meta.agentMeta?.provider ?? fallbackProvider ?? defaultProvider;
|
||||
const contextTokens =
|
||||
params.contextTokensOverride ?? lookupContextTokens(modelUsed) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
resolveContextTokensForModel({
|
||||
cfg,
|
||||
provider: providerUsed,
|
||||
model: modelUsed,
|
||||
contextTokensOverride: params.contextTokensOverride,
|
||||
fallbackContextTokens: DEFAULT_CONTEXT_TOKENS,
|
||||
}) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
const entry = sessionStore[sessionKey] ?? {
|
||||
sessionId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import { resolveContextTokensForModel } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
@@ -105,9 +105,13 @@ export async function getStatusSummary(
|
||||
});
|
||||
const configModel = resolved.model ?? DEFAULT_MODEL;
|
||||
const configContextTokens =
|
||||
cfg.agents?.defaults?.contextTokens ??
|
||||
lookupContextTokens(configModel) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
resolveContextTokensForModel({
|
||||
cfg,
|
||||
provider: resolved.provider ?? DEFAULT_PROVIDER,
|
||||
model: configModel,
|
||||
contextTokensOverride: cfg.agents?.defaults?.contextTokens,
|
||||
fallbackContextTokens: DEFAULT_CONTEXT_TOKENS,
|
||||
}) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
const now = Date.now();
|
||||
const storeCache = new Map<string, Record<string, SessionEntry | undefined>>();
|
||||
@@ -132,7 +136,13 @@ export async function getStatusSummary(
|
||||
const resolvedModel = resolveSessionModelRef(cfg, entry, opts.agentIdOverride);
|
||||
const model = resolvedModel.model ?? configModel ?? null;
|
||||
const contextTokens =
|
||||
entry?.contextTokens ?? lookupContextTokens(model) ?? configContextTokens ?? null;
|
||||
resolveContextTokensForModel({
|
||||
cfg,
|
||||
provider: resolvedModel.provider,
|
||||
model,
|
||||
contextTokensOverride: entry?.contextTokens,
|
||||
fallbackContextTokens: configContextTokens ?? undefined,
|
||||
}) ?? null;
|
||||
const total = resolveFreshSessionTotalTokens(entry);
|
||||
const totalTokensFresh =
|
||||
typeof entry?.totalTokens === "number" ? entry?.totalTokensFresh !== false : false;
|
||||
|
||||
Reference in New Issue
Block a user