Skip to content

fix: dedupe OTLP/proxy usage, correct Anthropic cache accounting, tiered pricing - #66

Merged
Haannbboo merged 8 commits into
mainfrom
fix/otlp-proxy-dedup
Aug 5, 2026
Merged

Haannbboo merged 8 commits into
mainfrom
fix/otlp-proxy-dedup

Conversation

@Haannbboo

Copy link
Copy Markdown
Owner

Summary

This PR changes high-risk areas: cost/token accounting, provider adapter behavior, schema migration, and usage dedup between the OTLP collector and proxy.

  • Fix OTLP + proxy double-recording the same usage (session ownership of usage rows, dedup on ingest).
  • Run the OTLP collector alongside the proxy in isolated tracking so both paths record consistently.
  • Correct Anthropic cache token accounting (cache_creation vs cache_read) across proxy, cost calc, sessions, and dashboard.
  • Support tiered model pricing from LiteLLM and YAML config (config/pricing.py), with daily re-fetch fixes and IPv6 stall avoidance in LiteLLM pricing lookup.
  • Schema migration adds sessions.cache_creation_tokens with backfill from usage.

Why / Context

OTLP-tracked sessions and proxy-recorded usage could count the same tokens twice. Cache token columns were mislabeled for Anthropic (creation vs read) causing wrong cost and dashboard totals. LiteLLM pricing lookups could stall on IPv6 and re-fetch stale data.

How It Works

  • Usage rows are assigned to a session; the recorder and proxy use consistent session ownership so OTLP spans and proxy calls don't duplicate.
  • cache_creation_tokens is tracked end-to-end and backfilled for pre-existing sessions.
  • Pricing: tiered (e.g. batch/detail) pricing supported from LiteLLM responses and YAML; lookup results cached per day to avoid repeated network fetches; IPv6 connection stalls bounded.

Manual QA

  • Happy path: normal proxy calls record usage once with correct cache token split
  • Isolated tracking: OTLP-only traffic records without proxy duplication
  • Migration: existing sessions backfilled with cache_creation_tokens from usage rows

Testing

  • uv run python -m pytest -q: pass — 712 passed
  • pre-commit run --all-files: not run on the final diff — state before last commit

Risk / Rollout / Rollback

  • Risk: dedup logic could drop legitimate duplicate usage if session ownership is misassigned; backfill UPDATE touches existing sessions rows.
  • Rollout: migration is additive (cache_creation_tokens column + backfill), safe on existing DBs.
  • Rollback: revert migration code; column can be left in place harmlessly.

Data / Privacy Impact

  • Raw prompts/responses/request bodies captured by default: no
  • Secrets/auth headers/cookies touched: no
  • Logs/errors scrubbed: yes — no new payload logging

Cost / Provider / Schema Impact

  • Cost/token accounting changed: yes — cache token split and tiered pricing
  • Provider normalization changed: yes — LiteLLM pricing lookup behavior
  • Streaming/tool-call behavior changed: no
  • Migration/backfill required: yes — sessions.cache_creation_tokens + backfill

Review

  • Independent code review completed before commit: no — committed prior to review; review pending
  • Must-fix review findings resolved: no — none filed yet
  • Standards checked against AGENTS.md, .agents/commands/llm-tracker.md, and .agents/commands/pre-pr.md: yes

Known Limitations / Follow-ups

  • Backfill uses a targeted UPDATE, not rebuild_sessions_from_usage, so evaluation columns are preserved.
  • LiteLLM pricing daily-cache is in-memory only; a restart re-fetches once.

Haannbboo and others added 6 commits August 1, 2026 03:11
…, and dashboard

- Add auth_scheme: x-api-key provider config so the proxy can inject
  Anthropic-native auth instead of always forcing Authorization: Bearer.
- Fix streaming usage extraction to merge Anthropic's message_start and
  message_delta events instead of the latter overwriting the former.
- Fix extract_usage: Anthropic's input_tokens/cache_read_input_tokens/
  cache_creation_input_tokens are disjoint, additive buckets (unlike
  OpenAI's subset semantics), so prompt_tokens was undercounted whenever
  cache reads were present.
- Price cache-write tokens (cache_creation_tokens) in calculate_costs,
  threaded through both the proxy and OTLP recording paths.
- Fix "Cache Hit Rate" (cached / (prompt + cache_creation), not cached /
  prompt) across ~9 backend and frontend locations that all silently read
  ~100% whenever cache writes dominated a request.
- Add sessions.cache_creation_tokens migration with a targeted,
  self-healing backfill (not rebuild_sessions_from_usage, which would
  wipe evaluation data) and an index on usage.session_id.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added Anthropic provider support with native API-key authentication and Claude model examples.
    • Added tiered pricing for long-context models, including cache-read and cache-write costs.
    • Added cache-write token visibility across logs, sessions, dashboards, charts, and cost breakdowns.
    • Added Qwen model and provider badges.
  • Bug Fixes
    • Improved streaming usage tracking, cache-hit calculations, and duplicate usage detection.
  • Documentation
    • Documented provider authentication requirements and Anthropic client headers.
  • Release
    • Updated version to 0.2.3.

Walkthrough

This release adds cache-creation token tracking, tiered pricing, Anthropic authentication and streaming support, cross-source usage deduplication, session migrations, and frontend cache-write reporting. The package version changes to 0.2.3.

Changes

Cache accounting and provider support

Layer / File(s) Summary
Tiered pricing ingestion
config/*, tests/test_pricing.py
Parses token-range pricing, applies fallbacks, skips malformed tiers, and manages fresh LiteLLM pricing caches with IPv4-only HTTPS fetching.
Tiered cost calculation
src/costs.py, src/api.py, tests/test_costs.py
Selects pricing by total input tokens, applies cache-write rates and provider multipliers, and serializes pricing tiers.
Usage persistence and deduplication
src/utils.py, src/database/*, src/recorder.py, src/schema_migrations.py, tests/test_utils.py, tests/test_database.py, tests/test_recorder.py
Normalizes cache tokens, merges matching proxy and OTLP records, persists session totals, updates aggregates, and repairs migrated session totals.
Provider authentication and streaming
README.md, config.example.yaml, config/*, src/proxy.py, tests/test_proxy.py
Adds Bearer or x-api-key authentication and combines usage from multiple Anthropic stream events.
Frontend cache reporting
frontend/src/*
Displays cache-write tokens and costs, updates cache-hit calculations, aggregates dashboard totals, and adds Qwen badges.
Release and isolated tracking wiring
VERSION, src/cli.py, tests/test_cli.py
Updates the package version and assigns the isolated OTLP logs endpoint before proxy startup.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit counts each cache-write byte,
Tiers hop upward, priced just right.
Anthropic streams through moonlit air,
Merged records settle in a lair.
Charts bloom with tokens, indigo and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: OTLP/proxy deduplication, Anthropic cache accounting, and tiered pricing.
Description check ✅ Passed The description directly explains the deduplication, cache accounting, pricing, migration, testing, and rollout changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/otlp-proxy-dedup

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 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.

Actionable comments posted: 8

🧹 Nitpick comments (7)
config/app.py (3)

102-134: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Neither tier parser guarantees ascending min_tokens. _select_tier in src/costs.py (Lines 100-113) scans tiers in sequence and stops at the first containing range, so it depends on ascending order. Both parsers pass the source order through unchanged, which makes correct billing depend on the input file.

  • config/app.py#L102-L134: sort the parsed tiers by min_tokens before returning them. YAML is user-authored, so out-of-order tiers are plausible here.
  • config/pricing.py#L119-L132: apply the same sort to the LiteLLM tiers. Upstream data is currently ordered, so this change is defensive.
🤖 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 `@config/app.py` around lines 102 - 134, Sort the parsed tiers by min_tokens
before returning them from the parser in config/app.py lines 102-134, so
_select_tier receives ascending ranges regardless of YAML order. Apply the same
sorting change to the LiteLLM tier parser in config/pricing.py lines 119-132;
both sites require direct updates.

238-238: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Normalize auth_scheme when reading it from YAML.

src/proxy.py compares provider.auth_scheme == "x-api-key" exactly. A value such as X-API-Key or "x-api-key " silently falls back to bearer authentication, and the request fails upstream with a confusing 401. Lowercase and strip the value here.

♻️ Proposed change
-            auth_scheme=provider.get("auth_scheme", "bearer"),
+            auth_scheme=str(provider.get("auth_scheme") or "bearer").strip().lower(),
🤖 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 `@config/app.py` at line 238, Normalize the value assigned to auth_scheme in
the provider configuration by stripping surrounding whitespace and converting it
to lowercase before constructing the provider settings. Keep the existing
"bearer" default intact when the YAML key is absent, so src/proxy.py can
continue its exact comparison reliably.

160-188: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Derive the flat fallback from the first parsed tier.

Lines 165-174 read raw_tiers[0] directly. _parse_tiered_cost can drop that same entry, for example when range is [None, 1000]. The flat prices then come from a tier that is not part of the final pricing. config/pricing.py Lines 152-160 uses the first parsed tier instead, so the two paths disagree. The existing test test_parse_model_cost_skips_malformed_tiers covers this input but does not assert cost.input.

Deriving effective_flat after _parse_tiered_cost runs would also remove the effective_flat is not flat juggling at Lines 187-188.

🤖 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 `@config/app.py` around lines 160 - 188, Update the tiered-cost handling around
_parse_tiered_cost to derive the flat fallback from the first successfully
parsed tier rather than raw_tiers[0], so malformed or skipped entries cannot
supply fallback prices. Reuse the parsed-tier result when assigning both tiers
and effective flat values, remove the effective_flat identity juggling, and
preserve explicit flat-price overrides and empty-tier behavior.
config/pricing.py (3)

327-332: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the stray ponytail: marker.

The comment text is useful. The ponytail: prefix looks like a leftover authoring artifact.

♻️ Proposed change
-    # ponytail: raw.githubusercontent.com's IPv6 candidates are unreachable on
+    # raw.githubusercontent.com's IPv6 candidates are unreachable on
🤖 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 `@config/pricing.py` around lines 327 - 332, Remove the stray “ponytail:”
prefix from the comment while preserving the remaining explanation unchanged.

352-364: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse _remote_costs before re-reading the fresh cache file.

While the cache is fresh, every fetch_remote_pricing() call reads and fully parses litellm_pricing.json, even when _remote_costs is already populated. The LiteLLM file is large, so this repeats a costly parse under _remote_lock and blocks get_remote_pricing() callers.

♻️ Proposed change
     with _remote_lock:
         if _cache_is_fresh():
+            if _remote_costs:
+                return dict(_remote_costs)
             cached = _load_local_cache()
🤖 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 `@config/pricing.py` around lines 352 - 364, Update the fresh-cache branch in
fetch_remote_pricing around _cache_is_fresh() to return a copy of the
already-populated _remote_costs before calling _load_local_cache(). Preserve the
existing file-load fallback when _remote_costs is empty, including assigning and
returning successfully parsed cached data.

119-132: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Tier order is taken from the upstream file.

_select_tier in src/costs.py depends on ascending min_tokens. This parser preserves the LiteLLM order without checking it. LiteLLM data is currently ordered, so this is defensive only. Sorting by min_tokens before returning removes the dependency on upstream ordering.

🤖 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 `@config/pricing.py` around lines 119 - 132, Update the tier parsing function
that builds ModelTier entries to sort the collected tiers by min_tokens before
returning them. Preserve the existing malformed-tier skipping and None result
behavior, while ensuring _select_tier receives tiers in ascending min_tokens
order regardless of upstream file ordering.
src/database/usage.py (1)

195-200: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use the module logger instead of a local import.

This module now imports logging at line 9 and defines logger at line 30. The nested import logging is redundant.

♻️ Proposed cleanup
         except Exception:
-            import logging
-
-            logging.getLogger(__name__).warning(
+            logger.warning(
                 "Failed to update session record for merged usage ts=%s", existing.ts
             )
🤖 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/database/usage.py` around lines 195 - 200, In the exception handler for
the merged usage session update, remove the nested logging import and use the
module-level logger defined near the top of the module instead of
logging.getLogger(__name__).
🤖 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.

Inline comments:
In `@frontend/src/charts/TrendChart.tsx`:
- Around line 46-47: Update the token composition in
frontend/src/charts/TrendChart.tsx at lines 46-47 to include cache-write tokens
in the displayed input segment or add a separate cache-write segment and tooltip
value, while preserving the cache-hit denominator. Update
frontend/src/pages/OverviewTab.tsx at lines 394-396 to display
totals.cacheCreationTokens alongside the input and output breakdown.

In `@frontend/src/pages/LogsPage.tsx`:
- Around line 437-449: Update the backend cost calculation to return separate
normal-input, cache-read, and cache-write cost components using the selected
pricing tier, rather than deriving them from input_cost_usd by token ratios.
Expose these components through the existing response data and update the
LogsPage cost rendering around
totalInputTokens/cacheCost/cacheWriteCost/actualInputCost to use the returned
values directly, removing the proportional split.

In `@src/costs.py`:
- Around line 152-158: Extend the ModelTier schema with a cache-creation rate
field mapped from LiteLLM’s cache_creation_input_token_cost, then in the
cache_write_cost calculation select the active tier’s rate when available and
fall back to ModelCost.cache_write otherwise. Ensure the selected per-token rate
is applied to cache_created before converting from per-million-token pricing.

In `@src/database/usage.py`:
- Line 73: Remove the stray “ponytail:” prefix from the comment in the usage
code, preserving the remaining comment text unchanged.
- Around line 164-167: Update the cross-process race comment near the Usage
query in the relevant lookup method to clarify that with_for_update() provides
row locking only on databases that support it, specifically PostgreSQL, and does
not lock rows on SQLite. If SQLite requires equivalent race protection, replace
or supplement this mechanism with a SQLite-compatible lock while preserving the
existing behavior on other backends.

In `@src/recorder.py`:
- Line 16: Update merge_duplicate_usage and both OTLP and proxy call paths in
record_usage to include Usage.cache_creation_tokens == cache_creation_tokens in
the duplicate lookup alongside the existing token-field matches, ensuring rows
with different cache-creation values do not merge.
- Around line 52-71: The record_usage flow must prevent concurrent OTLP and
proxy requests from inserting duplicate Usage rows when merge_duplicate_usage
finds no existing match. Enforce atomic coordination using a
commit-before-insert ordering or a database-level uniqueness constraint on the
shared call key, ensuring the existing windowed duplicate lookup cannot race
into two inserts; preserve duplicate enrichment and return behavior.

In `@src/schema_migrations.py`:
- Around line 816-865: Ensure the migration adds usage.cache_creation_tokens
before the sessions backfill can query it. In the migration flow around
sessions_cache_creation_added and the usage-dependent backfill, call
_ensure_column for the existing usage table with the appropriate INTEGER NOT
NULL DEFAULT 0 definition for both supported databases, record the change in
applied consistently, and then execute the existing sessions backfill using the
newly ensured column.

---

Nitpick comments:
In `@config/app.py`:
- Around line 102-134: Sort the parsed tiers by min_tokens before returning them
from the parser in config/app.py lines 102-134, so _select_tier receives
ascending ranges regardless of YAML order. Apply the same sorting change to the
LiteLLM tier parser in config/pricing.py lines 119-132; both sites require
direct updates.
- Line 238: Normalize the value assigned to auth_scheme in the provider
configuration by stripping surrounding whitespace and converting it to lowercase
before constructing the provider settings. Keep the existing "bearer" default
intact when the YAML key is absent, so src/proxy.py can continue its exact
comparison reliably.
- Around line 160-188: Update the tiered-cost handling around _parse_tiered_cost
to derive the flat fallback from the first successfully parsed tier rather than
raw_tiers[0], so malformed or skipped entries cannot supply fallback prices.
Reuse the parsed-tier result when assigning both tiers and effective flat
values, remove the effective_flat identity juggling, and preserve explicit
flat-price overrides and empty-tier behavior.

In `@config/pricing.py`:
- Around line 327-332: Remove the stray “ponytail:” prefix from the comment
while preserving the remaining explanation unchanged.
- Around line 352-364: Update the fresh-cache branch in fetch_remote_pricing
around _cache_is_fresh() to return a copy of the already-populated _remote_costs
before calling _load_local_cache(). Preserve the existing file-load fallback
when _remote_costs is empty, including assigning and returning successfully
parsed cached data.
- Around line 119-132: Update the tier parsing function that builds ModelTier
entries to sort the collected tiers by min_tokens before returning them.
Preserve the existing malformed-tier skipping and None result behavior, while
ensuring _select_tier receives tiers in ascending min_tokens order regardless of
upstream file ordering.

In `@src/database/usage.py`:
- Around line 195-200: In the exception handler for the merged usage session
update, remove the nested logging import and use the module-level logger defined
near the top of the module instead of logging.getLogger(__name__).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bd213b12-1750-4592-b226-c46711c3e3df

📥 Commits

Reviewing files that changed from the base of the PR and between 87afe2b and 1e34063.

⛔ Files ignored due to path filters (1)
  • frontend/public/models/qwen-color.svg is excluded by !**/*.svg
📒 Files selected for processing (34)
  • README.md
  • VERSION
  • config.example.yaml
  • config/app.py
  • config/models.py
  • config/pricing.py
  • frontend/src/charts/SparklineTrendPanel.tsx
  • frontend/src/charts/TopUsageChart.tsx
  • frontend/src/charts/TrendChart.tsx
  • frontend/src/components/SessionDetailPanel.tsx
  • frontend/src/hooks/useDashboardData.ts
  • frontend/src/i18n/zh.ts
  • frontend/src/model-badge.ts
  • frontend/src/pages/LogsPage.tsx
  • frontend/src/pages/OverviewTab.tsx
  • frontend/src/types.ts
  • frontend/src/utils.tsx
  • src/api.py
  • src/cli.py
  • src/costs.py
  • src/database/models.py
  • src/database/sessions.py
  • src/database/usage.py
  • src/proxy.py
  • src/recorder.py
  • src/schema_migrations.py
  • src/utils.py
  • tests/test_cli.py
  • tests/test_costs.py
  • tests/test_database.py
  • tests/test_pricing.py
  • tests/test_proxy.py
  • tests/test_recorder.py
  • tests/test_utils.py

Comment thread frontend/src/charts/TrendChart.tsx
Comment thread frontend/src/pages/LogsPage.tsx
Comment thread src/costs.py
Comment thread src/database/usage.py
Comment thread src/database/usage.py
Comment thread src/recorder.py
Comment thread src/recorder.py
Comment thread src/schema_migrations.py
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_pricing.py (2)

1685-1702: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This guard never reaches the connection path it protects.

_RaisingOpener.open raises before any connection is attempted. _IPv4OnlyHTTPSHandler therefore never runs, so _create_ipv4_connection never runs either. The docstring names the global socket.getaddrinfo patch as the bug under guard, but that patch would have lived in the connection path.

The test would still pass if someone reintroduced a global socket.getaddrinfo assignment inside the handler or inside _create_ipv4_connection. Drive the assertion through the connection path instead. Patch pricing_module.socket.socket or pricing_module.socket.getaddrinfo to fail, and let the real opener and handler run, so the resolution step is actually exercised before the assertion.

♻️ Proposed change to exercise the connection path
 def test_fetch_litellm_json_does_not_mutate_global_getaddrinfo(monkeypatch):
     """Regression guard for the original bug: this must not monkeypatch
     socket.getaddrinfo globally, since the same process also resolves DNS
     for concurrent, unrelated live proxy traffic."""
     original_getaddrinfo = pricing_module.socket.getaddrinfo
+    calls = {"resolved": 0}
 
-    class _RaisingOpener:
-        def open(self, *a, **k):
-            raise urllib.error.URLError("network unavailable in test")
+    def _failing_getaddrinfo(host, port, family, socktype):
+        calls["resolved"] += 1
+        assert family == pricing_module.socket.AF_INET
+        raise OSError("network unavailable in test")
 
-    monkeypatch.setattr(
-        pricing_module.urllib.request, "build_opener", lambda *a, **k: _RaisingOpener()
-    )
+    # Let the real opener and _IPv4OnlyHTTPSHandler run so the connection
+    # path is actually exercised.
+    monkeypatch.setattr(pricing_module.socket, "getaddrinfo", _failing_getaddrinfo)
 
     result = pricing_module._fetch_litellm_json()
 
     assert result is None
+    assert calls["resolved"] > 0, "the IPv4 connection path was never reached"
+
+
+def test_fetch_litellm_json_restores_global_getaddrinfo(monkeypatch):
+    original_getaddrinfo = pricing_module.socket.getaddrinfo
+
+    class _RaisingOpener:
+        def open(self, *a, **k):
+            raise urllib.error.URLError("network unavailable in test")
+
+    monkeypatch.setattr(
+        pricing_module.urllib.request, "build_opener", lambda *a, **k: _RaisingOpener()
+    )
+
+    assert pricing_module._fetch_litellm_json() is None
     assert pricing_module.socket.getaddrinfo is original_getaddrinfo

Note: monkeypatch restores pricing_module.socket.getaddrinfo at teardown, so the second test keeps the original identity assertion meaningful.

🤖 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 `@tests/test_pricing.py` around lines 1685 - 1702, Update
test_fetch_litellm_json_does_not_mutate_global_getaddrinfo to use the real
opener and _IPv4OnlyHTTPSHandler connection path instead of _RaisingOpener,
while patching pricing_module.socket.socket or pricing_module.socket.getaddrinfo
to fail after resolution is attempted. Keep the result assertion and verify
pricing_module.socket.getaddrinfo retains its original identity, ensuring
regressions in _create_ipv4_connection or the handler’s global DNS handling are
detected.

480-507: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the flat cost.cache_write for a tiers-only entry.

This entry has no top-level input_cost_per_token or output_cost_per_token, so _parse_model_entry takes the tiers-only branch and leaves the flat cost.cache_write as None even though cache_creation_input_token_cost is 3.75e-06. calculate_costs() falls back to the flat cache-write rate when no tier matches, so a tiers-only entry with no match drops the cache-write cost. Add an assertion that preserves the intended flat value.

💚 Proposed assertion for the flat cache-write rate
     result = _parse_model_entry("anthropic/claude-long-context", entry)
 
     assert result is not None
     _, cost = result
+    # Flat cache_write must survive a tiers-only entry; cost paths that miss
+    # every tier fall back to it.
+    assert cost.cache_write == 3.75
     first, second = cost.tiers
🤖 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 `@tests/test_pricing.py` around lines 480 - 507, Extend
test_parse_model_entry_tier_carries_own_cache_write_price to assert that the
parsed flat cost.cache_write equals 3.75 for this tiers-only entry. Keep the
existing per-tier assertions unchanged and validate the flat fallback value
alongside the tier-specific rates.
🤖 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.

Inline comments:
In `@tests/test_pricing.py`:
- Around line 235-251: The test_parse_model_cost_skips_malformed_tiers test only
verifies the retained tier, not the selected flat prices. Add assertions that
cost.input is 1.2 and cost.output is 4.8, confirming malformed tiers are skipped
before deriving effective_flat and matching the valid tier selected by the
LiteLLM parser.

---

Nitpick comments:
In `@tests/test_pricing.py`:
- Around line 1685-1702: Update
test_fetch_litellm_json_does_not_mutate_global_getaddrinfo to use the real
opener and _IPv4OnlyHTTPSHandler connection path instead of _RaisingOpener,
while patching pricing_module.socket.socket or pricing_module.socket.getaddrinfo
to fail after resolution is attempted. Keep the result assertion and verify
pricing_module.socket.getaddrinfo retains its original identity, ensuring
regressions in _create_ipv4_connection or the handler’s global DNS handling are
detected.
- Around line 480-507: Extend
test_parse_model_entry_tier_carries_own_cache_write_price to assert that the
parsed flat cost.cache_write equals 3.75 for this tiers-only entry. Keep the
existing per-tier assertions unchanged and validate the flat fallback value
alongside the tier-specific rates.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e56cc08-ecb9-4d41-abe1-c8fefd580253

📥 Commits

Reviewing files that changed from the base of the PR and between 87afe2b and c90035d.

⛔ Files ignored due to path filters (1)
  • frontend/public/models/qwen-color.svg is excluded by !**/*.svg
📒 Files selected for processing (36)
  • README.md
  • VERSION
  • config.example.yaml
  • config/app.py
  • config/models.py
  • config/pricing.py
  • frontend/src/charts/SparklineTrendPanel.tsx
  • frontend/src/charts/TopUsageChart.tsx
  • frontend/src/charts/TrendChart.tsx
  • frontend/src/components/SessionDetailPanel.tsx
  • frontend/src/hooks/useDashboardData.ts
  • frontend/src/i18n/zh.ts
  • frontend/src/model-badge.ts
  • frontend/src/pages/LogsPage.tsx
  • frontend/src/pages/OverviewTab.tsx
  • frontend/src/types.ts
  • frontend/src/utils.tsx
  • src/api.py
  • src/cli.py
  • src/costs.py
  • src/database/models.py
  • src/database/sessions.py
  • src/database/usage.py
  • src/otlp.py
  • src/proxy.py
  • src/recorder.py
  • src/schema_migrations.py
  • src/utils.py
  • tests/test_cli.py
  • tests/test_costs.py
  • tests/test_database.py
  • tests/test_otlp.py
  • tests/test_pricing.py
  • tests/test_proxy.py
  • tests/test_recorder.py
  • tests/test_utils.py
🚧 Files skipped from review as they are similar to previous changes (33)
  • frontend/src/hooks/useDashboardData.ts
  • src/cli.py
  • src/database/sessions.py
  • src/api.py
  • tests/test_cli.py
  • config/models.py
  • frontend/src/components/SessionDetailPanel.tsx
  • frontend/src/charts/TrendChart.tsx
  • frontend/src/i18n/zh.ts
  • tests/test_utils.py
  • src/recorder.py
  • frontend/src/pages/OverviewTab.tsx
  • frontend/src/types.ts
  • tests/test_proxy.py
  • VERSION
  • src/database/models.py
  • frontend/src/charts/TopUsageChart.tsx
  • src/schema_migrations.py
  • src/costs.py
  • config.example.yaml
  • README.md
  • frontend/src/model-badge.ts
  • config/app.py
  • tests/test_database.py
  • src/proxy.py
  • config/pricing.py
  • frontend/src/utils.tsx
  • tests/test_recorder.py
  • frontend/src/charts/SparklineTrendPanel.tsx
  • src/database/usage.py
  • frontend/src/pages/LogsPage.tsx
  • src/utils.py
  • tests/test_costs.py

Comment thread tests/test_pricing.py
@Haannbboo
Haannbboo merged commit 1fcb3f5 into main Aug 5, 2026
5 checks passed
@Haannbboo
Haannbboo deleted the fix/otlp-proxy-dedup branch August 5, 2026 01:53
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.

1 participant