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:
bmendonca3
2026-03-02 11:08:20 -07:00
committed by GitHub
parent 83c8406f01
commit a6489ab5e9
5 changed files with 45 additions and 9 deletions

View File

@@ -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 = [

View File

@@ -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") {

View File

@@ -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();

View File

@@ -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",

View File

@@ -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,