Skip to content

harness(langfuse): stamp model on the ModelCompleted generation#43

Merged
senamakel merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/langfuse-generation-model
Jul 8, 2026
Merged

harness(langfuse): stamp model on the ModelCompleted generation#43
senamakel merged 2 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/langfuse-generation-model

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

The per-call generation-create observation was built solely from AgentEvent::ModelCompleted, which carries no model name (only ModelStarted { call_id, model } does), so the exporter never set body["model"]. Langfuse could not map its pricing table and every agent-run generation recorded cost $0.

Fix: pre-scan the batch to correlate each call's model by call_id from ModelStarted, and stamp body["model"] on the matching ModelCompleted generation (AgentEvent enum unchanged). clean_nulls drops the key when a model can't be correlated, leaving the payload unchanged for that case. Adds a test asserting the generation carries the model ModelStarted announced.

Validation: cargo fmt clean; cargo test --lib green (996 passed, incl. the new test). Note: cargo clippy --lib --tests -D warnings fails only on two pre-existing, unrelated lints under Rust 1.93.0 in files this PR does not touch (collapsible_if in src/harness/model/mod.rs, duplicated attribute in src/harness/stream/mod.rs); this diff introduces zero clippy warnings.

Closes tinyhumansai/openhuman#4656

Summary by CodeRabbit

  • Bug Fixes
    • Generation ingestion events now include the correct model name (from matching model-start context) and carry the associated call identifier for more accurate downstream reporting and pricing.
    • Improved consistency when recording tool-call names during stream accumulation, ensuring tool names are captured once when provided.
  • Tests
    • Added a unit test to verify completed generation events preserve the expected model and call_id when the model is supplied via a prior model-start event.

The per-call generation-create observation was projected solely from
AgentEvent::ModelCompleted, which carries no model name (only
ModelStarted does), so the exporter never set body["model"]. Langfuse
could not map its pricing table and every agent-run generation recorded
cost $0.

Pre-scan the batch to correlate each call's model by call_id from
ModelStarted, and stamp body["model"] on the matching ModelCompleted
generation. clean_nulls drops the key when a model can't be correlated,
leaving the payload unchanged for that case.
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8feaf315-88dc-42c4-8322-a432e2974db7

📥 Commits

Reviewing files that changed from the base of the PR and between 7967b4f and 3f2c64a.

📒 Files selected for processing (2)
  • src/harness/model/mod.rs
  • src/harness/stream/mod.rs
💤 Files with no reviewable changes (1)
  • src/harness/stream/mod.rs

📝 Walkthrough

Walkthrough

Langfuse ingestion now correlates ModelStarted and ModelCompleted events by call_id and stamps the resolved model onto generation payloads. The PR also includes a small stream accumulator refactor and a test-module attribute adjustment.

Changes

Model correlation for Langfuse generations

Layer / File(s) Summary
Build and thread call model lookup
src/harness/observability/langfuse/mod.rs
Adds a BTreeMap import, scans observations to collect call_id to model mappings from ModelStarted events, and passes that mapping into observation_event.
Stamp generation payload and verify it
src/harness/observability/langfuse/mod.rs, src/harness/observability/langfuse/test.rs
Adds body.model to ModelCompleted generation payloads from the correlated lookup and adds a test that checks the resulting generation-create event.

Harness cleanup

Layer / File(s) Summary
Tool-name assignment and test module placement
src/harness/model/mod.rs, src/harness/stream/mod.rs
Simplifies the first non-empty tool_name assignment check in StreamAccumulator::push_tool_chunk and adjusts #[cfg(test)] placement around the stream test module.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

Poem

A bunny hopped through traces bright,
And stamped the model just right.
No more zero-cost sighs —
The ledger now smiles,
With Langfuse all tidy tonight. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes StreamAccumulator tool-name merging and a stream test cfg annotation, which are unrelated to the Langfuse objective. Move the lint-only cleanup to a separate PR, or document it as required scope if it must stay here.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately describes the main Langfuse model-stamping change.
Linked Issues check ✅ Passed The PR stamps the model from ModelStarted onto matching ModelCompleted generations, addressing the reported $0 cost issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/harness/observability/langfuse/test.rs (1)

98-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider covering the no-correlation case.

The test validates the happy path well. Given the PR relies on clean_nulls to drop model when no ModelStarted correlates, a small companion test asserting generation["body"].get("model").is_none() for a lone ModelCompleted would lock in that fallback behavior and guard against regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/harness/observability/langfuse/test.rs` around lines 98 - 142, Add a
companion test in the Langfuse observability test module to cover the
no-correlation fallback: use `LangfuseClient::proxy` and `build_ingestion_batch`
with only a lone `AgentEvent::ModelCompleted` (no matching `ModelStarted` /
`call_id` correlation), then assert the generated `generation-create` body has
no `model` field via `generation["body"].get("model").is_none()`. This should
sit alongside `generation_carries_model_from_model_started` and verify the
`clean_nulls` path in the ingestion projection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/harness/observability/langfuse/test.rs`:
- Around line 98-142: Add a companion test in the Langfuse observability test
module to cover the no-correlation fallback: use `LangfuseClient::proxy` and
`build_ingestion_batch` with only a lone `AgentEvent::ModelCompleted` (no
matching `ModelStarted` / `call_id` correlation), then assert the generated
`generation-create` body has no `model` field via
`generation["body"].get("model").is_none()`. This should sit alongside
`generation_carries_model_from_model_started` and verify the `clean_nulls` path
in the ingestion projection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d324fc0-c930-4cdd-99af-65f2d39e0b25

📥 Commits

Reviewing files that changed from the base of the PR and between b1f0d82 and 7967b4f.

📒 Files selected for processing (2)
  • src/harness/observability/langfuse/mod.rs
  • src/harness/observability/langfuse/test.rs

collapsible_if in StreamAccumulator::push_tool_chunk and a duplicated
#[cfg(test)] on the stream test module both fail CI's
cargo clippy --all-targets -- -D warnings under rust 1.96. Behavior-neutral.
@M3gA-Mind

Copy link
Copy Markdown
Contributor Author

CI guardian: the Rust SDK check was red on the same pre-existing main clippy failure under stable rust 1.96 (a collapsible_if in StreamAccumulator::push_tool_chunk plus a duplicated #[cfg(test)] on the stream test module), inherited by this branch. Pushed a behavior-neutral lint fix so CI can go green; no change to this PR's own logic. (Note: an unrelated git-worktree unit test, workspace::git::test::remove_force_deletes_dirty_worktree, is flaky under parallel --all-features runs — it passes on isolated re-run; if the CI job trips on it, it just needs a re-run, not a code change.)

@senamakel senamakel merged commit 4b00181 into tinyhumansai:main Jul 8, 2026
2 checks passed
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.

[observability] Model name not stamped on Langfuse generations → agent-run cost always $0

2 participants