From 64bd88efdb1db293a6ce51b3440b9d0f94f556a7 Mon Sep 17 00:00:00 2001 From: sharziki Date: Sat, 16 May 2026 13:12:46 -0400 Subject: [PATCH] fix: cast OpenRouter float token counts to int for ImageUsage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenRouter returns cost-weighted float token counts (e.g. 14417.92) for image generation models. Pydantic v2's strict int validation rejects these, raising `int_from_float` errors that abort the response. Fixes #28001 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../image_generation/transformation.py | 6 +- ...est_openrouter_image_gen_transformation.py | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/litellm/llms/openrouter/image_generation/transformation.py b/litellm/llms/openrouter/image_generation/transformation.py index a55716a5e50..43d6a059d6f 100644 --- a/litellm/llms/openrouter/image_generation/transformation.py +++ b/litellm/llms/openrouter/image_generation/transformation.py @@ -203,11 +203,11 @@ class OpenRouterImageGenerationConfig(BaseImageGenerationConfig): """ usage_data = response_json.get("usage", {}) if usage_data: - prompt_tokens = usage_data.get("prompt_tokens", 0) - total_tokens = usage_data.get("total_tokens", 0) + prompt_tokens = int(usage_data.get("prompt_tokens", 0)) + total_tokens = int(usage_data.get("total_tokens", 0)) completion_tokens_details = usage_data.get("completion_tokens_details", {}) - image_tokens = completion_tokens_details.get("image_tokens", 0) + image_tokens = int(completion_tokens_details.get("image_tokens", 0)) model_response.usage = ImageUsage( input_tokens=prompt_tokens, diff --git a/tests/test_litellm/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py b/tests/test_litellm/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py index 52a4fabaed7..fa93e85370b 100644 --- a/tests/test_litellm/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py +++ b/tests/test_litellm/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py @@ -574,6 +574,68 @@ class TestOpenRouterImageGenerationTransformation: exc_info.value ) + def test_transform_image_generation_response_float_token_counts(self): + """Test that float token counts from OpenRouter are cast to int. + + Reproduces https://github.com/BerriAI/litellm/issues/28001 where + OpenRouter returns cost-weighted float token counts (e.g. 14417.92) + that fail Pydantic's int validation for ImageUsage fields. + """ + response_data = { + "choices": [ + { + "message": { + "content": "Here is your image!", + "role": "assistant", + "images": [ + { + "image_url": { + "url": "data:image/png;base64,abc123" + }, + "index": 0, + "type": "image_url", + } + ], + } + } + ], + "usage": { + "prompt_tokens": 57.0, + "completion_tokens": 14417.92, + "total_tokens": 14474.92, + "completion_tokens_details": {"image_tokens": 14417.92}, + "cost": 0.0387243, + }, + "model": "openai/gpt-image-1", + } + + mock_response = MagicMock() + mock_response.json.return_value = response_data + mock_response.status_code = 200 + mock_response.headers = {} + + model_response = ImageResponse(data=[]) + + result = self.config.transform_image_generation_response( + model="openai/gpt-image-1", + raw_response=mock_response, + model_response=model_response, + logging_obj=self.logging_obj, + request_data={}, + optional_params={}, + litellm_params={}, + encoding=None, + ) + + # Token counts must be integers, not floats + assert result.usage is not None + assert isinstance(result.usage.input_tokens, int) + assert isinstance(result.usage.output_tokens, int) + assert isinstance(result.usage.total_tokens, int) + assert result.usage.input_tokens == 57 + assert result.usage.output_tokens == 14417 + assert result.usage.total_tokens == 14474 + def test_get_error_class(self): """Test that get_error_class returns OpenRouterException.""" error = self.config.get_error_class(