fix(token_counter): count assistant refusal content parts

token_counter raised UnsupportedContentTypeError on the refusal part that the OpenAI SDK
documents for assistant messages, so POST /utils/token_counter answered 500 for any history
containing one. A refusal now counts as text like the text part it replaces, and the content
block union carries it.
This commit is contained in:
dengzi 2026-11-03 10:50:12 +08:00
parent 20cb3ea439
commit ba4f91b5d1
3 changed files with 31 additions and 2 deletions

View file

@ -1691,7 +1691,13 @@ def _count_anthropic_content(
_TEXT_FIELD_BY_BLOCK_TYPE: Final = MappingProxyType(
{"text": "text", "thinking": "thinking", "redacted_thinking": "thinking", "compaction": "content"}
{
"text": "text",
"thinking": "thinking",
"redacted_thinking": "thinking",
"compaction": "content",
"refusal": "refusal",
}
)
@ -1771,7 +1777,7 @@ def _count_content_list(
f"Invalid content item type: {content_type}. "
f"Expected str or dict with 'type' field "
f"(text, image_url, image, document, file, tool_use, tool_result, thinking, redacted_thinking, "
f"compaction, tool_reference, search_result)."
f"compaction, tool_reference, search_result, refusal)."
)
return num_tokens
except Exception as e:

View file

@ -881,6 +881,11 @@ class ChatCompletionFileObject(TypedDict):
file: ChatCompletionFileObjectFile
class ChatCompletionRefusalObject(TypedDict):
type: ReadOnly[Literal["refusal"]]
refusal: ReadOnly[str]
OpenAIMessageContentListBlock = (
ChatCompletionTextObject
| ChatCompletionImageObject
@ -888,6 +893,7 @@ OpenAIMessageContentListBlock = (
| ChatCompletionDocumentObject
| ChatCompletionVideoObject
| ChatCompletionFileObject
| ChatCompletionRefusalObject
)
OpenAIMessageContent = str | Iterable[OpenAIMessageContentListBlock]

View file

@ -3626,3 +3626,20 @@ def test_openai_model_check_holds_while_openai_models_are_registered_concurrentl
registering.result()
finally:
litellm.open_ai_chat_completion_models.difference_update(registered)
def test_refusal_content_part_counts_as_the_text_it_replaces() -> None:
"""An assistant refusal part is a documented OpenAI content block, so it must count instead of raising"""
with_text = [
{"role": "user", "content": "A request"},
{"role": "assistant", "content": [{"type": "text", "text": "I cannot help with that request."}]},
{"role": "user", "content": "Please suggest a safe alternative."},
]
with_refusal = [
{"role": "user", "content": "A request"},
{"role": "assistant", "content": [{"type": "refusal", "refusal": "I cannot help with that request."}]},
{"role": "user", "content": "Please suggest a safe alternative."},
]
assert token_counter_old(model="gpt-4o", messages=with_refusal) == token_counter_old(
model="gpt-4o", messages=with_text
)