fix(batches): map anthropic batch lines through the anthropic message transformer

Anthropic result lines carry a message-shaped body, so ModelResponse(**body) produced empty choices and a usage key that blocked the batch-rate usage attach. Extract the existing AnthropicResponse -> ModelResponse conversion used for /v1/messages logging into anthropic_message_to_model_response and reuse it for anthropic line items.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-23 08:00:26 +00:00
parent ed23e2e82d
commit 096ba84f00
3 changed files with 34 additions and 15 deletions

View file

@ -121,11 +121,17 @@ def _line_model(
_BatchLineResult: TypeAlias = "ModelResponse | EmbeddingResponse | ResponsesAPIResponse"
def _line_result(call_type: str, response_body: Mapping[str, object]) -> _BatchLineResult:
def _line_result(
call_type: str, custom_llm_provider: _BatchLineProvider, response_body: Mapping[str, object]
) -> _BatchLineResult:
if call_type == "aembedding":
return EmbeddingResponse(**response_body) # pyright: ignore[reportArgumentType] # provider output bodies are dicts expanded as response ctor kwargs
if call_type == "aresponses":
return ResponsesAPIResponse(**response_body) # pyright: ignore[reportArgumentType] # same as above
if custom_llm_provider == "anthropic":
from litellm.litellm_core_utils.litellm_logging import anthropic_message_to_model_response
return anthropic_message_to_model_response(response_body, None)
return ModelResponse(**response_body) # pyright: ignore[reportArgumentType] # same as above
@ -235,7 +241,7 @@ async def _emit_line_event(
stats: Final = _safe_output_line_stats(entry, custom_llm_provider, model_name, model_info)
try:
result: Final = _line_result(call_type, response_body)
result: Final = _line_result(call_type, custom_llm_provider, response_body)
except Exception: # noqa: BLE001 # one unparseable line must not drop the rest of the batch's line events
verbose_logger.warning(
"batch output line could not be reconstructed as a %s response, skipping it. custom_id=%s",

View file

@ -535,6 +535,24 @@ def _provider_response_id(source: object) -> str | None:
return candidate if isinstance(candidate, str) and candidate else None
def anthropic_message_to_model_response(result: Mapping[str, object], speed: str | None) -> ModelResponse:
import httpx
from litellm.types.llms.anthropic import AnthropicResponse
pydantic_result: Final = AnthropicResponse.model_validate(result)
return litellm.AnthropicConfig().transform_parsed_response(
completion_response=pydantic_result.model_dump(),
raw_response=httpx.Response(
status_code=200,
headers={},
),
model_response=litellm.ModelResponse(id=_provider_response_id(result)),
json_mode=None,
speed=speed,
)
def mask_api_base_credentials(api_base: str) -> str:
if "key=" not in api_base:
return api_base
@ -4237,19 +4255,8 @@ class Logging(LiteLLMLoggingBaseClass):
litellm_params={},
)
else:
from litellm.types.llms.anthropic import AnthropicResponse
pydantic_result: Final = AnthropicResponse.model_validate(result)
import httpx
result = litellm.AnthropicConfig().transform_parsed_response(
completion_response=pydantic_result.model_dump(),
raw_response=httpx.Response(
status_code=200,
headers={},
),
model_response=litellm.ModelResponse(id=provider_response_id),
json_mode=None,
result = anthropic_message_to_model_response(
cast(Mapping[str, object], result),
speed=self.optional_params.get("speed") if self.optional_params else None,
)
return result

View file

@ -368,6 +368,8 @@ ANTHROPIC_OUTPUT_JSONL = json.dumps(
"role": "assistant",
"content": [{"type": "text", "text": "hello b2"}],
"model": "claude-3",
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 1, "output_tokens": 2},
},
},
@ -482,3 +484,7 @@ async def test_line_items_anthropic_shapes(recorder):
line = next(e for e in recorder.success_events if _hidden(e).get("batch_custom_id") == "b2")
assert _hidden(line)["batch_line_status_code"] == 200
assert line["litellm_params"]["batch_parent_id"] == batch.id
payload = _payload(line)
assert payload["response"]["choices"][0]["message"]["content"] == "hello b2"
assert payload["prompt_tokens"] == 1
assert payload["completion_tokens"] == 2