mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
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:
parent
60ed90d3f5
commit
8d4cb39fb5
4 changed files with 120 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue