Skip to content

fix(billing): record charge lines for embeddings, images, moderations, and rerank - #542

Merged
njbrake merged 2 commits into
mozilla-ai:mainfrom
AmirF194:fix/432-passthrough-billing-meters
Aug 10, 2026
Merged

fix(billing): record charge lines for embeddings, images, moderations, and rerank#542
njbrake merged 2 commits into
mozilla-ai:mainfrom
AmirF194:fix/432-passthrough-billing-meters

Conversation

@AmirF194

@AmirF194 AmirF194 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

Passthrough usage rows for embeddings, images, moderations, and rerank set cost but never billing_meters or pricing_breakdown, unlike the chat/messages/responses path (calculate_metered_cost) and the imported-usage repricing path, both of which set all three together. Every one of those rows carried a bare cost with no auditable per-meter breakdown.

Root cause: run_passthrough (_passthrough.py) only ever wrote usage_log.cost from compute_cost; nothing in the shared scaffold or in the four routes wrote the two audit fields.

Fix: added a compute_meters callback to run_passthrough, mirroring compute_cost's signature and only invoked once a cost has been priced, and implemented it per route: input tokens for embeddings/rerank, image count for images, a flat per-request charge for moderations.

Not covered: audio (transcriptions/speech) is one of the four surfaces this issue names, but it never calls compute_cost at all today (no per-second/per-minute pricing model exists yet for it), so there is no cost for a meters breakdown to attach to. Audio billing needs its own pricing unit first, hence Refs #432 rather than Fixes #432.

Relevant issues

Refs #432

Verification

  • New regression tests (test_embeddings_billing_meters_tracked_with_pricing and the images/moderations/rerank equivalents) fail on main (billing_meters is None) and pass on this branch.
  • Full suite passes: make test (2596 unit + integration tests), make lint, make typecheck.
  • Did not check: audio billing, out of scope as noted above.

PR Type

  • Bug Fix

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).

AI Usage

  • AI was used for drafting/refactoring.

AI Model/Tool used: Claude (Anthropic), in an agentic coding session directed and verified by the account owner.

Any additional AI details you'd like to share: None.

  • I am an AI Agent filling out this form (check box if true)

Summary

  • Added billing details for passthrough embeddings, images, moderations, and rerank requests.
  • Added usage tracking for input tokens, image count, and moderation requests.
  • Avoided billing lines for unpriced moderation requests.
  • Added integration tests for all four routes.

Benefits

Usage logs now show how passthrough charges are calculated. This improves billing visibility and auditability.

…, and rerank

run_passthrough set usage_log.cost from compute_cost but never wrote
billing_meters or pricing_breakdown, unlike the chat/messages/responses
path (calculate_metered_cost) and the imported-usage repricing path,
both of which set cost, meters, and breakdown together. Every
embeddings/images/moderations/rerank usage row carried a bare cost with
no auditable per-meter breakdown.

Add a compute_meters callback to run_passthrough, mirroring
compute_cost's signature and only invoked once a cost has been priced,
and implement it per route: input tokens for embeddings/rerank, image
count for images, a flat per-request charge for moderations.

Refs mozilla-ai#432
@coderabbitai

coderabbitai Bot commented Aug 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aefe5cd1-2118-4286-ab21-0f33c289c110

📥 Commits

Reviewing files that changed from the base of the PR and between 2ae28a2 and 87dff16.

📒 Files selected for processing (6)
  • src/gateway/api/routes/embeddings.py
  • src/gateway/api/routes/moderations.py
  • src/gateway/api/routes/rerank.py
  • tests/integration/test_embeddings_endpoint.py
  • tests/integration/test_moderations_endpoint.py
  • tests/integration/test_rerank_endpoint.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/integration/test_rerank_endpoint.py
  • tests/integration/test_embeddings_endpoint.py
  • src/gateway/api/routes/embeddings.py
  • src/gateway/api/routes/moderations.py
  • src/gateway/api/routes/rerank.py

Walkthrough

Passthrough usage logging now supports billing meters and pricing breakdowns. Embeddings, images, moderations, and rerank routes provide endpoint-specific meter callbacks. Integration tests verify priced and unpriced billing data.

Changes

Passthrough billing metering

Layer / File(s) Summary
Meter callback contract and persistence
src/gateway/api/routes/_passthrough.py
run_passthrough accepts an optional compute_meters callback and persists its billing meters and pricing breakdown after cost calculation.
Endpoint-specific meter callbacks
src/gateway/api/routes/embeddings.py, src/gateway/api/routes/images.py, src/gateway/api/routes/moderations.py, src/gateway/api/routes/rerank.py
The routes compute input-token, image-count, or request-count meters and pass the callbacks to run_passthrough.
Billing meter integration coverage
tests/integration/test_embeddings_endpoint.py, tests/integration/test_images_endpoint.py, tests/integration/test_moderations_endpoint.py, tests/integration/test_rerank_endpoint.py
Integration tests verify billing meters and pricing breakdowns for priced requests. Moderation tests also verify that unpriced requests omit charge-line fields.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: njbrake, tbille

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title accurately describes the billing change and uses imperative wording, but it is 81 characters and exceeds the approximately 70-character limit. Shorten the title while keeping the fix(billing): prefix and the main billing change within approximately 70 characters.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the cause, fix, scope, issue, verification, checklist, PR type, and AI usage; the unchecked items are non-critical.
Linked Issues check ✅ Passed For [#432], the PR implements charge-line tracking for priced embeddings, images, moderations, and rerank, and documents audio and broader unit-model scope.
Out of Scope Changes check ✅ Passed All code and tests support [#432] by adding route-specific billing meters and pricing breakdowns for the four targeted passthrough endpoints.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai
coderabbitai Bot requested a review from njbrake August 7, 2026 22:47
Moderations prices as a flat per-request rate and returns 0.0 rather than
None when a model is unpriced, so the meters callback ran on the free path
and wrote a `$0.00` line the dashboard renders as a "billed meters" block
explaining a charge that never happened. Record nothing there instead, and
take the per-request rate from the cost, which is the same number when one
request is billed.

Also name the embeddings and rerank input meter `total_input_tokens`: that
is the meter the billed-token SQL and the dashboard's token bar read, so
the row's own meter becomes their source instead of the raw-column
fallback. Both routes log the same count as their prompt tokens, so the
reported value is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@njbrake njbrake left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note: this review was drafted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his; the prose is Claude's.

Approving. The mechanism is right, the rate math checks out on all four routes (per_image_cost stores raw USD per image, flat_request_cost divides by 1e6, both are reflected correctly in the lines), and the two line shapes match what the dashboard's breakdown renderer discriminates on. Full suite, lint, typecheck, and openapi-check all pass locally.

I pushed one commit to your branch rather than leaving it as review comments:

Unpriced moderations recorded a zero charge line. compute_cost there returns flat_request_cost(pricing), which is 0.0 and not None for an unpriced model, so compute_meters ran on the free path and wrote {"requests": 1} plus a $0.00 line. The dashboard renders that as a "Billed meters" block explaining a charge that never happened, which contradicts the route's own comment about unpriced moderations being intentionally silent, and diverges from calculate_metered_cost, which suppresses empty lines. Now returns None when nothing was charged, with a regression test.

Renamed the embeddings and rerank input meter to total_input_tokens. input_tokens had no readers: the billed-token SQL (usage.py:_billed_expr) and the dashboard's token bar (tokenComposition) both read total_input_tokens by name, so those rows were still falling through to the raw columns. Both routes log the same count as their prompt tokens, so no reported value changes; the meter just stops being decorative.

One thing I left alone deliberately. images and requests are genuinely new non-token units, which is the part of #432 this PR does not claim to solve, and they now land in the /v1/usage response before any reader exists for them, so a later rename is a wire change. I am fine shipping them as-is, but flagging that we are naming them now.

Minor, non-blocking: n_images is derived twice in images.py, once in each callback. Worth extracting so they cannot drift.

@coderabbitai
coderabbitai Bot requested review from njbrake and tbille August 10, 2026 16:55
@njbrake
njbrake deployed to integration-tests August 10, 2026 16:58 — with GitHub Actions Active
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/gateway/api/routes/embeddings.py 87.50% 1 Missing ⚠️
src/gateway/api/routes/images.py 85.71% 1 Missing ⚠️
src/gateway/api/routes/rerank.py 87.50% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/gateway/api/routes/_passthrough.py 97.47% <100.00%> (ø)
src/gateway/api/routes/moderations.py 100.00% <100.00%> (ø)
src/gateway/api/routes/embeddings.py 97.87% <87.50%> (ø)
src/gateway/api/routes/images.py 97.50% <85.71%> (ø)
src/gateway/api/routes/rerank.py 98.03% <87.50%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@njbrake
njbrake merged commit 98cf54f into mozilla-ai:main Aug 10, 2026
8 checks passed
@AmirF194
AmirF194 deleted the fix/432-passthrough-billing-meters branch August 10, 2026 18:10
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.

3 participants