diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b7bb959e22..24d3878d779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai - MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) - Agents/Claude CLI: persist explicit `openclaw agent --session-id` runs under a stable session key so follow-ups can reuse the stored CLI binding and resume the same underlying Claude session. - Agents/CLI backends: invalidate stored CLI session reuse when local CLI login state or the selected auth profile credential changes, so relogin and token rotation stop resuming stale sessions. +- Agents/Anthropic: preserve native `toolu_*` replay ids on direct Anthropic and Anthropic Vertex paths so cache-sensitive history stops rewriting known-valid Anthropic tool-use ids. (#52612) - Gateway/macOS: recover installed-but-unloaded LaunchAgents during `openclaw gateway start` and `restart`, while still preferring live unmanaged gateways during restart recovery. (#43766) Thanks @HenryC-3. - Auth/failover: persist selected fallback overrides before retrying, shorten `auth_permanent` lockouts, and refresh websocket/shared-auth sessions only when real auth changes occur so retries and secret rotations behave predictably. (#60404, #60323, #60387) - Cron: replay interrupted recurring jobs on the first gateway restart instead of waiting for a second restart. (#60583) Thanks @joelnishanth. diff --git a/extensions/anthropic-vertex/index.test.ts b/extensions/anthropic-vertex/index.test.ts index af2fde917d6..998d2d71d91 100644 --- a/extensions/anthropic-vertex/index.test.ts +++ b/extensions/anthropic-vertex/index.test.ts @@ -70,6 +70,7 @@ describe("anthropic-vertex provider plugin", () => { sanitizeMode: "full", sanitizeToolCallIds: true, toolCallIdMode: "strict", + preserveNativeAnthropicToolUseIds: true, preserveSignatures: true, repairToolUseResultPairing: true, validateAnthropicTurns: true, diff --git a/extensions/anthropic-vertex/index.ts b/extensions/anthropic-vertex/index.ts index e7263c81028..427813db7b8 100644 --- a/extensions/anthropic-vertex/index.ts +++ b/extensions/anthropic-vertex/index.ts @@ -1,5 +1,5 @@ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildNativeAnthropicReplayPolicyForModel } from "openclaw/plugin-sdk/provider-model-shared"; import { mergeImplicitAnthropicVertexProvider, resolveAnthropicVertexConfigApiKey, @@ -7,9 +7,6 @@ import { } from "./api.js"; const PROVIDER_ID = "anthropic-vertex"; -const ANTHROPIC_BY_MODEL_REPLAY_HOOKS = buildProviderReplayFamilyHooks({ - family: "anthropic-by-model", -}); export default definePluginEntry({ id: PROVIDER_ID, @@ -39,7 +36,7 @@ export default definePluginEntry({ }, }, resolveConfigApiKey: ({ env }) => resolveAnthropicVertexConfigApiKey(env), - ...ANTHROPIC_BY_MODEL_REPLAY_HOOKS, + buildReplayPolicy: ({ modelId }) => buildNativeAnthropicReplayPolicyForModel(modelId), }); }, }); diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index 2383a047253..a6b325e4e12 100644 --- a/extensions/anthropic/index.test.ts +++ b/extensions/anthropic/index.test.ts @@ -28,6 +28,7 @@ describe("anthropic provider replay hooks", () => { sanitizeMode: "full", sanitizeToolCallIds: true, toolCallIdMode: "strict", + preserveNativeAnthropicToolUseIds: true, preserveSignatures: true, repairToolUseResultPairing: true, validateAnthropicTurns: true, diff --git a/extensions/anthropic/replay-policy.ts b/extensions/anthropic/replay-policy.ts index 5acb0c973b8..57b485d8d4d 100644 --- a/extensions/anthropic/replay-policy.ts +++ b/extensions/anthropic/replay-policy.ts @@ -2,11 +2,11 @@ import type { ProviderReplayPolicy, ProviderReplayPolicyContext, } from "openclaw/plugin-sdk/plugin-entry"; -import { buildAnthropicReplayPolicyForModel } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildNativeAnthropicReplayPolicyForModel } from "openclaw/plugin-sdk/provider-model-shared"; /** * Returns the provider-owned replay policy for Anthropic transports. */ export function buildAnthropicReplayPolicy(ctx: ProviderReplayPolicyContext): ProviderReplayPolicy { - return buildAnthropicReplayPolicyForModel(ctx.modelId); + return buildNativeAnthropicReplayPolicyForModel(ctx.modelId); } diff --git a/src/agents/pi-embedded-helpers/images.ts b/src/agents/pi-embedded-helpers/images.ts index d396f22cf6a..d663de83aab 100644 --- a/src/agents/pi-embedded-helpers/images.ts +++ b/src/agents/pi-embedded-helpers/images.ts @@ -43,6 +43,7 @@ export async function sanitizeSessionMessagesImages( options?: { sanitizeMode?: "full" | "images-only"; sanitizeToolCallIds?: boolean; + preserveNativeAnthropicToolUseIds?: boolean; /** * Mode for tool call ID sanitization: * - "strict" (alphanumeric only) @@ -66,7 +67,9 @@ export async function sanitizeSessionMessagesImages( // We sanitize historical session messages because Anthropic can reject a request // if the transcript contains oversized base64 images (default max side 1200px). const sanitizedIds = shouldSanitizeToolCallIds - ? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode) + ? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode, { + preserveNativeAnthropicToolUseIds: options?.preserveNativeAnthropicToolUseIds, + }) : messages; const out: AgentMessage[] = []; for (const msg of sanitizedIds) { diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts index 710028e5d8e..8962874e9b5 100644 --- a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts +++ b/src/agents/pi-embedded-runner.sanitize-session-history.test.ts @@ -16,6 +16,7 @@ import { TEST_SESSION_ID, } from "./pi-embedded-runner.sanitize-session-history.test-harness.js"; import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js"; +import type { TranscriptPolicy } from "./transcript-policy.js"; import { makeZeroUsageSnapshot } from "./usage.js"; vi.mock("./pi-embedded-helpers.js", async () => ({ @@ -121,6 +122,7 @@ describe("sanitizeSessionHistory", () => { provider?: string; modelApi?: string; modelId?: string; + policy?: TranscriptPolicy; }) => sanitizeSessionHistory({ messages: params.messages, @@ -129,6 +131,7 @@ describe("sanitizeSessionHistory", () => { modelId: params.modelId ?? "claude-opus-4-6", sessionManager: makeMockSessionManager(), sessionId: TEST_SESSION_ID, + policy: params.policy, }); const getAssistantMessage = (messages: AgentMessage[]) => { @@ -930,6 +933,79 @@ describe("sanitizeSessionHistory", () => { ]); }); + it("keeps the earlier anthropic replay prefix stable after a later subagent turn", async () => { + setNonGoogleModelApi(); + + const priorToolId = "toolu_01ABCDEF1234567890"; + const laterToolId = "toolu_01ZZZZZZ9999999999"; + const nativeAnthropicPolicy: TranscriptPolicy = { + sanitizeMode: "full", + sanitizeToolCallIds: true, + toolCallIdMode: "strict", + preserveNativeAnthropicToolUseIds: true, + repairToolUseResultPairing: true, + preserveSignatures: true, + sanitizeThoughtSignatures: undefined, + sanitizeThinkingSignatures: false, + dropThinkingBlocks: true, + applyGoogleTurnOrdering: false, + validateGeminiTurns: false, + validateAnthropicTurns: true, + allowSyntheticToolResults: true, + }; + const baseMessages = castAgentMessages([ + makeUserMessage("Read IDENTITY.md"), + makeAssistantMessage( + [{ type: "toolUse", id: priorToolId, name: "read", input: { path: "IDENTITY.md" } }], + { stopReason: "toolUse" }, + ), + { + role: "toolResult", + toolCallId: priorToolId, + toolUseId: priorToolId, + toolName: "read", + content: [{ type: "text", text: "ok" }], + isError: false, + }, + makeAssistantMessage([{ type: "text", text: "done" }]), + ]); + const withSubagentMessages = castAgentMessages([ + ...baseMessages, + makeUserMessage("Ask a subagent for an emoji"), + makeAssistantMessage( + [{ type: "toolUse", id: laterToolId, name: "subagent", input: { prompt: "emoji" } }], + { stopReason: "toolUse" }, + ), + { + role: "toolResult", + toolCallId: laterToolId, + toolUseId: laterToolId, + toolName: "subagent", + content: [{ type: "text", text: "😀" }], + isError: false, + }, + makeAssistantMessage([{ type: "text", text: "it was 😀" }]), + ]); + + const sanitizedBase = await sanitizeAnthropicHistory({ + messages: baseMessages, + policy: nativeAnthropicPolicy, + }); + const sanitizedWithSubagent = await sanitizeAnthropicHistory({ + messages: withSubagentMessages, + policy: nativeAnthropicPolicy, + }); + + expect(sanitizedWithSubagent.slice(0, sanitizedBase.length)).toEqual(sanitizedBase); + expect((sanitizedBase[1] as Extract).content).toEqual([ + { type: "toolUse", id: priorToolId, name: "read", input: { path: "IDENTITY.md" } }, + ]); + expect( + (sanitizedBase[2] as Extract & { toolUseId?: string }) + .toolCallId, + ).toBe(priorToolId); + }); + it("preserves latest assistant thinking blocks for amazon-bedrock replay", async () => { setNonGoogleModelApi(); diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index 39ff44b3058..fd36c885a78 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -657,6 +657,7 @@ export async function sanitizeSessionHistory(params: { sanitizeMode: policy.sanitizeMode, sanitizeToolCallIds: policy.sanitizeToolCallIds, toolCallIdMode: policy.toolCallIdMode, + preserveNativeAnthropicToolUseIds: policy.preserveNativeAnthropicToolUseIds, preserveSignatures: policy.preserveSignatures, sanitizeThoughtSignatures: policy.sanitizeThoughtSignatures, ...resolveImageSanitizationLimits(params.config), diff --git a/src/agents/pi-embedded-runner/replay-history.ts b/src/agents/pi-embedded-runner/replay-history.ts index 0ddae78868f..9e69e209b66 100644 --- a/src/agents/pi-embedded-runner/replay-history.ts +++ b/src/agents/pi-embedded-runner/replay-history.ts @@ -470,6 +470,7 @@ export async function sanitizeSessionHistory(params: { sanitizeMode: policy.sanitizeMode, sanitizeToolCallIds: policy.sanitizeToolCallIds, toolCallIdMode: policy.toolCallIdMode, + preserveNativeAnthropicToolUseIds: policy.preserveNativeAnthropicToolUseIds, preserveSignatures: policy.preserveSignatures, sanitizeThoughtSignatures: policy.sanitizeThoughtSignatures, ...resolveImageSanitizationLimits(params.config), diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index ff2a2df214d..9839069988e 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -1039,7 +1039,13 @@ export async function runEmbeddedAttempt( if (!Array.isArray(messages)) { return inner(model, context, options); } - const sanitized = sanitizeToolCallIdsForCloudCodeAssist(messages as AgentMessage[], mode); + const sanitized = sanitizeToolCallIdsForCloudCodeAssist( + messages as AgentMessage[], + mode, + { + preserveNativeAnthropicToolUseIds: transcriptPolicy.preserveNativeAnthropicToolUseIds, + }, + ); if (sanitized === messages) { return inner(model, context, options); } diff --git a/src/agents/tool-call-id.test.ts b/src/agents/tool-call-id.test.ts index ced9c7ee8a5..80a5cba5889 100644 --- a/src/agents/tool-call-id.test.ts +++ b/src/agents/tool-call-id.test.ts @@ -239,6 +239,60 @@ describe("sanitizeToolCallIdsForCloudCodeAssist", () => { expectSingleToolCallRewrite(out, "whatsapplogin17687998415271", "strict"); }); + it("preserves native anthropic ids while sanitizing mixed-provider ids when requested", () => { + const nativeId = "toolu_01ABCDEF1234567890"; + const nonNativeId = "call_123|fc_123"; + const input = castAgentMessages([ + { + role: "assistant", + content: [ + { type: "toolUse", id: nativeId, name: "read", input: { path: "IDENTITY.md" } }, + { type: "toolUse", id: nonNativeId, name: "read", input: { path: "README.md" } }, + ], + }, + { + role: "toolResult", + toolCallId: nativeId, + toolUseId: nativeId, + toolName: "read", + content: [{ type: "text", text: "identity" }], + }, + { + role: "toolResult", + toolCallId: nonNativeId, + toolUseId: nonNativeId, + toolName: "read", + content: [{ type: "text", text: "readme" }], + }, + ]); + + const out = sanitizeToolCallIdsForCloudCodeAssist(input, "strict", { + preserveNativeAnthropicToolUseIds: true, + }); + + expect(out).not.toBe(input); + expect((out[0] as Extract).content).toEqual([ + { type: "toolUse", id: nativeId, name: "read", input: { path: "IDENTITY.md" } }, + { type: "toolUse", id: "call123fc123", name: "read", input: { path: "README.md" } }, + ]); + expect( + (out[1] as Extract & { toolUseId?: string }) + .toolCallId, + ).toBe(nativeId); + expect( + (out[1] as Extract & { toolUseId?: string }) + .toolUseId, + ).toBe(nativeId); + expect( + (out[2] as Extract & { toolUseId?: string }) + .toolCallId, + ).toBe("call123fc123"); + expect( + (out[2] as Extract & { toolUseId?: string }) + .toolUseId, + ).toBe("call123fc123"); + }); + it("avoids collisions with alphanumeric-only suffixes", () => { const input = buildDuplicateIdCollisionInput(); diff --git a/src/agents/tool-call-id.ts b/src/agents/tool-call-id.ts index c7c68994458..8cfabc622db 100644 --- a/src/agents/tool-call-id.ts +++ b/src/agents/tool-call-id.ts @@ -2,6 +2,7 @@ import { createHash } from "node:crypto"; import type { AgentMessage } from "@mariozechner/pi-agent-core"; export type ToolCallIdMode = "strict" | "strict9"; +const NATIVE_ANTHROPIC_TOOL_USE_ID_RE = /^toolu_[A-Za-z0-9_]+$/; const STRICT9_LEN = 9; const TOOL_CALL_TYPES = new Set(["toolCall", "toolUse", "functionCall"]); @@ -97,6 +98,10 @@ function shortHash(text: string, length = 8): string { return createHash("sha256").update(text).digest("hex").slice(0, length); } +function isNativeAnthropicToolUseId(id: string): boolean { + return NATIVE_ANTHROPIC_TOOL_USE_ID_RE.test(id); +} + function makeUniqueToolId(params: { id: string; used: Set; mode: ToolCallIdMode }): string { if (params.mode === "strict9") { const base = sanitizeToolCallId(params.id, params.mode); @@ -144,7 +149,10 @@ function makeUniqueToolId(params: { id: string; used: Set; mode: ToolCal return `${candidate.slice(0, MAX_LEN - ts.length)}${ts}`; } -function createOccurrenceAwareResolver(mode: ToolCallIdMode): { +function createOccurrenceAwareResolver( + mode: ToolCallIdMode, + options?: { preserveNativeAnthropicToolUseIds?: boolean }, +): { resolveAssistantId: (id: string) => string; resolveToolResultId: (id: string) => string; } { @@ -152,6 +160,7 @@ function createOccurrenceAwareResolver(mode: ToolCallIdMode): { const assistantOccurrences = new Map(); const orphanToolResultOccurrences = new Map(); const pendingByRawId = new Map(); + const preserveNativeAnthropicToolUseIds = options?.preserveNativeAnthropicToolUseIds === true; const allocate = (seed: string): string => { const next = makeUniqueToolId({ id: seed, used, mode }); @@ -159,10 +168,23 @@ function createOccurrenceAwareResolver(mode: ToolCallIdMode): { return next; }; + const allocatePreservingNativeAnthropicId = (id: string, occurrence: number): string => { + if ( + preserveNativeAnthropicToolUseIds && + isNativeAnthropicToolUseId(id) && + occurrence === 1 && + !used.has(id) + ) { + used.add(id); + return id; + } + return allocate(occurrence === 1 ? id : `${id}:${occurrence}`); + }; + const resolveAssistantId = (id: string): string => { const occurrence = (assistantOccurrences.get(id) ?? 0) + 1; assistantOccurrences.set(id, occurrence); - const next = allocate(occurrence === 1 ? id : `${id}:${occurrence}`); + const next = allocatePreservingNativeAnthropicId(id, occurrence); const pending = pendingByRawId.get(id); if (pending) { pending.push(next); @@ -184,6 +206,15 @@ function createOccurrenceAwareResolver(mode: ToolCallIdMode): { const occurrence = (orphanToolResultOccurrences.get(id) ?? 0) + 1; orphanToolResultOccurrences.set(id, occurrence); + if ( + preserveNativeAnthropicToolUseIds && + isNativeAnthropicToolUseId(id) && + occurrence === 1 && + !used.has(id) + ) { + used.add(id); + return id; + } return allocate(`${id}:tool_result:${occurrence}`); }; @@ -267,6 +298,7 @@ function rewriteToolResultIds(params: { export function sanitizeToolCallIdsForCloudCodeAssist( messages: AgentMessage[], mode: ToolCallIdMode = "strict", + options?: { preserveNativeAnthropicToolUseIds?: boolean }, ): AgentMessage[] { // Strict mode: only [a-zA-Z0-9] // Strict9 mode: only [a-zA-Z0-9], length 9 (Mistral tool call requirement) @@ -274,7 +306,7 @@ export function sanitizeToolCallIdsForCloudCodeAssist( // duplicate tool-call IDs. Track assistant occurrences in-order so repeated // raw IDs receive distinct rewritten IDs, while matching tool results consume // the same rewritten IDs in encounter order. - const { resolveAssistantId, resolveToolResultId } = createOccurrenceAwareResolver(mode); + const { resolveAssistantId, resolveToolResultId } = createOccurrenceAwareResolver(mode, options); let changed = false; const out = messages.map((msg) => { diff --git a/src/agents/transcript-policy.ts b/src/agents/transcript-policy.ts index 50257e8f706..817141fcdbe 100644 --- a/src/agents/transcript-policy.ts +++ b/src/agents/transcript-policy.ts @@ -11,6 +11,7 @@ export type TranscriptPolicy = { sanitizeMode: TranscriptSanitizeMode; sanitizeToolCallIds: boolean; toolCallIdMode?: ToolCallIdMode; + preserveNativeAnthropicToolUseIds: boolean; repairToolUseResultPairing: boolean; preserveSignatures: boolean; sanitizeThoughtSignatures?: { @@ -29,6 +30,7 @@ const DEFAULT_TRANSCRIPT_POLICY: TranscriptPolicy = { sanitizeMode: "images-only", sanitizeToolCallIds: false, toolCallIdMode: undefined, + preserveNativeAnthropicToolUseIds: false, repairToolUseResultPairing: true, preserveSignatures: false, sanitizeThoughtSignatures: undefined, @@ -114,6 +116,9 @@ function mergeTranscriptPolicy( ? { sanitizeToolCallIds: policy.sanitizeToolCallIds } : {}), ...(policy.toolCallIdMode ? { toolCallIdMode: policy.toolCallIdMode as ToolCallIdMode } : {}), + ...(typeof policy.preserveNativeAnthropicToolUseIds === "boolean" + ? { preserveNativeAnthropicToolUseIds: policy.preserveNativeAnthropicToolUseIds } + : {}), ...(typeof policy.repairToolUseResultPairing === "boolean" ? { repairToolUseResultPairing: policy.repairToolUseResultPairing } : {}), diff --git a/src/plugin-sdk/provider-model-shared.ts b/src/plugin-sdk/provider-model-shared.ts index 6831b4cb206..980184c42c8 100644 --- a/src/plugin-sdk/provider-model-shared.ts +++ b/src/plugin-sdk/provider-model-shared.ts @@ -8,6 +8,7 @@ import { buildAnthropicReplayPolicyForModel, buildGoogleGeminiReplayPolicy, buildHybridAnthropicOrOpenAIReplayPolicy, + buildNativeAnthropicReplayPolicyForModel, buildOpenAICompatibleReplayPolicy, buildPassthroughGeminiSanitizingReplayPolicy, buildStrictAnthropicReplayPolicy, @@ -49,6 +50,7 @@ export { buildAnthropicReplayPolicyForModel, buildGoogleGeminiReplayPolicy, buildHybridAnthropicOrOpenAIReplayPolicy, + buildNativeAnthropicReplayPolicyForModel, buildOpenAICompatibleReplayPolicy, buildPassthroughGeminiSanitizingReplayPolicy, resolveTaggedReasoningOutputMode, diff --git a/src/plugins/provider-replay-helpers.test.ts b/src/plugins/provider-replay-helpers.test.ts index e0bef311f8e..df006394c9a 100644 --- a/src/plugins/provider-replay-helpers.test.ts +++ b/src/plugins/provider-replay-helpers.test.ts @@ -3,6 +3,7 @@ import { buildAnthropicReplayPolicyForModel, buildGoogleGeminiReplayPolicy, buildHybridAnthropicOrOpenAIReplayPolicy, + buildNativeAnthropicReplayPolicyForModel, buildOpenAICompatibleReplayPolicy, buildPassthroughGeminiSanitizingReplayPolicy, resolveTaggedReasoningOutputMode, @@ -33,6 +34,8 @@ describe("provider replay helpers", () => { it("derives claude-only anthropic replay policy from the model id", () => { expect(buildAnthropicReplayPolicyForModel("claude-sonnet-4-6")).toMatchObject({ + sanitizeToolCallIds: true, + toolCallIdMode: "strict", dropThinkingBlocks: true, validateAnthropicTurns: true, }); @@ -41,6 +44,20 @@ describe("provider replay helpers", () => { ); }); + it("builds native Anthropic replay policy with selective tool-call id preservation", () => { + expect(buildNativeAnthropicReplayPolicyForModel("claude-sonnet-4-6")).toMatchObject({ + sanitizeMode: "full", + sanitizeToolCallIds: true, + toolCallIdMode: "strict", + preserveNativeAnthropicToolUseIds: true, + preserveSignatures: true, + repairToolUseResultPairing: true, + validateAnthropicTurns: true, + allowSyntheticToolResults: true, + dropThinkingBlocks: true, + }); + }); + it("builds hybrid anthropic or openai replay policy", () => { expect( buildHybridAnthropicOrOpenAIReplayPolicy( diff --git a/src/plugins/provider-replay-helpers.ts b/src/plugins/provider-replay-helpers.ts index c0d61008162..3e12c4b7855 100644 --- a/src/plugins/provider-replay-helpers.ts +++ b/src/plugins/provider-replay-helpers.ts @@ -37,12 +37,24 @@ export function buildOpenAICompatibleReplayPolicy( } export function buildStrictAnthropicReplayPolicy( - options: { dropThinkingBlocks?: boolean } = {}, + options: { + dropThinkingBlocks?: boolean; + sanitizeToolCallIds?: boolean; + preserveNativeAnthropicToolUseIds?: boolean; + } = {}, ): ProviderReplayPolicy { + const sanitizeToolCallIds = options.sanitizeToolCallIds ?? true; return { sanitizeMode: "full", - sanitizeToolCallIds: true, - toolCallIdMode: "strict", + ...(sanitizeToolCallIds + ? { + sanitizeToolCallIds: true, + toolCallIdMode: "strict" as const, + ...(options.preserveNativeAnthropicToolUseIds + ? { preserveNativeAnthropicToolUseIds: true } + : {}), + } + : {}), preserveSignatures: true, repairToolUseResultPairing: true, validateAnthropicTurns: true, @@ -57,6 +69,14 @@ export function buildAnthropicReplayPolicyForModel(modelId?: string): ProviderRe }); } +export function buildNativeAnthropicReplayPolicyForModel(modelId?: string): ProviderReplayPolicy { + return buildStrictAnthropicReplayPolicy({ + dropThinkingBlocks: (modelId?.toLowerCase() ?? "").includes("claude"), + sanitizeToolCallIds: true, + preserveNativeAnthropicToolUseIds: true, + }); +} + export function buildHybridAnthropicOrOpenAIReplayPolicy( ctx: ProviderReplayPolicyContext, options: { anthropicModelDropThinkingBlocks?: boolean } = {}, diff --git a/src/plugins/types.ts b/src/plugins/types.ts index a7cc20ee5a9..d66cc002b56 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -583,6 +583,7 @@ export type ProviderReplayPolicy = { sanitizeMode?: ProviderReplaySanitizeMode; sanitizeToolCallIds?: boolean; toolCallIdMode?: ProviderReplayToolCallIdMode; + preserveNativeAnthropicToolUseIds?: boolean; preserveSignatures?: boolean; sanitizeThoughtSignatures?: { allowBase64Only?: boolean;