Skip to content

Fix/langfuse llmp generation - #195

Merged
a-kore merged 3 commits into
mainfrom
fix/langfuse-llmp-generation
Aug 26, 2026
Merged

Fix/langfuse llmp generation#195
a-kore merged 3 commits into
mainfrom
fix/langfuse-llmp-generation

Conversation

@a-kore

@a-kore a-kore commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

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

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧪 Test improvements
  • 🔒 Security fix

Changes Made

  • _client.py creates the Langfuse generation directly, setting model, input, output, and usage_details. LiteLLM's langfuse_otel callback emits nothing when the call runs inside an active Langfuse span, which is every predict() because they are wrapped in @langfuse_observe.
  • _client.py and langfuse_tracing.py stop registering langfuse_otel. It is unusable against the Langfuse v4 SDK this repo depends on, and it stamps llm.cost.total from LiteLLM's own response_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.py selects the OpenAI-compatible route with custom_llm_provider="openai" and passes the bare model name. Langfuse matches its price table on the bare name and finds nothing against an openai/ prefix. Both forms route identically.
  • tests/conftest.py disables Langfuse tracing suite-wide. Several suites load real credentials from .env, so mocked-litellm unit tests were shipping zero-usage observations to the live project.
  • Nine Langfuse model definitions were replaced with custom ones so reasoning tokens stop being priced on top of output. See Deployment Notes.
  • Regenerated the concierge context. The catalog was stale against main, so this sweeps in some unrelated changes across 23 artifacts and adds a missing entry for energy_oil_forecasting/starter_agent/tools.py.

Testing

  • Tests pass locally (uv run pytest tests/)
  • Type checking passes (uv run mypy <src_dir>)
  • Linting passes (uv run ruff check src_dir/)
  • Manual testing performed (describe below)

Manual testing details:

Ran a real QuantileGridLLMPredictor and a real AgentPredictor against the proxy on gemini-3.5-flash, then compared each Langfuse generation's costDetails.total against the proxy's billed cost_usd from GET /admin/request-logs:

Path Langfuse Proxy billed
llm_completion input=642, output=334, $0.003969 $0.0039690000
call_llm input=411, output=10639, $0.0963675 $0.0963675000

Also 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 @observe wrapper 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.0 while this repo requires langfuse>=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:

Models Reason
Eight Gemini models, gpt-5.4 Stop reasoning tokens being priced on top of output.
gpt-5.5 No Langfuse definition existed, so runs logged tokens but no cost.

These models no longer receive Langfuse's managed price updates, and custom definitions carry no input_cached_tokens price, 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's GET /admin/me/models; re-sync there if proxy pricing changes.

Retire the explicit generation in langfuse_generation when issue 24123 closes and langfuse_otel is confirmed to emit a generation under an active Langfuse span. That condition is recorded in the function's docstring.

Checklist

  • Code follows the project's style guidelines
  • Self-review of code completed
  • Documentation updated (if applicable)
  • No sensitive information (API keys, credentials) exposed

a-kore added 3 commits August 24, 2026 15:58
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.
@a-kore
a-kore requested a review from ethancjackson August 24, 2026 21:47
@a-kore
a-kore merged commit e981626 into main Aug 26, 2026
1 of 2 checks passed
@a-kore
a-kore deleted the fix/langfuse-llmp-generation branch August 26, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants