fix: normalize non-compliant tool call IDs for strict OpenAI-compatible providers (e.g. Mistral)#22318
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryAdds tool call ID normalization in
Confidence Score: 4/5
|
| Filename | Overview |
|---|---|
| litellm/llms/openai/chat/gpt_transformation.py | Adds _normalize_tool_call_ids() method and calls it in both transform_request and async_transform_request. Implementation is clean with a two-pass approach (collect then rewrite). Minor style issue with constant placement between imports (already flagged in prior thread). |
| tests/test_litellm/llms/openai/chat/test_openai_gpt_transformation.py | Adds 6 well-structured unit tests covering compliant IDs, non-compliant remapping, determinism, multiple tool calls, plain messages, and end-to-end transform_request. All tests are mock-only with no network calls. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[transform_request / async_transform_request] --> B[_normalize_tool_call_ids]
B --> C{Scan assistant messages\nfor non-compliant\ntool_call IDs}
C -->|All IDs compliant| D[Return messages unchanged]
C -->|Non-compliant IDs found| E[Build MD5-based\nid_mapping dict]
E --> F[Rewrite assistant\ntool_calls.id]
F --> G[Rewrite tool message\ntool_call_id]
G --> H[Return normalized messages]
Last reviewed commit: f694823
| IDs that already satisfy ``^[a-zA-Z0-9]{1,64}$`` are left unchanged so | ||
| there is zero overhead for well-behaved providers. | ||
| """ | ||
| _VALID_TOOL_CALL_ID = re.compile(r"^[a-zA-Z0-9]{1,64}$") |
There was a problem hiding this comment.
Regex recompiled on every call
_VALID_TOOL_CALL_ID is compiled via re.compile() on every invocation of _normalize_tool_call_ids. Since this method is called on every request, consider hoisting the compiled pattern to a module-level or class-level constant. This also aligns with the project convention of avoiding per-call overhead in the request path (CLAUDE.md: "Avoid imports within methods").
| _VALID_TOOL_CALL_ID = re.compile(r"^[a-zA-Z0-9]{1,64}$") | |
| _VALID_TOOL_CALL_ID = re.compile(r"^[a-zA-Z0-9]{1,64}$") |
Could be moved to the top of the file as:
_VALID_TOOL_CALL_ID_RE = re.compile(r"^[a-zA-Z0-9]{1,64}$")
Additional Comments (1)
The non-base-class branch (line 480) delegates to Consider adding |
…egex to module level Addresses review feedback on BerriAI#22318: - `async_transform_request` base-class path was missing the `_normalize_tool_call_ids()` call, leaving async callers exposed to the same 400 BadRequestError from strict providers (e.g. Mistral) - Hoist `_VALID_TOOL_CALL_ID_RE` to module level to avoid recompiling the regex on every request Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…penAI-compatible providers
Some providers (e.g. MiniMax via OpenRouter) return tool call IDs in
non-standard formats like `call_function_jlv0n7uyomle_1` which contain
underscores and exceed OpenAI's strict alphanumeric-only requirement.
When those IDs are forwarded in a subsequent request to a strict provider
(e.g. Mistral), the request is rejected with a 400 BadRequestError that
does not trigger fallback logic.
Adds `_normalize_tool_call_ids()` to `OpenAIGPTConfig`, called from
`transform_request()`. It scans the message list once, detects any
tool call IDs that don't match `^[a-zA-Z0-9]{1,64}$`, and rewrites both
the assistant `tool_calls[].id` and the matching tool result
`tool_call_id` fields with a stable deterministic MD5-based 9-char ID,
keeping both sides of the pair in sync.
Fixes BerriAI#22317
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…egex to module level Addresses review feedback on BerriAI#22318: - `async_transform_request` base-class path was missing the `_normalize_tool_call_ids()` call, leaving async callers exposed to the same 400 BadRequestError from strict providers (e.g. Mistral) - Hoist `_VALID_TOOL_CALL_ID_RE` to module level to avoid recompiling the regex on every request Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
55318b2 to
f694823
Compare
|
@greptile please re-review this |
Summary
Fixes #22317
Some OpenAI-compatible providers (e.g. MiniMax via OpenRouter) return tool call IDs in non-standard formats like
call_function_jlv0n7uyomle_1— using the function name and a numeric suffix. When the router forwards a subsequent turn of that conversation to a strict provider (e.g. Mistral), the request is rejected:Because this is a 400
BadRequestError, the router does not retry or fall back — the error surfaces directly to the caller.Fix
Adds
_normalize_tool_call_ids()toOpenAIGPTConfig, called fromtransform_request(). It:tool_calls[].idvalues (anything not matching^[a-zA-Z0-9]{1,64}$)tool_calls[].idand the matching tool resulttool_call_idin a second pass — keeping the pair in syncThis is analogous to the existing
_sanitize_anthropic_tool_use_id()infactory.py, but generalised for the OpenAI-to-OpenAI routing case where both sides of the ID pair must match.Test plan
test_compliant_ids_unchanged— valid IDs are not modifiedtest_non_compliant_id_is_remapped— bad ID is replaced with a valid 9-char alphanumeric IDtest_remapping_is_deterministic— same input always produces same outputtest_multiple_tool_calls_each_remapped_consistently— distinct IDs get distinct mappings; both assistant and tool messages updatedtest_messages_without_tool_calls_unaffected— plain messages pass through unchangedtest_transform_request_normalizes_ids— end-to-end throughtransform_request()🤖 Generated with Claude Code