Skip to content

feat: add single-model pricing endpoint and containing-match fallback - #65

Merged
Haannbboo merged 3 commits into
mainfrom
feat/pricing-single-model-api
Aug 1, 2026
Merged

feat: add single-model pricing endpoint and containing-match fallback#65
Haannbboo merged 3 commits into
mainfrom
feat/pricing-single-model-api

Conversation

@Haannbboo

Copy link
Copy Markdown
Owner

Summary

This PR changes high-risk area(s): cost/token accounting and provider/model normalization.

  • Adds GET /pricing/{model:path} to look up resolved pricing for a single model, mirroring the resolution used at record time.
  • Refactors resolve_model_cost into resolve_cost_match, adding a containing-name fallback: mimo-v2.5-pro resolves to LiteLLM's openrouter/xiaomi/mimo-v2.5-pro. On multiple containing matches, the cheapest combined rate wins.
  • Accepts LiteLLM responses-mode pricing entries so GPT Pro/Codex models resolve from the catalog.
  • Removes now-redundant manual model pricing from config.example.yaml (covered by LiteLLM).
  • Adds /pricing/ to the Vite dev proxy.

Why / Context

Manual pricing config listed many models LiteLLM already prices, and LiteLLM prefixes many models with vendor names (e.g. openrouter/xiaomi/mimo-v2.5-pro), so exact name matching missed them. Users need a per-model pricing lookup that matches what billing actually uses.

How It Works

  • src/costs.py::resolve_cost_match priority: provider exact → global exact → provider containing (cheapest) → global containing (cheapest). Containing matches compare only the last path segment (key.rsplit("/",1)[-1] == model) to avoid family/variant collisions (gpt-5 vs gpt-5-mini).
  • GET /pricing/{model:path} returns model (the matched key), resolved, base + effective rates, source (litellm/yaml), scope (global or provider name), multiplier; or resolved: false with zero rates when nothing matches. Optional provider= query applies that provider's overrides/multiplier.

Design Decisions

  • Decision: Containing fallback selects the cheapest across all vendor keys.
    Why: requested behavior.
    Trade-off: a bare model name with no exact price now bills at the cheapest matching vendor's price instead of $0 (previously silent). This is an estimate heuristic, not per-request token weighting.

Manual QA

  • GET /pricing/mimo-v2.5-promodel: openrouter/xiaomi/mimo-v2.5-pro, $1.0/$3.0/$0.2
  • GET /pricing/mimo-v2.5model: openrouter/xiaomi/mimo-v2.5, $0.4/$2.0/$0.08
  • GET /pricing/z-ai/glm-5.1-20260406 → exact yaml override
  • GET /pricing/unknown-modelresolved: false, zero rates
  • /pricing/ (empty) → 422
  • Verified live against the restarted service

Testing

  • uv run python -m pytest -q: pass — 669 passed
  • uv run ruff format --check + ruff check: pass
  • uv run python -m mypy src/: pass
  • pre-commit run --files <touched files>: pass (includes full pytest hook)
  • cd frontend && node --test tests/vite-api-proxy-routes-regression.test.mjs: pass
  • cd frontend && npm test: 45 pre-existing failures unrelated to this change (confirmed identical with the proxy change stashed)

Risk / Rollout / Rollback

  • Risk: containing fallback changes record-time billing for previously-unpriced bare model names (from $0 to cheapest containing vendor). Exact matches and all existing precedence behavior unchanged.
  • Rollout: no schema changes; runtime config re-fetch picks up the new parser behavior.
  • Rollback: revert this commit; prior resolve_model_cost behavior restored.

Data / Privacy Impact

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

Cost / Provider / Schema Impact

  • Cost/token accounting changed: yes — containing fallback for unknown bare model names (see Design Decisions)
  • Provider normalization changed: yes — containing fallback + responses mode acceptance
  • Streaming/tool-call behavior changed: no
  • Migration/backfill required: no

Review

  • Independent code review completed before commit: yes
  • Must-fix review findings resolved: yes (ruff-format, mypy narrowing; also dropped dead match_type field, added global-exact-beats-provider-containing test)
  • Standards checked against AGENTS.md, .agents/commands/llm-tracker.md, and .agents/commands/pre-pr.md: yes

Known Limitations / Follow-ups

  • Containing fallback picks the cheapest across all vendors, not per-request token-weighted; documented as an estimate.
  • No recompute of historical usage rows; pricing changes apply to new records only.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Haannbboo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e8f35d33-557d-4b70-a52e-a773f211b950

📥 Commits

Reviewing files that changed from the base of the PR and between ff499f1 and 6701b22.

📒 Files selected for processing (5)
  • config/app.py
  • config/models.py
  • src/api.py
  • src/costs.py
  • tests/test_costs.py
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added model-specific pricing lookup with provider-aware rates, fallback matching, multipliers, and resolution details.
    • Pricing lookup now supports LiteLLM response-mode models and model identifiers containing slashes.
    • Added proxy support for nested pricing routes.
  • Bug Fixes

    • Improved pricing resolution for exact, case-insensitive, provider-specific, and lowest-cost matches.
    • Unrecognized models now return clear zero-cost metadata.
  • Chores

    • Updated the project version to 0.2.2.
    • Simplified example pricing configuration.

Walkthrough

The PR adds model-specific pricing resolution, cost-match precedence rules, provider multipliers, and a GET /pricing/{model:path} endpoint. The frontend proxy forwards nested pricing routes. Pricing configuration and responses model parsing are updated.

Changes

Pricing resolution flow

Layer / File(s) Summary
Pricing sources and cost matching
VERSION, config.example.yaml, config/pricing.py, src/costs.py, tests/test_costs.py, tests/test_pricing.py
Cost resolution now supports exact and containing matches with provider and global precedence. The resolver selects the cheapest containing match and returns match metadata. responses models are recognized and parsed.
Single-model pricing API
src/api.py, tests/test_pricing.py
GET /pricing/{model:path} resolves configured and remote pricing, applies provider multipliers, and returns source, scope, effective costs, and resolution status. Tests cover precedence, matching, unresolved models, and validation.
Frontend pricing proxy
frontend/vite-api-proxy.js, frontend/tests/vite-api-proxy-routes-regression.test.mjs
The Vite proxy forwards exact and nested /pricing routes. Regression tests cover both route forms.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ViteApiProxy
  participant PricingAPI
  participant CostResolver
  Client->>ViteApiProxy: Request /pricing/{model}
  ViteApiProxy->>PricingAPI: Forward pricing request
  PricingAPI->>CostResolver: Resolve configured and remote costs
  CostResolver-->>PricingAPI: Match metadata or unresolved result
  PricingAPI-->>ViteApiProxy: Pricing response
  ViteApiProxy-->>Client: Return pricing data
Loading

Poem

A rabbit hops through pricing keys,
Finds exact costs beneath the trees.
Provider paths and models align,
The proxy carries each design.
“Responses” joins the model queue—
And version two-point-two shines too!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% 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 single-model pricing endpoint and containing-match fallback, which are the main changes.
Description check ✅ Passed The description directly explains the endpoint, pricing-resolution changes, configuration updates, testing, and rollout impact.
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 feat/pricing-single-model-api

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: 1

🧹 Nitpick comments (1)
src/api.py (1)

947-1011: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the provider-multiplier lookup into a shared helper.

get_model_pricing (lines 970-974) re-implements the same provider-multiplier resolution already present in get_pricing (lines 921-927): read config_snapshot["providers"][provider], then extract price_multiplier with an isinstance guard. Both endpoints compute the same value with slightly different code shapes.

Extract a small helper, for example _resolve_provider_multiplier(config_snapshot, provider) -> float, and call it from both endpoints. This keeps the multiplier logic in one place and prevents the two pricing endpoints from silently diverging if this logic changes later.

♻️ Proposed refactor
+def _resolve_provider_multiplier(config_snapshot: dict, provider: str | None) -> float:
+    if provider is None:
+        return 1.0
+    provider_config = config_snapshot.get("providers", {}).get(provider, {})
+    if not isinstance(provider_config, dict):
+        return 1.0
+    return float(provider_config.get("price_multiplier", 1.0))
+
+
 `@app.get`("/pricing/{model:path}")
 async def get_model_pricing(model: str, provider: str | None = None):
     ...
-    multiplier = 1.0
-    if provider is not None:
-        provider_config = config_snapshot.get("providers", {}).get(provider, {})
-        if isinstance(provider_config, dict):
-            multiplier = float(provider_config.get("price_multiplier", 1.0))
+    multiplier = _resolve_provider_multiplier(config_snapshot, provider)
🤖 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/api.py` around lines 947 - 1011, Extract the duplicated provider
multiplier lookup into a shared _resolve_provider_multiplier(config_snapshot,
provider) helper, preserving the existing provider dictionary and isinstance
guard with a default multiplier of 1.0. Replace the local multiplier logic in
both get_pricing and get_model_pricing with calls to this helper.
🤖 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 `@src/costs.py`:
- Around line 41-53: Update _find_containing and the resolve_model_cost fallback
path to avoid scanning every pricing-map entry on each lookup. Build or reuse an
index keyed by each key’s trailing model segment, preserving exact-match
precedence and selecting the lowest-cost matching vendor entry; apply the same
indexed lookup to both provider and global maps.

---

Nitpick comments:
In `@src/api.py`:
- Around line 947-1011: Extract the duplicated provider multiplier lookup into a
shared _resolve_provider_multiplier(config_snapshot, provider) helper,
preserving the existing provider dictionary and isinstance guard with a default
multiplier of 1.0. Replace the local multiplier logic in both get_pricing and
get_model_pricing with calls to this helper.
🪄 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: 27090c83-9c66-4801-8c08-02442dc6f834

📥 Commits

Reviewing files that changed from the base of the PR and between 4194b2e and ff499f1.

📒 Files selected for processing (9)
  • VERSION
  • config.example.yaml
  • config/pricing.py
  • frontend/tests/vite-api-proxy-routes-regression.test.mjs
  • frontend/vite-api-proxy.js
  • src/api.py
  • src/costs.py
  • tests/test_costs.py
  • tests/test_pricing.py

Comment thread src/costs.py Outdated
@Haannbboo
Haannbboo merged commit 87afe2b into main Aug 1, 2026
5 checks passed
@Haannbboo
Haannbboo deleted the feat/pricing-single-model-api branch August 1, 2026 03: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.

1 participant