mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 04:57:09 +02:00
fix(agents): suppress Anthropic beta headers for custom endpoints
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Agents/Anthropic: send implicit Anthropic beta headers only to direct public Anthropic endpoints, including OAuth, so custom Anthropic-compatible providers no longer mis-handle unsupported beta flags unless explicitly configured. Refs #73346. Thanks @byBrodowski.
|
||||
- Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock, including dist-runtime canonical roots, so Docker Desktop/WSL cold starts no longer hold `.openclaw-runtime-mirror.lock` while scanning slow persisted volumes. Fixes #73339. Thanks @1yihui.
|
||||
- Channels/LINE: persist inbound image, video, audio, and file downloads in `~/.openclaw/media/inbound/` instead of temporary files so agents can still read LINE media after `/tmp` cleanup. Fixes #73370. Thanks @hijirii and @wenxu007.
|
||||
- Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger `RangeError: Maximum call stack size exceeded`. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.
|
||||
|
||||
@@ -62,10 +62,16 @@ function latestAnthropicRequest() {
|
||||
};
|
||||
}
|
||||
|
||||
function latestAnthropicRequestHeaders() {
|
||||
return new Headers(latestAnthropicRequest().init?.headers);
|
||||
}
|
||||
|
||||
function makeAnthropicTransportModel(
|
||||
params: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
provider?: string;
|
||||
baseUrl?: string;
|
||||
reasoning?: boolean;
|
||||
maxTokens?: number;
|
||||
headers?: Record<string, string>;
|
||||
@@ -77,8 +83,8 @@ function makeAnthropicTransportModel(
|
||||
id: params.id ?? "claude-sonnet-4-6",
|
||||
name: params.name ?? "Claude Sonnet 4.6",
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
provider: params.provider ?? "anthropic",
|
||||
baseUrl: params.baseUrl ?? "https://api.anthropic.com",
|
||||
reasoning: params.reasoning ?? true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
@@ -159,6 +165,97 @@ describe("anthropic transport stream", () => {
|
||||
model: "claude-sonnet-4-6",
|
||||
stream: true,
|
||||
});
|
||||
expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBe(
|
||||
"fine-grained-tool-streaming-2025-05-14",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not add implicit Anthropic beta headers for custom compatible API-key endpoints", async () => {
|
||||
const model = makeAnthropicTransportModel({
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://custom-proxy.example",
|
||||
});
|
||||
|
||||
await runTransportStream(
|
||||
model,
|
||||
{
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
} as AnthropicStreamContext,
|
||||
{
|
||||
apiKey: "sk-ant-api",
|
||||
} as AnthropicStreamOptions,
|
||||
);
|
||||
|
||||
expect(guardedFetchMock).toHaveBeenCalledWith(
|
||||
"https://custom-proxy.example/v1/messages",
|
||||
expect.objectContaining({ method: "POST" }),
|
||||
);
|
||||
expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not add implicit Anthropic beta headers for custom compatible OAuth endpoints", async () => {
|
||||
await runTransportStream(
|
||||
makeAnthropicTransportModel({
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://custom-proxy.example",
|
||||
}),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
} as AnthropicStreamContext,
|
||||
{
|
||||
apiKey: "sk-ant-oat-token",
|
||||
} as AnthropicStreamOptions,
|
||||
);
|
||||
|
||||
const headers = latestAnthropicRequestHeaders();
|
||||
expect(headers.get("authorization")).toBe("Bearer sk-ant-oat-token");
|
||||
expect(headers.get("anthropic-beta")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps Anthropic beta headers for direct Anthropic OAuth endpoints", async () => {
|
||||
await runTransportStream(
|
||||
makeAnthropicTransportModel(),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
} as AnthropicStreamContext,
|
||||
{
|
||||
apiKey: "sk-ant-oat-token",
|
||||
} as AnthropicStreamOptions,
|
||||
);
|
||||
|
||||
expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBe(
|
||||
"claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14",
|
||||
);
|
||||
});
|
||||
|
||||
it("recognizes schemeless api.anthropic.com base URLs as direct Anthropic", async () => {
|
||||
await runTransportStream(
|
||||
makeAnthropicTransportModel({ baseUrl: "api.anthropic.com" }),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
} as AnthropicStreamContext,
|
||||
{
|
||||
apiKey: "sk-ant-api",
|
||||
} as AnthropicStreamOptions,
|
||||
);
|
||||
|
||||
expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBe(
|
||||
"fine-grained-tool-streaming-2025-05-14",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not add implicit Anthropic beta headers for foreign hosts mentioning api.anthropic.com", async () => {
|
||||
await runTransportStream(
|
||||
makeAnthropicTransportModel({ baseUrl: "https://attacker.example/api.anthropic.com" }),
|
||||
{
|
||||
messages: [{ role: "user", content: "hello" }],
|
||||
} as AnthropicStreamContext,
|
||||
{
|
||||
apiKey: "sk-ant-api",
|
||||
} as AnthropicStreamOptions,
|
||||
);
|
||||
|
||||
expect(latestAnthropicRequestHeaders().get("anthropic-beta")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores non-positive runtime maxTokens overrides and falls back to the model limit", async () => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveAnthropicPayloadPolicy,
|
||||
} from "./anthropic-payload-policy.js";
|
||||
import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./copilot-dynamic-headers.js";
|
||||
import { resolveProviderEndpoint } from "./provider-attribution.js";
|
||||
import { buildGuardedModelFetch } from "./provider-transport-fetch.js";
|
||||
import { transformTransportMessages } from "./transport-message-transform.js";
|
||||
import {
|
||||
@@ -191,6 +192,27 @@ function isAnthropicOAuthToken(apiKey: string): boolean {
|
||||
return apiKey.includes("sk-ant-oat");
|
||||
}
|
||||
|
||||
function isDirectAnthropicModel(model: Pick<AnthropicTransportModel, "provider" | "baseUrl">) {
|
||||
if (normalizeLowercaseStringOrEmpty(model.provider) !== "anthropic") {
|
||||
return false;
|
||||
}
|
||||
const endpointClass = resolveProviderEndpoint(model.baseUrl).endpointClass;
|
||||
return endpointClass === "default" || endpointClass === "anthropic-public";
|
||||
}
|
||||
|
||||
function buildAnthropicBetaHeader(
|
||||
model: AnthropicTransportModel,
|
||||
betaFeatures: readonly string[],
|
||||
params: { oauth: boolean },
|
||||
): string | undefined {
|
||||
if (!isDirectAnthropicModel(model)) {
|
||||
return undefined;
|
||||
}
|
||||
return params.oauth
|
||||
? `claude-code-20250219,oauth-2025-04-20,${betaFeatures.join(",")}`
|
||||
: betaFeatures.join(",");
|
||||
}
|
||||
|
||||
function toClaudeCodeName(name: string): string {
|
||||
return CLAUDE_CODE_TOOL_LOOKUP.get(normalizeLowercaseStringOrEmpty(name)) ?? name;
|
||||
}
|
||||
@@ -638,6 +660,7 @@ function createAnthropicTransportClient(params: {
|
||||
betaFeatures.push("interleaved-thinking-2025-05-14");
|
||||
}
|
||||
if (isAnthropicOAuthToken(apiKey)) {
|
||||
const betaHeader = buildAnthropicBetaHeader(model, betaFeatures, { oauth: true });
|
||||
return {
|
||||
client: createAnthropicMessagesClient({
|
||||
apiKey: null,
|
||||
@@ -647,7 +670,7 @@ function createAnthropicTransportClient(params: {
|
||||
{
|
||||
accept: "application/json",
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
"anthropic-beta": `claude-code-20250219,oauth-2025-04-20,${betaFeatures.join(",")}`,
|
||||
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
||||
"user-agent": `claude-cli/${CLAUDE_CODE_VERSION}`,
|
||||
"x-app": "cli",
|
||||
},
|
||||
@@ -659,6 +682,7 @@ function createAnthropicTransportClient(params: {
|
||||
isOAuthToken: true,
|
||||
};
|
||||
}
|
||||
const betaHeader = buildAnthropicBetaHeader(model, betaFeatures, { oauth: false });
|
||||
return {
|
||||
client: createAnthropicMessagesClient({
|
||||
apiKey,
|
||||
@@ -667,7 +691,7 @@ function createAnthropicTransportClient(params: {
|
||||
{
|
||||
accept: "application/json",
|
||||
"anthropic-dangerous-direct-browser-access": "true",
|
||||
"anthropic-beta": betaFeatures.join(","),
|
||||
...(betaHeader ? { "anthropic-beta": betaHeader } : {}),
|
||||
},
|
||||
model.headers,
|
||||
options?.headers,
|
||||
|
||||
Reference in New Issue
Block a user