( messages: OpenAI.Chat.ChatCompletionMessageParam[], modelId: string, )
| 162 | * @see https://github.com/cline/cline/issues/8214 |
| 163 | */ |
| 164 | export function sanitizeGeminiMessages( |
| 165 | messages: OpenAI.Chat.ChatCompletionMessageParam[], |
| 166 | modelId: string, |
| 167 | ): OpenAI.Chat.ChatCompletionMessageParam[] { |
| 168 | // Only sanitize for Gemini models |
| 169 | if (!modelId.includes("gemini")) { |
| 170 | return messages |
| 171 | } |
| 172 | |
| 173 | const droppedToolCallIds = new Set<string>() |
| 174 | const sanitized: OpenAI.Chat.ChatCompletionMessageParam[] = [] |
| 175 | |
| 176 | for (const msg of messages) { |
| 177 | if (msg.role === "assistant") { |
| 178 | const anyMsg = msg as any |
| 179 | const toolCalls = anyMsg.tool_calls as OpenAI.Chat.ChatCompletionMessageToolCall[] | undefined |
| 180 | const reasoningDetails = anyMsg.reasoning_details as ReasoningDetail[] | undefined |
| 181 | |
| 182 | if (Array.isArray(toolCalls) && toolCalls.length > 0) { |
| 183 | const hasReasoningDetails = Array.isArray(reasoningDetails) && reasoningDetails.length > 0 |
| 184 | |
| 185 | if (!hasReasoningDetails) { |
| 186 | // No reasoning_details at all - drop all tool calls |
| 187 | for (const tc of toolCalls) { |
| 188 | if (tc?.id) { |
| 189 | droppedToolCallIds.add(tc.id) |
| 190 | } |
| 191 | } |
| 192 | // Keep any textual content, but drop the tool_calls themselves |
| 193 | if (anyMsg.content) { |
| 194 | sanitized.push({ role: "assistant", content: anyMsg.content } as any) |
| 195 | } |
| 196 | continue |
| 197 | } |
| 198 | |
| 199 | // Filter reasoning_details to only include entries matching tool call IDs |
| 200 | // This prevents mismatched reasoning details from poisoning the request |
| 201 | const validToolCalls: OpenAI.Chat.ChatCompletionMessageToolCall[] = [] |
| 202 | const validReasoningDetails: ReasoningDetail[] = [] |
| 203 | |
| 204 | for (const tc of toolCalls) { |
| 205 | // Check if there's a reasoning_detail with matching id |
| 206 | const matchingDetails = reasoningDetails.filter((d) => d.id === tc.id) |
| 207 | |
| 208 | if (matchingDetails.length > 0) { |
| 209 | validToolCalls.push(tc) |
| 210 | validReasoningDetails.push(...matchingDetails) |
| 211 | } else { |
| 212 | // No matching reasoning_detail - drop this tool call |
| 213 | if (tc?.id) { |
| 214 | droppedToolCallIds.add(tc.id) |
| 215 | } |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | // Also include reasoning_details that don't have an id (legacy format) |
| 220 | const detailsWithoutId = reasoningDetails.filter((d) => !d.id) |
| 221 | validReasoningDetails.push(...detailsWithoutId) |
no test coverage detected