fix(anthropic): fix batch cost always 0 - translate JSONL format and fix model_info fallback

Root cause 2: transform_file_content_response returned raw Anthropic JSONL
but calculate_batch_cost_and_usage expected OpenAI format. Added
_translate_anthropic_batch_item_to_openai() to convert Anthropic
{result.message.usage.input_tokens} to OpenAI {response.body.usage.prompt_tokens}.

Root cause 3: deployment_model_info fell back to {} (falsy) instead of None,
causing batch_cost_calculator to skip the global pricing table lookup.

Fixes #27944
This commit is contained in:
Rana-Hassan7272 2026-05-23 00:57:57 +05:00
parent 60ed90d3f5
commit 8d4cb39fb5
4 changed files with 120 additions and 1 deletions

View file

@ -338,7 +338,7 @@ class CheckBatchCost:
# Pass deployment model_info so custom batch pricing
# (input_cost_per_token_batches etc.) is used for cost calc
deployment_model_info = deployment_info.model_info.model_dump() if deployment_info.model_info else {}
deployment_model_info = deployment_info.model_info.model_dump() if deployment_info.model_info else None
batch_cost, batch_usage, batch_models = (
await calculate_batch_cost_and_usage(
file_content_dictionary=file_content_as_dict,

View file

@ -25,6 +25,17 @@ async def calculate_batch_cost_and_usage(
deployment-specific pricing (e.g. input_cost_per_token_batches)
is used instead of the global cost map.
"""
# Translate Anthropic JSONL format to OpenAI format so the shared
# cost/usage helpers work uniformly across providers.
# Anthropic uses {result: {type, message: {usage: {input_tokens, output_tokens}}}}
# but helpers expect {response: {status_code, body: {usage: {prompt_tokens, ...}}}}
# Ref: https://github.com/BerriAI/litellm/issues/27944
if custom_llm_provider == "anthropic":
file_content_dictionary = [
_translate_anthropic_batch_item_to_openai(item)
for item in file_content_dictionary
]
batch_cost = _batch_cost_calculator(
custom_llm_provider=custom_llm_provider,
file_content_dictionary=file_content_dictionary,
@ -515,6 +526,51 @@ def _get_response_from_batch_job_output_file(batch_job_output_file: dict) -> Any
return _response_body
def _translate_anthropic_batch_item_to_openai(item: dict) -> dict:
"""
Translate a single Anthropic batch result JSONL item to OpenAI format
so the shared cost/usage helpers can process it uniformly.
Anthropic format:
{"custom_id": "x", "result": {"type": "succeeded",
"message": {"model": "claude-...",
"usage": {"input_tokens": N, "output_tokens": M}}}}
OpenAI format expected by _get_response_from_batch_job_output_file:
{"custom_id": "x", "response": {"status_code": 200,
"body": {"model": "claude-...",
"usage": {"prompt_tokens": N, "completion_tokens": M,
"total_tokens": N+M}}}}
Ref: https://github.com/BerriAI/litellm/issues/27944
"""
result = item.get("result", {})
if result.get("type") != "succeeded":
# failed/errored items — return as-is so _batch_response_was_successful
# correctly treats them as non-200
return item
message = result.get("message", {})
anthropic_usage = message.get("usage", {})
prompt_tokens = anthropic_usage.get("input_tokens", 0)
completion_tokens = anthropic_usage.get("output_tokens", 0)
return {
"custom_id": item.get("custom_id"),
"response": {
"status_code": 200,
"body": {
"model": message.get("model", ""),
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": prompt_tokens + completion_tokens,
},
},
},
}
def _batch_response_was_successful(batch_job_output_file: dict) -> bool:
"""
Check if the batch job response status == 200

View file

@ -326,6 +326,18 @@ class TestAnthropicFilesConfig:
assert url == f"{ANTHROPIC_FILES_API_BASE}/v1/messages/batches/msgbatch_abc123/results"
assert params == {}
def test_transform_file_content_request_msgbatch_routes_to_batch_endpoint(self):
"""msgbatch_* IDs must route to batch results endpoint not Files API.
Ref: https://github.com/BerriAI/litellm/issues/27944
"""
url, params = self.config.transform_file_content_request(
file_content_request={"file_id": "msgbatch_abc123"},
optional_params={},
litellm_params={},
)
assert url == f"{ANTHROPIC_FILES_API_BASE}/v1/messages/batches/msgbatch_abc123/results"
assert params == {}
def test_transform_file_content_response(self):
mock_response = Mock(spec=httpx.Response)
result = self.config.transform_file_content_response(

View file

@ -0,0 +1,51 @@
"""
Tests for Anthropic batch JSONL translation.
Ref: https://github.com/BerriAI/litellm/issues/27944
"""
import pytest
from litellm.batches.batch_utils import _translate_anthropic_batch_item_to_openai
def test_translate_succeeded_item():
item = {
"custom_id": "req-1",
"result": {
"type": "succeeded",
"message": {
"model": "claude-opus-4-7",
"usage": {"input_tokens": 100, "output_tokens": 50},
},
},
}
result = _translate_anthropic_batch_item_to_openai(item)
assert result["response"]["status_code"] == 200
body = result["response"]["body"]
assert body["model"] == "claude-opus-4-7"
assert body["usage"]["prompt_tokens"] == 100
assert body["usage"]["completion_tokens"] == 50
assert body["usage"]["total_tokens"] == 150
assert result["custom_id"] == "req-1"
def test_translate_failed_item_passes_through():
item = {
"custom_id": "req-2",
"result": {"type": "errored", "error": {"type": "server_error"}},
}
result = _translate_anthropic_batch_item_to_openai(item)
# failed items pass through unchanged — no response.status_code == 200
assert result == item
def test_translate_zero_usage():
item = {
"custom_id": "req-3",
"result": {
"type": "succeeded",
"message": {"model": "claude-haiku-4-5", "usage": {}},
},
}
result = _translate_anthropic_batch_item_to_openai(item)
assert result["response"]["body"]["usage"]["prompt_tokens"] == 0
assert result["response"]["body"]["usage"]["completion_tokens"] == 0
assert result["response"]["body"]["usage"]["total_tokens"] == 0