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");
|
||||
});
|
||||
|
||||
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 = [
|
||||
{
|
||||
role: "assistant",
|
||||
@@ -169,10 +169,10 @@ describe("sanitizeSessionMessagesImages", () => {
|
||||
|
||||
const assistant = out[0] as unknown as { content?: Array<{ type?: string; id?: string }> };
|
||||
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 };
|
||||
expect(toolResult.toolCallId).toBe("call_123|fc_456");
|
||||
expect(toolResult.toolCallId).toBe("call123fc456");
|
||||
});
|
||||
it("filters whitespace-only assistant text blocks", async () => {
|
||||
const input = [
|
||||
|
||||
@@ -54,12 +54,12 @@ export async function sanitizeSessionMessagesImages(
|
||||
maxDimensionPx: options?.maxDimensionPx,
|
||||
maxBytes: options?.maxBytes,
|
||||
};
|
||||
const shouldSanitizeToolCallIds = options?.sanitizeToolCallIds === true;
|
||||
// We sanitize historical session messages because Anthropic can reject a request
|
||||
// if the transcript contains oversized base64 images (default max side 1200px).
|
||||
const sanitizedIds =
|
||||
allowNonImageSanitization && options?.sanitizeToolCallIds
|
||||
? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode)
|
||||
: messages;
|
||||
const sanitizedIds = shouldSanitizeToolCallIds
|
||||
? sanitizeToolCallIdsForCloudCodeAssist(messages, options.toolCallIdMode)
|
||||
: messages;
|
||||
const out: AgentMessage[] = [];
|
||||
for (const msg of sanitizedIds) {
|
||||
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 () => {
|
||||
setNonGoogleModelApi();
|
||||
|
||||
|
||||
@@ -44,6 +44,16 @@ describe("resolveTranscriptPolicy", () => {
|
||||
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", () => {
|
||||
const policy = resolveTranscriptPolicy({
|
||||
provider: "moonshot",
|
||||
|
||||
@@ -94,6 +94,7 @@ export function resolveTranscriptPolicy(params: {
|
||||
(provider === "openrouter" || provider === "opencode" || provider === "kilocode") &&
|
||||
modelId.toLowerCase().includes("gemini");
|
||||
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
|
||||
// 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 sanitizeToolCallIds = isGoogle || isMistral || isAnthropic;
|
||||
const sanitizeToolCallIds =
|
||||
isGoogle || isMistral || isAnthropic || requiresOpenAiCompatibleToolIdSanitization;
|
||||
const toolCallIdMode: ToolCallIdMode | undefined = isMistral
|
||||
? "strict9"
|
||||
: sanitizeToolCallIds
|
||||
@@ -117,7 +119,8 @@ export function resolveTranscriptPolicy(params: {
|
||||
|
||||
return {
|
||||
sanitizeMode: isOpenAi ? "images-only" : needsNonImageSanitize ? "full" : "images-only",
|
||||
sanitizeToolCallIds: !isOpenAi && sanitizeToolCallIds,
|
||||
sanitizeToolCallIds:
|
||||
(!isOpenAi && sanitizeToolCallIds) || requiresOpenAiCompatibleToolIdSanitization,
|
||||
toolCallIdMode,
|
||||
repairToolUseResultPairing,
|
||||
preserveSignatures: false,
|
||||
|
||||
Reference in New Issue
Block a user