mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 05:01:15 +00:00
fix(agents): preserve native Anthropic replay tool ids (#61254)
* fix(agents): preserve native Anthropic replay tool ids * docs(changelog): note native Anthropic replay ids * fix(agents): preserve native Anthropic replay ids selectively
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -70,6 +70,7 @@ describe("anthropic-vertex provider plugin", () => {
|
||||
sanitizeMode: "full",
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
preserveNativeAnthropicToolUseIds: true,
|
||||
preserveSignatures: true,
|
||||
repairToolUseResultPairing: true,
|
||||
validateAnthropicTurns: true,
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ describe("anthropic provider replay hooks", () => {
|
||||
sanitizeMode: "full",
|
||||
sanitizeToolCallIds: true,
|
||||
toolCallIdMode: "strict",
|
||||
preserveNativeAnthropicToolUseIds: true,
|
||||
preserveSignatures: true,
|
||||
repairToolUseResultPairing: true,
|
||||
validateAnthropicTurns: true,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<AgentMessage, { role: "assistant" }>).content).toEqual([
|
||||
{ type: "toolUse", id: priorToolId, name: "read", input: { path: "IDENTITY.md" } },
|
||||
]);
|
||||
expect(
|
||||
(sanitizedBase[2] as Extract<AgentMessage, { role: "toolResult" }> & { toolUseId?: string })
|
||||
.toolCallId,
|
||||
).toBe(priorToolId);
|
||||
});
|
||||
|
||||
it("preserves latest assistant thinking blocks for amazon-bedrock replay", async () => {
|
||||
setNonGoogleModelApi();
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<AgentMessage, { role: "assistant" }>).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<AgentMessage, { role: "toolResult" }> & { toolUseId?: string })
|
||||
.toolCallId,
|
||||
).toBe(nativeId);
|
||||
expect(
|
||||
(out[1] as Extract<AgentMessage, { role: "toolResult" }> & { toolUseId?: string })
|
||||
.toolUseId,
|
||||
).toBe(nativeId);
|
||||
expect(
|
||||
(out[2] as Extract<AgentMessage, { role: "toolResult" }> & { toolUseId?: string })
|
||||
.toolCallId,
|
||||
).toBe("call123fc123");
|
||||
expect(
|
||||
(out[2] as Extract<AgentMessage, { role: "toolResult" }> & { toolUseId?: string })
|
||||
.toolUseId,
|
||||
).toBe("call123fc123");
|
||||
});
|
||||
|
||||
it("avoids collisions with alphanumeric-only suffixes", () => {
|
||||
const input = buildDuplicateIdCollisionInput();
|
||||
|
||||
|
||||
@@ -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<string>; 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<string>; 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<string, number>();
|
||||
const orphanToolResultOccurrences = new Map<string, number>();
|
||||
const pendingByRawId = new Map<string, string[]>();
|
||||
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) => {
|
||||
|
||||
@@ -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 }
|
||||
: {}),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
buildAnthropicReplayPolicyForModel,
|
||||
buildGoogleGeminiReplayPolicy,
|
||||
buildHybridAnthropicOrOpenAIReplayPolicy,
|
||||
buildNativeAnthropicReplayPolicyForModel,
|
||||
buildOpenAICompatibleReplayPolicy,
|
||||
buildPassthroughGeminiSanitizingReplayPolicy,
|
||||
buildStrictAnthropicReplayPolicy,
|
||||
@@ -49,6 +50,7 @@ export {
|
||||
buildAnthropicReplayPolicyForModel,
|
||||
buildGoogleGeminiReplayPolicy,
|
||||
buildHybridAnthropicOrOpenAIReplayPolicy,
|
||||
buildNativeAnthropicReplayPolicyForModel,
|
||||
buildOpenAICompatibleReplayPolicy,
|
||||
buildPassthroughGeminiSanitizingReplayPolicy,
|
||||
resolveTaggedReasoningOutputMode,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 } = {},
|
||||
|
||||
@@ -583,6 +583,7 @@ export type ProviderReplayPolicy = {
|
||||
sanitizeMode?: ProviderReplaySanitizeMode;
|
||||
sanitizeToolCallIds?: boolean;
|
||||
toolCallIdMode?: ProviderReplayToolCallIdMode;
|
||||
preserveNativeAnthropicToolUseIds?: boolean;
|
||||
preserveSignatures?: boolean;
|
||||
sanitizeThoughtSignatures?: {
|
||||
allowBase64Only?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user