fix(langfuse): pass lint gates and pin the decisions generation to a fixture

Return the decisions input/output directly instead of rebinding them, log
only the answers as output so provider extras like the response id stay out,
and check the exported generation (cost, tokens, input, output) against a
golden fixture like the other Langfuse tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rodrigo Azevedo 2026-10-27 13:18:43 -03:00
parent 2c23948072
commit c0753dcdb4
3 changed files with 45 additions and 21 deletions

View file

@ -638,9 +638,11 @@ class LangFuseLogger:
elif response_obj is not None and isinstance(response_obj, litellm.ResponsesAPIResponse):
input = prompt
output = self._get_responses_api_content_for_langfuse(response_obj)
elif response_obj is not None and isinstance(response_obj, (DecisionsResponse, OpenAIDecisionResponse)):
input = prompt
output = response_obj.model_dump(mode="json", exclude={"usage"})
elif isinstance(response_obj, (DecisionsResponse, OpenAIDecisionResponse)):
return ( # pyright: ignore[reportUnknownVariableType] # prompt is the caller's untyped dict, as in every branch
prompt,
response_obj.model_dump(mode="json", include={"answers"}),
)
elif (
kwargs.get("call_type") is not None
and kwargs.get("call_type") == "_arealtime"

View file

@ -0,0 +1,38 @@
{
"name": "litellm-asystemone",
"parent_span_id": null,
"attributes": {
"langfuse.observation.cost_details": {
"total": 1.1886e-05
},
"langfuse.observation.input": {
"messages": [
{
"content": "{\"state\": \"Customer writes: my payout has failed three days in a row.\", \"questions\": {\"urgent\": {\"type\": \"noul\", \"instructions\": \"Is this urgent?\"}}}",
"role": "user"
}
]
},
"langfuse.observation.level": "DEFAULT",
"langfuse.observation.metadata.api_base": "https://openrouter.ai/api/alpha/decisions",
"langfuse.observation.model.name": "typesafe/jev-1.13",
"langfuse.observation.model.parameters": {},
"langfuse.observation.output": {
"answers": {
"urgent": {
"noul": 0.87,
"type": "noul"
}
}
},
"langfuse.observation.type": "generation",
"langfuse.observation.usage_details": {
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"input": 283,
"output": 20,
"total": 303
},
"langfuse.trace.name": "litellm-asystemone"
}
}

View file

@ -445,7 +445,7 @@ async def test_langfuse_logging_systemone(
**({"id": response_id} if response_id is not None else {}),
"model": "typesafe/jev-1.13",
"answers": answers,
"usage": {"input_tokens": 283, "output_tokens": 20},
"usage": {"input_tokens": 283, "output_tokens": 20, "cost": 1.1886e-05},
}
)
await litellm.asystemone(
@ -457,23 +457,7 @@ async def test_langfuse_logging_systemone(
)
await _flush_langfuse()
assert upstream.call_count == 1
generations: Final = tuple(
TypeAdapter(dict[str, object]).validate_python(span["attributes"])
for span in _exported_spans(route)
if span["trace_id"] == resolve_trace_id(trace_id)
and TypeAdapter(dict[str, object]).validate_python(span["attributes"]).get("langfuse.observation.type")
== "generation"
)
assert len(generations) == 1
assert generations[0]["langfuse.observation.usage_details"] == {
"input": 283,
"output": 20,
"total": 303,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
}
output: Final = TypeAdapter(dict[str, object]).validate_python(generations[0]["langfuse.observation.output"])
assert output["answers"] == answers
_expected_generation(route, "systemone.json", trace_id)
@pytest.mark.asyncio