Fix/langfuse llmp generation - #195
Merged
Merged
Conversation
LiteLLM's langfuse_otel callback emits no generation when the completion runs inside an already-active Langfuse span. Every LLM-process predict() is wrapped in @langfuse_observe, so that is the configuration this repo always runs in, and token usage never reached Langfuse. An A/B of identical calls differing only in the wrapper confirms it: without @observe a litellm_request generation arrives with model and cost, and with it only the root span does. Langfuse's own data shows these generations stopping in early June 2026, when the current dependency pair landed. Create the generation directly instead, setting model, input, output, and usage_details. Cost is left to Langfuse, which derives it from usage against its own price table, and those prices match the proxy's published rates. A gemini-3.5-flash call billed by the proxy at $0.0039690000 records as $0.003969. The model name is passed bare rather than openai/-prefixed, because Langfuse matches its price table on the bare name. Langfuse tracing is also disabled across the test suite. Several suites load real credentials from .env, so without that guard unit tests with a mocked litellm shipped zero-usage observations to the live project.
The compat write-up was not committed, leaving five docstrings and comments pointing at planning-docs/litellm-langfuse-compat.md. Source should not cite planning documents in any case: they move and are deleted independently of the code. Each site keeps the explanation it needs inline. The retirement condition for langfuse_generation stays in its docstring, so the exit criteria survive without an external reference.
ethancjackson
approved these changes
Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LLM costs did not reach Langfuse. The LLM-process path logged no generations at all, and the agent path logged them at $0. Both now reconcile exactly against the proxy's billing.
This is the repo-side half of the cost-tracking work. The other half, the Vector proxy omitting Gemini thinking tokens from
completion_tokens, is already fixed upstream and verified.Clickup Ticket(s):
Type of Change
Changes Made
_client.pycreates the Langfuse generation directly, setting model, input, output, andusage_details. LiteLLM'slangfuse_otelcallback emits nothing when the call runs inside an active Langfuse span, which is everypredict()because they are wrapped in@langfuse_observe._client.pyandlangfuse_tracing.pystop registeringlangfuse_otel. It is unusable against the Langfuse v4 SDK this repo depends on, and it stampsllm.cost.totalfrom LiteLLM's ownresponse_cost, which is 0 for proxy-routed models. Langfuse honours a supplied cost instead of deriving one, so the callback pinned agent generations to $0.agent_factory.pyselects the OpenAI-compatible route withcustom_llm_provider="openai"and passes the bare model name. Langfuse matches its price table on the bare name and finds nothing against anopenai/prefix. Both forms route identically.tests/conftest.pydisables Langfuse tracing suite-wide. Several suites load real credentials from.env, so mocked-litellmunit tests were shipping zero-usage observations to the live project.output. See Deployment Notes.Testing
uv run pytest tests/)uv run mypy <src_dir>)uv run ruff check src_dir/)Manual testing details:
Ran a real
QuantileGridLLMPredictorand a realAgentPredictoragainst the proxy ongemini-3.5-flash, then compared each Langfuse generation'scostDetails.totalagainst the proxy's billedcost_usdfromGET /admin/request-logs:llm_completioncall_llmAlso confirmed a full test run emits zero observations to Langfuse, verifying the new suite-wide guard.
An A/B of identical calls differing only in the
@observewrapper confirmed the root cause on both litellm 1.89.3 and 1.97.0: nested produces no generation, standalone produces one with model and cost.Screenshots/Recordings
Not applicable. Verification was numeric and is recorded under Testing.
Related Issues
Blocked upstream by BerriAI/litellm issue 24123, "Support Langfuse Python SDK v4", open since 2026-03-19 with no maintainer response. LiteLLM pins
langfuse = ^2.45.0while this repo requireslangfuse>=4.5.1.BerriAI/litellm issue 11742, the nesting bug, is closed, but its fix predates litellm 1.85.0 by seven months. Every version this repo runs already contains it, and it is not the cause here. litellm 1.97.0 was tested and rejected; it changes nothing about this behaviour.
Deployment Notes
Eleven custom Langfuse model definitions were created in the project. These live outside this repo and are not created by any migration:
gpt-5.4output.gpt-5.5These models no longer receive Langfuse's managed price updates, and custom definitions carry no
input_cached_tokensprice, so cached input is priced at the full rate. That over-reports slightly when prompt caching is active, which is far smaller than the doubled reasoning count it replaces. Prices come from the proxy'sGET /admin/me/models; re-sync there if proxy pricing changes.Retire the explicit generation in
langfuse_generationwhen issue 24123 closes andlangfuse_otelis confirmed to emit a generation under an active Langfuse span. That condition is recorded in the function's docstring.Checklist