mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix(openrouter): move usage accounting to extra_body for compatibility
This commit is contained in:
parent
cf2db415b8
commit
49f85239b1
2 changed files with 52 additions and 15 deletions
|
|
@ -167,17 +167,19 @@ class OpenrouterConfig(OpenAIGPTConfig):
|
|||
messages = self._move_cache_control_to_content(messages)
|
||||
|
||||
extra_body = optional_params.pop("extra_body", {})
|
||||
response = super().transform_request(
|
||||
request = super().transform_request(
|
||||
model, messages, optional_params, litellm_params, headers
|
||||
)
|
||||
response.update(extra_body)
|
||||
request.update(extra_body)
|
||||
|
||||
# ALWAYS add usage parameter to get cost data from OpenRouter
|
||||
# This ensures cost tracking works for all OpenRouter models
|
||||
if "usage" not in response:
|
||||
response["usage"] = {"include": True}
|
||||
# Usage accounting is an OpenRouter-specific flag, not a valid top-level
|
||||
# OpenAI chat param. Sending it at the top level makes OpenAI-compatible
|
||||
# clients reject the request ("unexpected keyword argument 'usage'"), so
|
||||
# it must travel inside `extra_body` instead.
|
||||
extra_body = request.setdefault("extra_body", {})
|
||||
extra_body.setdefault("usage", {"include": True})
|
||||
|
||||
return response
|
||||
return request
|
||||
|
||||
def transform_response(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ def test_openrouter_cost_tracking_non_streaming():
|
|||
Test OpenRouter cost tracking for non-streaming completions.
|
||||
|
||||
Verifies:
|
||||
1. Request includes usage.include=true to get cost data
|
||||
1. Request asks for usage accounting via extra_body, not a top-level usage field
|
||||
2. Response extracts cost from usage.cost and stores in _hidden_params
|
||||
"""
|
||||
from unittest.mock import Mock, patch
|
||||
|
|
@ -380,7 +380,6 @@ def test_openrouter_cost_tracking_non_streaming():
|
|||
|
||||
config = OpenrouterConfig()
|
||||
|
||||
# Test request adds usage parameter
|
||||
transformed_request = config.transform_request(
|
||||
model="openrouter/anthropic/claude-sonnet-4.5",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
|
|
@ -388,8 +387,8 @@ def test_openrouter_cost_tracking_non_streaming():
|
|||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
assert "usage" in transformed_request
|
||||
assert transformed_request["usage"] == {"include": True}
|
||||
assert "usage" not in transformed_request
|
||||
assert transformed_request["extra_body"]["usage"] == {"include": True}
|
||||
|
||||
# Test response extracts cost
|
||||
mock_response = Mock(spec=httpx.Response)
|
||||
|
|
@ -460,13 +459,12 @@ def test_openrouter_cost_tracking_streaming():
|
|||
Test OpenRouter cost tracking for streaming completions.
|
||||
|
||||
Verifies:
|
||||
1. Request includes usage.include=true (same as non-streaming)
|
||||
1. Request asks for usage accounting via extra_body (same as non-streaming)
|
||||
2. Streaming chunks preserve usage/cost data in the final chunk
|
||||
3. Cost field is accessible in the usage object
|
||||
"""
|
||||
config = OpenrouterConfig()
|
||||
|
||||
# Test request adds usage parameter for streaming
|
||||
transformed_request = config.transform_request(
|
||||
model="openrouter/anthropic/claude-sonnet-4.5",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
|
|
@ -474,8 +472,8 @@ def test_openrouter_cost_tracking_streaming():
|
|||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
assert "usage" in transformed_request
|
||||
assert transformed_request["usage"] == {"include": True}
|
||||
assert "usage" not in transformed_request
|
||||
assert transformed_request["extra_body"]["usage"] == {"include": True}
|
||||
|
||||
# Test streaming chunks preserve cost data
|
||||
handler = OpenRouterChatCompletionStreamingHandler(
|
||||
|
|
@ -528,6 +526,43 @@ def test_openrouter_cost_tracking_streaming():
|
|||
assert result2.usage.cost == 0.0001
|
||||
|
||||
|
||||
def test_openrouter_usage_accounting_not_top_level():
|
||||
"""
|
||||
Regression: usage accounting must be requested via extra_body, never as a
|
||||
top-level field. A top-level `usage` is rejected by OpenAI-compatible clients
|
||||
with "unexpected keyword argument 'usage'" (e.g. for OpenAI models served
|
||||
through OpenRouter).
|
||||
"""
|
||||
transformed_request = OpenrouterConfig().transform_request(
|
||||
model="openrouter/openai/gpt-4o",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "usage" not in transformed_request
|
||||
assert transformed_request["extra_body"]["usage"] == {"include": True}
|
||||
|
||||
|
||||
def test_openrouter_usage_accounting_preserves_user_extra_body():
|
||||
"""
|
||||
User-provided OpenRouter params still flatten to the top level (issue #8425)
|
||||
while usage accounting is added under extra_body without clobbering them.
|
||||
"""
|
||||
transformed_request = OpenrouterConfig().transform_request(
|
||||
model="openrouter/openai/gpt-4o",
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
optional_params={"extra_body": {"provider": {"order": ["OpenAI"]}}},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "usage" not in transformed_request
|
||||
assert transformed_request["provider"]["order"] == ["OpenAI"]
|
||||
assert transformed_request["extra_body"]["usage"] == {"include": True}
|
||||
|
||||
|
||||
def test_openrouter_reasoning_models_allow_reasoning_effort_param():
|
||||
"""
|
||||
OpenRouter reasoning-capable models should accept the reasoning_effort param.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue