From bc7cce592057d785c5dfc7467b8bdb84ac43660f Mon Sep 17 00:00:00 2001 From: James Date: Fri, 25 Sep 2026 11:57:33 -0700 Subject: [PATCH] docs(rust): describe the null arguments mapping and decline coverage Fix the tool call doc comment that still claimed a non-string arguments declines, and spell out the null-to-None mapping it now shares with the deserializer. Note the deliberate absence of deny_unknown_fields, since python reads only function.arguments and the extra id, type and name keys are ignored rather than declined. Restore decline coverage for malformed tool call shapes: tool_calls that is not a list, a tool call with no function key, and a non-string non-null arguments such as a number or an object, all surfaced as the crate parse error so the host falls back to the python counter --- litellm-rust/crates/token-counter/src/types.rs | 12 +++++++++--- .../crates/token-counter/tests/token_counter.rs | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/litellm-rust/crates/token-counter/src/types.rs b/litellm-rust/crates/token-counter/src/types.rs index 7f01219d4c0..73c21039fcf 100644 --- a/litellm-rust/crates/token-counter/src/types.rs +++ b/litellm-rust/crates/token-counter/src/types.rs @@ -139,9 +139,15 @@ impl<'de> Visitor<'de> for TextValueVisitor { /// The parts of an assistant tool call Python counts: only the `arguments` /// string contributes (`_count_function_call_tokens` in -/// `litellm_core_utils/token_counter.py`). Absent arguments count as the empty -/// string; a non-string `arguments` declines so Python handles the fallback -/// instead of miscounting. +/// `litellm_core_utils/token_counter.py`). An absent arguments key counts as the +/// empty string, while an explicit null counts as the string "None" the way +/// Python's `str(None)` does; any other non-string `arguments` declines so +/// Python handles the fallback instead of miscounting. +/// +/// `deny_unknown_fields` is deliberately absent from this shape, and from +/// `ToolCallFunction` and `LegacyFunctionCall`: Python reads only +/// `function.arguments` off a tool call, so the extra `id`, `type` and `name` +/// keys the OpenAI shape carries are ignored rather than declined. #[derive(Clone, Debug, Deserialize, PartialEq)] pub(crate) struct ToolCall { pub(crate) function: ToolCallFunction, diff --git a/litellm-rust/crates/token-counter/tests/token_counter.rs b/litellm-rust/crates/token-counter/tests/token_counter.rs index 10dd5abbbb0..77dc618db38 100644 --- a/litellm-rust/crates/token-counter/tests/token_counter.rs +++ b/litellm-rust/crates/token-counter/tests/token_counter.rs @@ -262,6 +262,18 @@ mod json { #[rstest] #[case::not_json(b"not json" as &[u8])] #[case::messages_not_a_list(br#"{"model":"m","messages":"hi"}"#)] +#[case::tool_calls_not_a_list( + br#"{"model":"m","messages":[{"role":"assistant","tool_calls":"nope"}]}"# +)] +#[case::tool_call_without_function( + br#"{"model":"m","messages":[{"role":"assistant","tool_calls":[{"id":"1","type":"function"}]}]}"# +)] +#[case::tool_call_arguments_is_a_number( + br#"{"model":"m","messages":[{"role":"assistant","tool_calls":[{"id":"1","type":"function","function":{"name":"f","arguments":42}}]}]}"# +)] +#[case::tool_call_arguments_is_an_object( + br#"{"model":"m","messages":[{"role":"assistant","tool_calls":[{"id":"1","type":"function","function":{"name":"f","arguments":{"city":"Tokyo"}}}]}]}"# +)] #[case::dict_content( br#"{"model":"m","messages":[{"role":"user","content":{"type":"text","text":"x"}}]}"# )]