This commit is contained in:
Naineel Soyantar 2026-09-12 23:53:51 -07:00 committed by GitHub
commit 12aa28cb53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 233 additions and 1 deletions

View file

@ -8,11 +8,19 @@ encrypted_content for multi-turn stateless workflows.
Docs: https://openrouter.ai/docs/api/reference/responses/overview
"""
from typing import Final
from typing import Any, Final
import httpx
import litellm
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import (
ResponseCompletedEvent,
ResponseInputParam,
ResponsesAPIResponse,
ResponsesAPIStreamingResponse,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import LlmProviders
@ -75,3 +83,92 @@ class OpenRouterResponsesAPIConfig(OpenAIResponsesAPIConfig):
def supports_native_websocket(self) -> bool:
"""OpenRouter does not support native WebSocket for Responses API"""
return False
def transform_responses_api_request(
self,
model: str,
input: str | ResponseInputParam,
response_api_optional_request_params: dict,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> dict:
"""
Same as the chat completions path: always ask OpenRouter to include
cost data in the response's `usage` object, so `transform_response_api_response`
below has something to extract. Without this, OpenRouter omits `usage.cost`
and every Responses API request gets logged with $0 spend.
"""
request: Final = super().transform_responses_api_request(
model=model,
input=input,
response_api_optional_request_params=response_api_optional_request_params,
litellm_params=litellm_params,
headers=headers,
)
if "usage" not in request:
request["usage"] = {"include": True}
return request
def transform_response_api_response(
self,
model: str,
raw_response: httpx.Response,
logging_obj: Any,
) -> ResponsesAPIResponse:
"""
Extracts cost information from the response body, mirroring
`OpenrouterConfig.transform_response` on the chat completions path.
OpenRouter returns cost information in the `usage` object when
`usage.include=true` is set on the request (see `transform_responses_api_request`).
"""
response: Final = super().transform_response_api_response(
model=model,
raw_response=raw_response,
logging_obj=logging_obj,
)
try:
response_json: Final = raw_response.json()
if "usage" in response_json and response_json["usage"]:
response_cost: Final = response_json["usage"].get("cost")
if response_cost is not None:
# Store cost in hidden params for the cost calculator to use
if not hasattr(response, "_hidden_params"):
response._hidden_params = {}
if "additional_headers" not in response._hidden_params:
response._hidden_params["additional_headers"] = {}
response._hidden_params["additional_headers"]["llm_provider-x-litellm-response-cost"] = float(
response_cost
)
except Exception:
# If we can't extract cost, continue without it - don't fail the response
pass
return response
def transform_streaming_response(
self,
model: str,
parsed_chunk: dict,
logging_obj: Any,
) -> ResponsesAPIStreamingResponse:
response_event: Final = super().transform_streaming_response(
model=model,
parsed_chunk=parsed_chunk,
logging_obj=logging_obj,
)
if not isinstance(response_event, ResponseCompletedEvent):
return response_event
usage: Final = response_event.response.usage
if usage is None or usage.cost is None:
return response_event
response_event.response._hidden_params["additional_headers"] = {
**response_event.response._hidden_params.get("additional_headers", {}),
"llm_provider-x-litellm-response-cost": float(usage.cost),
}
return response_event

View file

@ -9,6 +9,10 @@ reasoning.encrypted_content for multi-turn stateless workflows.
Related issue: https://github.com/BerriAI/litellm/issues/22189
"""
import json
from unittest.mock import Mock
import httpx
import pytest
import litellm
@ -82,6 +86,137 @@ class TestOpenRouterResponsesAPIConfig:
assert "OpenRouter API key is required" in str(e)
class TestOpenRouterResponsesAPICostTracking:
"""
Regression tests: OpenRouter's Responses API must request and extract cost
data the same way the chat completions path already does. Without this,
every request routed through the Responses API (e.g. Codex-style clients)
gets logged with $0 spend despite real token usage.
"""
def test_transform_request_adds_usage_include(self):
"""Request should always ask OpenRouter to include usage.cost in the response."""
config = OpenRouterResponsesAPIConfig()
from litellm.types.router import GenericLiteLLMParams
request = config.transform_responses_api_request(
model="openai/gpt-5-mini",
input="Hello",
response_api_optional_request_params={},
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert request.get("usage") == {"include": True}
def test_transform_request_preserves_existing_usage_param(self):
"""An explicitly-set usage param should not be clobbered."""
config = OpenRouterResponsesAPIConfig()
from litellm.types.router import GenericLiteLLMParams
request = config.transform_responses_api_request(
model="openai/gpt-5-mini",
input="Hello",
response_api_optional_request_params={"usage": {"include": False}},
litellm_params=GenericLiteLLMParams(),
headers={},
)
assert request.get("usage") == {"include": False}
def test_transform_response_extracts_cost(self):
"""Response should pull usage.cost into hidden params for the cost calculator."""
config = OpenRouterResponsesAPIConfig()
body = {
"id": "resp_abc123",
"object": "response",
"created_at": 1700000000,
"status": "completed",
"model": "openai/gpt-5-mini",
"output": [],
"usage": {
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150,
"cost": 0.01234,
},
}
raw_response = Mock(spec=httpx.Response)
raw_response.text = json.dumps(body)
raw_response.json.return_value = body
raw_response.headers = {}
result = config.transform_response_api_response(
model="openai/gpt-5-mini",
raw_response=raw_response,
logging_obj=Mock(),
)
assert (
result._hidden_params["additional_headers"][
"llm_provider-x-litellm-response-cost"
]
== 0.01234
)
def test_transform_response_without_cost_does_not_error(self):
"""Missing usage.cost should not raise or set the header."""
config = OpenRouterResponsesAPIConfig()
body = {
"id": "resp_abc123",
"object": "response",
"created_at": 1700000000,
"status": "completed",
"model": "openai/gpt-5-mini",
"output": [],
"usage": {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150},
}
raw_response = Mock(spec=httpx.Response)
raw_response.text = json.dumps(body)
raw_response.json.return_value = body
raw_response.headers = {}
result = config.transform_response_api_response(
model="openai/gpt-5-mini",
raw_response=raw_response,
logging_obj=Mock(),
)
assert "llm_provider-x-litellm-response-cost" not in result._hidden_params.get(
"additional_headers", {}
)
def test_transform_streaming_response_extracts_completed_response_cost(self):
config = OpenRouterResponsesAPIConfig()
result = config.transform_streaming_response(
model="openai/gpt-5-mini",
parsed_chunk={
"type": "response.completed",
"response": {
"id": "resp_abc123",
"object": "response",
"created_at": 1700000000,
"status": "completed",
"model": "openai/gpt-5-mini",
"output": [],
"usage": {
"input_tokens": 100,
"output_tokens": 50,
"total_tokens": 150,
"cost": 0.01234,
},
},
},
logging_obj=Mock(),
)
assert (
result.response._hidden_params["additional_headers"]["llm_provider-x-litellm-response-cost"]
== 0.01234
)
class TestOpenRouterResponsesAPIRegistration:
"""Test that OpenRouter is properly registered as a native Responses API provider."""