mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 22:54:33 +00:00
fix(agents): cap openai-completions tool call ids to provider-safe format (#31947)
Co-authored-by: bmendonca3 <bmendonca3@users.noreply.github.com>
This commit is contained in:
@@ -146,7 +146,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
expect(toolResult.toolUseId).toBe("callabcitem123");
|
expect(toolResult.toolUseId).toBe("callabcitem123");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not sanitize tool IDs in images-only mode", async () => {
|
it("sanitizes tool IDs in images-only mode when explicitly enabled", async () => {
|
||||||
const input = [
|
const input = [
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
@@ -169,10 +169,10 @@ describe("sanitizeSessionMessagesImages", () => {
|
|||||||
|
|
||||||
const assistant = out[0] as unknown as { content?: Array<{ type?: string; id?: string }> };
|
const assistant = out[0] as unknown as { content?: Array<{ type?: string; id?: string }> };
|
||||||
const toolCall = assistant.content?.find((b) => b.type === "toolCall");
|
const toolCall = assistant.content?.find((b) => b.type === "toolCall");
|
||||||
expect(toolCall?.id).toBe("call_123|fc_456");
|
expect(toolCall?.id).toBe("call123fc456");
|
||||||
|
|
||||||
const toolResult = out[1] as unknown as { toolCallId?: string };
|
const toolResult = out[1] as unknown as { toolCallId?: string };
|
||||||
expect(toolResult.toolCallId).toBe("call_123|fc_456");
|
expect(toolResult.toolCallId).toBe("call123fc456");
|
||||||
});
|
});
|
||||||
it("filters whitespace-only assistant text blocks", async () => {
|
it("filters whitespace-only assistant text blocks", async () => {
|
||||||
const input = [
|
const input = [
|
||||||
|
|||||||
@@ -54,12 +54,12 @@ export async function sanitizeSessionMessagesImages(
|
|||||||
maxDimensionPx: options?.maxDimensionPx,
|
maxDimensionPx: options?.maxDimensionPx,
|
||||||
maxBytes: options?.maxBytes,
|
maxBytes: options?.maxBytes,
|
||||||
};
|
};
|
||||||
|
const shouldSanitizeToolCallIds = options?.sanitizeToolCallIds === true;
|
||||||
// We sanitize historical session messages because Anthropic can reject a request
|
// We sanitize historical session messages because Anthropic can reject a request
|
||||||
// if the transcript contains oversized base64 images (default max side 1200px).
|
// if the transcript contains oversized base64 images (default max side 1200px).
|
||||||
const sanitizedIds =
|
const sanitizedIds = shouldSanitizeToolCallIds
|
||||||
allowNonImageSanitization && options?.sanitizeToolCallIds
|
? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode)
|
||||||
? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode)
|
: messages;
|
||||||
: messages;
|
|
||||||
const out: AgentMessage[] = [];
|
const out: AgentMessage[] = [];
|
||||||
for (const msg of sanitizedIds) {
|
for (const msg of sanitizedIds) {
|
||||||
if (!msg || typeof msg !== "object") {
|
if (!msg || typeof msg !== "object") {
|
||||||
|
|||||||
@@ -191,6 +191,29 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sanitizes tool call ids for openai-completions", async () => {
|
||||||
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
|
await sanitizeSessionHistory({
|
||||||
|
messages: mockMessages,
|
||||||
|
modelApi: "openai-completions",
|
||||||
|
provider: "openai",
|
||||||
|
modelId: "gpt-5.2",
|
||||||
|
sessionManager: mockSessionManager,
|
||||||
|
sessionId: TEST_SESSION_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(helpers.sanitizeSessionMessagesImages).toHaveBeenCalledWith(
|
||||||
|
mockMessages,
|
||||||
|
"session:history",
|
||||||
|
expect.objectContaining({
|
||||||
|
sanitizeMode: "images-only",
|
||||||
|
sanitizeToolCallIds: true,
|
||||||
|
toolCallIdMode: "strict",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("annotates inter-session user messages before context sanitization", async () => {
|
it("annotates inter-session user messages before context sanitization", async () => {
|
||||||
setNonGoogleModelApi();
|
setNonGoogleModelApi();
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,16 @@ describe("resolveTranscriptPolicy", () => {
|
|||||||
expect(policy.toolCallIdMode).toBeUndefined();
|
expect(policy.toolCallIdMode).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enables strict tool call id sanitization for openai-completions APIs", () => {
|
||||||
|
const policy = resolveTranscriptPolicy({
|
||||||
|
provider: "openai",
|
||||||
|
modelId: "gpt-5.2",
|
||||||
|
modelApi: "openai-completions",
|
||||||
|
});
|
||||||
|
expect(policy.sanitizeToolCallIds).toBe(true);
|
||||||
|
expect(policy.toolCallIdMode).toBe("strict");
|
||||||
|
});
|
||||||
|
|
||||||
it("enables user-turn merge for strict OpenAI-compatible providers", () => {
|
it("enables user-turn merge for strict OpenAI-compatible providers", () => {
|
||||||
const policy = resolveTranscriptPolicy({
|
const policy = resolveTranscriptPolicy({
|
||||||
provider: "moonshot",
|
provider: "moonshot",
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export function resolveTranscriptPolicy(params: {
|
|||||||
(provider === "openrouter" || provider === "opencode" || provider === "kilocode") &&
|
(provider === "openrouter" || provider === "opencode" || provider === "kilocode") &&
|
||||||
modelId.toLowerCase().includes("gemini");
|
modelId.toLowerCase().includes("gemini");
|
||||||
const isCopilotClaude = provider === "github-copilot" && modelId.toLowerCase().includes("claude");
|
const isCopilotClaude = provider === "github-copilot" && modelId.toLowerCase().includes("claude");
|
||||||
|
const requiresOpenAiCompatibleToolIdSanitization = params.modelApi === "openai-completions";
|
||||||
|
|
||||||
// GitHub Copilot's Claude endpoints can reject persisted `thinking` blocks with
|
// GitHub Copilot's Claude endpoints can reject persisted `thinking` blocks with
|
||||||
// non-binary/non-base64 signatures (e.g. thinkingSignature: "reasoning_text").
|
// non-binary/non-base64 signatures (e.g. thinkingSignature: "reasoning_text").
|
||||||
@@ -102,7 +103,8 @@ export function resolveTranscriptPolicy(params: {
|
|||||||
|
|
||||||
const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini;
|
const needsNonImageSanitize = isGoogle || isAnthropic || isMistral || isOpenRouterGemini;
|
||||||
|
|
||||||
const sanitizeToolCallIds = isGoogle || isMistral || isAnthropic;
|
const sanitizeToolCallIds =
|
||||||
|
isGoogle || isMistral || isAnthropic || requiresOpenAiCompatibleToolIdSanitization;
|
||||||
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
|
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
|
||||||
? "strict9"
|
? "strict9"
|
||||||
: sanitizeToolCallIds
|
: sanitizeToolCallIds
|
||||||
@@ -117,7 +119,8 @@ export function resolveTranscriptPolicy(params: {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
|
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
|
||||||
sanitizeToolCallIds: !isOpenAi && sanitizeToolCallIds,
|
sanitizeToolCallIds:
|
||||||
|
(!isOpenAi && sanitizeToolCallIds) || requiresOpenAiCompatibleToolIdSanitization,
|
||||||
toolCallIdMode,
|
toolCallIdMode,
|
||||||
repairToolUseResultPairing,
|
repairToolUseResultPairing,
|
||||||
preserveSignatures: false,
|
preserveSignatures: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user