dropOrphanTools removes tool messages that are not part of the contiguous tool run answering the assistant directly before them: sending one alone 400s on every OpenAI-compatible backend ("tool message without preceding tool_calls"). Positional like dropDanglingToolCalls, and for the same reason: a
(kept []Message)
| 322 | // result whose own assistant was dropped, leaving a tool message right after |
| 323 | // a user message on the wire. |
| 324 | // |
| 325 | // Empty IDs are orphans on both ends: an unidentifiable tool message has no |
| 326 | // valid pairing. |
| 327 | func dropOrphanTools(kept []Message) []Message { |
| 328 | out := kept[:0] |
| 329 | // IDs issued by the assistant whose contiguous tool run we're inside; |
| 330 | // nil once any non-tool message ends the run. |
| 331 | var current map[string]bool |
| 332 | for _, m := range kept { |
| 333 | switch m.Role { |
| 334 | case RoleAssistant: |
| 335 | current = map[string]bool{} |
| 336 | for _, tc := range m.ToolCalls { |
| 337 | if tc.ID != "" { |
| 338 | current[tc.ID] = true |
| 339 | } |
| 340 | } |
| 341 | case RoleTool: |
| 342 | if m.ToolCallID == "" || !current[m.ToolCallID] { |
| 343 | continue |
| 344 | } |
| 345 | default: |
| 346 | current = nil |
| 347 | } |
| 348 | out = append(out, m) |
| 349 | } |
| 350 | return out |
| 351 | } |