mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(agents): bound Anthropic cache markers
This commit is contained in:
@@ -14,6 +14,7 @@ type TestPayload = {
|
||||
messages: Array<{ role: string; content: unknown }>;
|
||||
service_tier?: string;
|
||||
system?: unknown;
|
||||
tools?: unknown;
|
||||
};
|
||||
|
||||
function textBlock(text: string, cache_control?: { type: "ephemeral"; ttl?: "1h" }) {
|
||||
@@ -214,6 +215,55 @@ describe("anthropic payload policy", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the latest tool result when only one message cache marker remains", () => {
|
||||
const policy = resolveAnthropicPayloadPolicy({
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
cacheRetention: "short",
|
||||
enableCacheControl: true,
|
||||
});
|
||||
const payload: TestPayload = {
|
||||
system: [
|
||||
{ type: "text", text: "Claude Code identity." },
|
||||
{ type: "text", text: "Follow policy." },
|
||||
],
|
||||
tools: [{ name: "Read", cache_control: { type: "ephemeral" } }],
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Investigate the cache writes." }],
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "I'll inspect the logs." }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "tool_result", tool_use_id: "tool_1", content: "log chunk" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
applyAnthropicPayloadPolicyToParams(payload, policy);
|
||||
|
||||
expect(payload.messages[0]).toEqual({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Investigate the cache writes." }],
|
||||
});
|
||||
expect(payload.messages[2]).toEqual({
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool_1",
|
||||
content: "log chunk",
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("denies proxied Anthropic service tier but honors explicit long TTL for custom hosts", () => {
|
||||
const policy = resolveAnthropicPayloadPolicy({
|
||||
provider: "anthropic",
|
||||
|
||||
@@ -27,6 +27,8 @@ type AnthropicPayloadPolicyInput = {
|
||||
serviceTier?: AnthropicServiceTier;
|
||||
};
|
||||
|
||||
const ANTHROPIC_CACHE_CONTROL_LIMIT = 4;
|
||||
|
||||
/** @deprecated Anthropic-family provider payload helper; do not use from third-party plugins. */
|
||||
export type AnthropicPayloadPolicy = {
|
||||
allowsServiceTier: boolean;
|
||||
@@ -142,8 +144,9 @@ function stripAnthropicSystemPromptBoundary(system: unknown): void {
|
||||
function applyAnthropicCacheControlToMessages(
|
||||
messages: unknown,
|
||||
cacheControl: AnthropicEphemeralCacheControl,
|
||||
markerLimit: number,
|
||||
): void {
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
if (!Array.isArray(messages) || messages.length === 0 || markerLimit <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -162,6 +165,10 @@ function applyAnthropicCacheControlToMessages(
|
||||
|
||||
const content = record.content;
|
||||
if (typeof content === "string") {
|
||||
if (fallbackToolResult && markerLimit === 1) {
|
||||
fallbackToolResult.cache_control = cacheControl;
|
||||
return;
|
||||
}
|
||||
record.content = [
|
||||
{
|
||||
type: "text",
|
||||
@@ -169,6 +176,9 @@ function applyAnthropicCacheControlToMessages(
|
||||
cache_control: cacheControl,
|
||||
},
|
||||
];
|
||||
if (fallbackToolResult && markerLimit > 1) {
|
||||
fallbackToolResult.cache_control = cacheControl;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,8 +194,12 @@ function applyAnthropicCacheControlToMessages(
|
||||
|
||||
const blockRecord = block as Record<string, unknown>;
|
||||
if (blockRecord.type === "text" || blockRecord.type === "image") {
|
||||
if (fallbackToolResult && markerLimit === 1) {
|
||||
fallbackToolResult.cache_control = cacheControl;
|
||||
return;
|
||||
}
|
||||
blockRecord.cache_control = cacheControl;
|
||||
if (fallbackToolResult) {
|
||||
if (fallbackToolResult && markerLimit > 1) {
|
||||
fallbackToolResult.cache_control = cacheControl;
|
||||
}
|
||||
return;
|
||||
@@ -201,6 +215,20 @@ function applyAnthropicCacheControlToMessages(
|
||||
}
|
||||
}
|
||||
|
||||
function countAnthropicCacheControlMarkers(blocks: unknown): number {
|
||||
if (!Array.isArray(blocks)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const block of blocks) {
|
||||
if (block && typeof block === "object" && "cache_control" in block) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/** @deprecated Anthropic-family provider payload helper; do not use from third-party plugins. */
|
||||
export function resolveAnthropicPayloadPolicy(
|
||||
input: AnthropicPayloadPolicyInput,
|
||||
@@ -246,7 +274,14 @@ export function applyAnthropicPayloadPolicyToParams(
|
||||
return;
|
||||
}
|
||||
|
||||
applyAnthropicCacheControlToMessages(payloadObj.messages, policy.cacheControl);
|
||||
const usedMarkers =
|
||||
countAnthropicCacheControlMarkers(payloadObj.system) +
|
||||
countAnthropicCacheControlMarkers(payloadObj.tools);
|
||||
applyAnthropicCacheControlToMessages(
|
||||
payloadObj.messages,
|
||||
policy.cacheControl,
|
||||
ANTHROPIC_CACHE_CONTROL_LIMIT - usedMarkers,
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated Anthropic-family provider payload helper; do not use from third-party plugins. */
|
||||
|
||||
@@ -41,6 +41,8 @@ import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copi
|
||||
import { adjustMaxTokensForThinking, buildBaseOptions } from "./simple-options.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
|
||||
const ANTHROPIC_CACHE_CONTROL_LIMIT = 4;
|
||||
|
||||
/**
|
||||
* Resolve cache retention preference.
|
||||
* Defaults to "short" and uses OPENCLAW_CACHE_RETENTION for backward compatibility.
|
||||
@@ -939,9 +941,27 @@ function buildParams(
|
||||
options?: AnthropicOptions,
|
||||
): MessageCreateParamsStreaming {
|
||||
const { cacheControl } = getCacheControl(model, options?.cacheRetention);
|
||||
const systemCacheControlCount = !cacheControl
|
||||
? 0
|
||||
: isOAuthTokenResult
|
||||
? 1 + (context.systemPrompt ? 1 : 0)
|
||||
: context.systemPrompt
|
||||
? 1
|
||||
: 0;
|
||||
const toolCacheControlCount = cacheControl && context.tools?.length ? 1 : 0;
|
||||
const messageCacheControlLimit = Math.max(
|
||||
0,
|
||||
ANTHROPIC_CACHE_CONTROL_LIMIT - systemCacheControlCount - toolCacheControlCount,
|
||||
);
|
||||
const params: MessageCreateParamsStreaming = {
|
||||
model: model.id,
|
||||
messages: convertMessages(context.messages, model, isOAuthTokenResult, cacheControl),
|
||||
messages: convertMessages(
|
||||
context.messages,
|
||||
model,
|
||||
isOAuthTokenResult,
|
||||
cacheControl,
|
||||
messageCacheControlLimit,
|
||||
),
|
||||
max_tokens: options?.maxTokens ?? model.maxTokens,
|
||||
stream: true,
|
||||
};
|
||||
@@ -1041,6 +1061,7 @@ function convertMessages(
|
||||
model: Model<"anthropic-messages">,
|
||||
isOAuthTokenValue: boolean,
|
||||
cacheControl?: CacheControlEphemeral,
|
||||
messageCacheControlLimit = 4,
|
||||
): MessageParam[] {
|
||||
const params: MessageParam[] = [];
|
||||
|
||||
@@ -1178,7 +1199,7 @@ function convertMessages(
|
||||
}
|
||||
}
|
||||
|
||||
if (cacheControl && params.length > 0) {
|
||||
if (cacheControl && params.length > 0 && messageCacheControlLimit > 0) {
|
||||
let fallbackToolResult: ContentBlockParam | undefined;
|
||||
|
||||
for (let i = params.length - 1; i >= 0; i--) {
|
||||
@@ -1191,14 +1212,13 @@ function convertMessages(
|
||||
for (let j = message.content.length - 1; j >= 0; j--) {
|
||||
const block = message.content[j];
|
||||
if (block.type === "text" || block.type === "image") {
|
||||
(block as typeof block & { cache_control?: typeof cacheControl }).cache_control =
|
||||
cacheControl;
|
||||
if (fallbackToolResult) {
|
||||
(
|
||||
fallbackToolResult as typeof fallbackToolResult & {
|
||||
cache_control?: typeof cacheControl;
|
||||
}
|
||||
).cache_control = cacheControl;
|
||||
if (fallbackToolResult && messageCacheControlLimit === 1) {
|
||||
applyContentBlockCacheControl(fallbackToolResult, cacheControl);
|
||||
return params;
|
||||
}
|
||||
applyContentBlockCacheControl(block, cacheControl);
|
||||
if (fallbackToolResult && messageCacheControlLimit > 1) {
|
||||
applyContentBlockCacheControl(fallbackToolResult, cacheControl);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
@@ -1210,6 +1230,10 @@ function convertMessages(
|
||||
}
|
||||
|
||||
if (typeof message.content === "string") {
|
||||
if (fallbackToolResult && messageCacheControlLimit === 1) {
|
||||
applyContentBlockCacheControl(fallbackToolResult, cacheControl);
|
||||
return params;
|
||||
}
|
||||
message.content = [
|
||||
{
|
||||
type: "text",
|
||||
@@ -1217,16 +1241,15 @@ function convertMessages(
|
||||
cache_control: cacheControl,
|
||||
},
|
||||
] as ContentBlockParam[];
|
||||
if (fallbackToolResult && messageCacheControlLimit > 1) {
|
||||
applyContentBlockCacheControl(fallbackToolResult, cacheControl);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackToolResult) {
|
||||
(
|
||||
fallbackToolResult as typeof fallbackToolResult & {
|
||||
cache_control?: typeof cacheControl;
|
||||
}
|
||||
).cache_control = cacheControl;
|
||||
applyContentBlockCacheControl(fallbackToolResult, cacheControl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1268,6 +1291,14 @@ function buildSystemPromptBlocks(
|
||||
return blocks.length > 0 ? blocks : [{ type: "text", text: "" }];
|
||||
}
|
||||
|
||||
function applyContentBlockCacheControl(
|
||||
block: ContentBlockParam,
|
||||
cacheControl: CacheControlEphemeral,
|
||||
): void {
|
||||
(block as ContentBlockParam & { cache_control?: CacheControlEphemeral }).cache_control =
|
||||
cacheControl;
|
||||
}
|
||||
|
||||
function shouldUseFineGrainedToolStreamingBeta(
|
||||
model: Model<"anthropic-messages">,
|
||||
context: Context,
|
||||
|
||||
Reference in New Issue
Block a user