mirror of
https://github.com/cline/cline.git
synced 2026-10-24 02:02:27 +08:00
fix(llms): put Bedrock cache points on tool results
The Bedrock cachePoint went on the last role: "user" message only, and tool results are role: "tool", so within a turn the cached prefix stopped at the person's message and every tool step paid full input for the turn's tool history. applyBedrockCachePoints now marks the last two user or tool messages: the newest writes at the end of the request, the one before sits where the previous request wrote, so a step with many parallel tool calls stays within Bedrock's ~20-block lookback. This matches the pre-SDK handler, which marked the last two user messages, tool results included. Two marks stay within Claude's four cache checkpoints.
This commit is contained in:
@@ -70,7 +70,7 @@ import {
|
||||
shouldApplyPromptCache,
|
||||
} from "./routing/anthropic-compatible";
|
||||
import {
|
||||
applyBedrockCachePointToLastUserMessage,
|
||||
applyBedrockCachePoints,
|
||||
shouldApplyBedrockCachePoint,
|
||||
} from "./routing/bedrock-cache-point";
|
||||
import {
|
||||
@@ -407,7 +407,7 @@ function buildAiSdkRequestMessages(
|
||||
}) as Array<Record<string, unknown>>;
|
||||
|
||||
if (shouldApplyBedrockCachePoint(request, context)) {
|
||||
applyBedrockCachePointToLastUserMessage(aiMessages);
|
||||
applyBedrockCachePoints(aiMessages);
|
||||
return aiMessages;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
} from "@cline/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyBedrockCachePointToLastUserMessage,
|
||||
applyBedrockCachePoints,
|
||||
BEDROCK_ROUTING_METADATA,
|
||||
resolveBedrockCachePointRoute,
|
||||
shouldApplyBedrockCachePoint,
|
||||
@@ -86,21 +86,47 @@ describe("bedrock cache-point routing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyBedrockCachePointToLastUserMessage", () => {
|
||||
it("attaches a message-level cachePoint marker to the last user message", () => {
|
||||
describe("applyBedrockCachePoints", () => {
|
||||
const cachePoint = { bedrock: { cachePoint: { type: "default" } } };
|
||||
|
||||
it("marks the last two user messages", () => {
|
||||
const messages: Array<Record<string, unknown>> = [
|
||||
{ role: "user", content: [{ type: "text", text: "first" }] },
|
||||
{ role: "assistant", content: [{ type: "text", text: "answer" }] },
|
||||
{ role: "user", content: [{ type: "text", text: "second" }] },
|
||||
{ role: "assistant", content: [{ type: "text", text: "answer" }] },
|
||||
{ role: "user", content: [{ type: "text", text: "third" }] },
|
||||
];
|
||||
|
||||
applyBedrockCachePointToLastUserMessage(messages);
|
||||
applyBedrockCachePoints(messages);
|
||||
|
||||
expect(messages[0]).not.toHaveProperty("providerOptions");
|
||||
expect(messages[1]).not.toHaveProperty("providerOptions");
|
||||
expect(messages[2].providerOptions).toEqual({
|
||||
bedrock: { cachePoint: { type: "default" } },
|
||||
});
|
||||
expect(messages.map((message) => message.providerOptions)).toEqual([
|
||||
undefined,
|
||||
undefined,
|
||||
cachePoint,
|
||||
undefined,
|
||||
cachePoint,
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks tool results, so a turn's steps read the previous step's write", () => {
|
||||
const messages: Array<Record<string, unknown>> = [
|
||||
{ role: "user", content: [{ type: "text", text: "go" }] },
|
||||
{ role: "assistant", content: [{ type: "tool-call", toolCallId: "1" }] },
|
||||
{ role: "tool", content: [{ type: "tool-result", toolCallId: "1" }] },
|
||||
{ role: "assistant", content: [{ type: "tool-call", toolCallId: "2" }] },
|
||||
{ role: "tool", content: [{ type: "tool-result", toolCallId: "2" }] },
|
||||
];
|
||||
|
||||
applyBedrockCachePoints(messages);
|
||||
|
||||
expect(messages.map((message) => message.providerOptions)).toEqual([
|
||||
undefined,
|
||||
undefined,
|
||||
cachePoint,
|
||||
undefined,
|
||||
cachePoint,
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves existing message provider options", () => {
|
||||
@@ -112,7 +138,7 @@ describe("applyBedrockCachePointToLastUserMessage", () => {
|
||||
},
|
||||
];
|
||||
|
||||
applyBedrockCachePointToLastUserMessage(messages);
|
||||
applyBedrockCachePoints(messages);
|
||||
|
||||
expect(messages[0].providerOptions).toEqual({
|
||||
other: { keep: true },
|
||||
@@ -125,7 +151,7 @@ describe("applyBedrockCachePointToLastUserMessage", () => {
|
||||
{ role: "assistant", content: [{ type: "text", text: "answer" }] },
|
||||
];
|
||||
|
||||
applyBedrockCachePointToLastUserMessage(messages);
|
||||
applyBedrockCachePoints(messages);
|
||||
|
||||
expect(messages[0]).not.toHaveProperty("providerOptions");
|
||||
});
|
||||
|
||||
@@ -58,23 +58,30 @@ export function createBedrockCachePointProviderOptions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a message-level cache-point marker to the last user message. The
|
||||
* Bedrock message converter appends the `cachePoint` block after that
|
||||
* message's content, caching the whole prefix (tools, system, and history)
|
||||
* up to it — mirroring the Anthropic writer's checkpoint placement.
|
||||
* Attach a message-level cache-point marker to the last two user or tool
|
||||
* messages. The Bedrock message converter appends each `cachePoint` block
|
||||
* after that message's content, caching the prefix (tools, system, and
|
||||
* history) up to it.
|
||||
*
|
||||
* Tool results are `role: "tool"`, so marking only user messages would leave
|
||||
* every step of a turn cached no further than the person's message. The
|
||||
* newest mark writes at the end of the request; the one before it sits where
|
||||
* the previous request wrote, so the read holds even when a step with many
|
||||
* parallel tool calls adds more than Bedrock's ~20-block lookback.
|
||||
*/
|
||||
export function applyBedrockCachePointToLastUserMessage(
|
||||
export function applyBedrockCachePoints(
|
||||
messages: Array<Record<string, unknown>>,
|
||||
): void {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
let marked = 0;
|
||||
for (let i = messages.length - 1; i >= 0 && marked < 2; i--) {
|
||||
const message = messages[i];
|
||||
if (message?.role !== "user") {
|
||||
if (message?.role !== "user" && message?.role !== "tool") {
|
||||
continue;
|
||||
}
|
||||
message.providerOptions = {
|
||||
...(message.providerOptions as Record<string, unknown> | undefined),
|
||||
...createBedrockCachePointProviderOptions(),
|
||||
};
|
||||
return;
|
||||
marked += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,11 @@
|
||||
// 3. the families the adapter does classify keep their native shapes from
|
||||
// the id alone even when the catalog does not list the model, so a
|
||||
// catalog that lags a model launch never switches thinking off.
|
||||
import type { GatewayStreamRequest, ModelReasoningOption } from "@cline/shared";
|
||||
import type {
|
||||
AgentMessage,
|
||||
GatewayStreamRequest,
|
||||
ModelReasoningOption,
|
||||
} from "@cline/shared";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createGateway } from "../gateway";
|
||||
|
||||
@@ -122,6 +126,91 @@ async function wireRequest(
|
||||
return { path, sent };
|
||||
}
|
||||
|
||||
describe("Bedrock prompt-cache wire contract", () => {
|
||||
it("puts a cachePoint after the last two tool results, not only after the person's message", async () => {
|
||||
let body: { messages?: Array<{ content: Record<string, unknown>[] }> } = {};
|
||||
const fetchStub = (async (_input, init) => {
|
||||
body = JSON.parse((init?.body as string) ?? "{}");
|
||||
return new Response(JSON.stringify({ message: "stub" }), {
|
||||
status: 400,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
const gateway = createGateway({
|
||||
providerConfigs: [
|
||||
{
|
||||
providerId: "bedrock",
|
||||
apiKey: "test-bearer-key",
|
||||
fetch: fetchStub,
|
||||
options: { region: "us-east-1", authentication: "apikey" },
|
||||
},
|
||||
],
|
||||
});
|
||||
const step = (id: string): AgentMessage[] => [
|
||||
{
|
||||
id: `a${id}`,
|
||||
role: "assistant",
|
||||
createdAt: 1,
|
||||
content: [
|
||||
{
|
||||
type: "tool-call",
|
||||
toolCallId: id,
|
||||
toolName: "read_files",
|
||||
input: { paths: [`${id}.ts`] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: `r${id}`,
|
||||
role: "tool",
|
||||
createdAt: 2,
|
||||
content: [
|
||||
{
|
||||
type: "tool-result",
|
||||
toolCallId: id,
|
||||
toolName: "read_files",
|
||||
output: "contents",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
for await (const _event of await gateway.stream({
|
||||
providerId: "bedrock",
|
||||
modelId: "us.anthropic.claude-sonnet-4-6",
|
||||
maxTokens: 16,
|
||||
tools: [
|
||||
{
|
||||
name: "read_files",
|
||||
description: "Read files",
|
||||
inputSchema: { type: "object" },
|
||||
},
|
||||
],
|
||||
messages: [
|
||||
{
|
||||
id: "u",
|
||||
role: "user",
|
||||
createdAt: 0,
|
||||
content: [{ type: "text", text: "Read the files" }],
|
||||
},
|
||||
...step("t1"),
|
||||
...step("t2"),
|
||||
],
|
||||
})) {
|
||||
// drain
|
||||
}
|
||||
const blocks = (body.messages ?? []).map((message) =>
|
||||
message.content.map((block) => Object.keys(block)[0]),
|
||||
);
|
||||
expect(blocks).toEqual([
|
||||
["text"],
|
||||
["toolUse"],
|
||||
["toolResult", "cachePoint"],
|
||||
["toolUse"],
|
||||
["toolResult", "cachePoint"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Bedrock reasoning wire contract", () => {
|
||||
it.each([
|
||||
"us.openai.gpt-6-astra",
|
||||
|
||||
Reference in New Issue
Block a user