Skip to content

feat(openrouter): add API usage/credit provider (task 006)#8

Merged
ogrodev merged 2 commits into
mainfrom
task/006-provider-openrouter
Jun 3, 2026
Merged

feat(openrouter): add API usage/credit provider (task 006)#8
ogrodev merged 2 commits into
mainfrom
task/006-provider-openrouter

Conversation

@ogrodev
Copy link
Copy Markdown
Owner

@ogrodev ogrodev commented Jun 3, 2026

Summary

Task 006 — OpenRouter API usage. Read OpenRouter credit/usage with the key stored by task 003 and render it as a normalized credit window in the popover.

  • core (crates/core/src/providers/openrouter.rs): OpenRouterStrategy (API-key) reads the key via the SecretStore port and polls GET /api/v1/key; pure parse_key_info/parse_credits (lossy, ADR 0015) + usage_windows map to one Custom credit window. Prepaid credit has no reset, so the window carries no countdown — stated honestly.
  • The account-wide /credits call is the fallback bound only: fetched solely when the key has no usable spend cap, so a normal capped key no longer makes a recurring (often 403) round-trip on every poll.
  • adapters: openrouter_strategy() builder + hand-run openrouter_live example.
  • app: route openrouter through usage_route/fetch_for; reportsUsage('openrouter'). Status/error mapping (401/403/429/5xx/transport) feeds the existing stale/error UI; data stays siloed to its own tile (no account identity invented).

Acceptance criteria (task 006) — all met

  • Usage/credit with percent used + reset countdown where one applies
  • Auto-refreshes with the rest and on opening the popover
  • Fetch failure → stale/error, last-known retained, others unaffected
  • No reset window stated honestly (no invented countdown)
  • Siloed to its own tile

Review

Ran the packaged thermo review agents before finalizing:

  • thermo-nuclear-review (correctness/security): no blockers, no majors — verified HTTPS-only bearer, no key logging, no credential write-back, lossy/no-panic parsing, correct status mapping, no regression to other providers, correct siloing, 15s per-probe timeout.
  • thermo-nuclear-code-quality-review (maintainability): approve with changes, no blockers.

Fixes applied from the reviews:

  • Gate the /credits call on absence of a usable key cap (avoids a recurring 403 every poll); extracted a shared key_cap predicate so the gate and window selection can't drift.
  • Added coverage: non-positive-cap fall-through, zero-credits fall-through, 5xx + transport error on /key, and an assertion that a capped key skips /credits.
  • is_finite() symmetry on the credits branch; FixedClockFakeClock (sibling convention); validate_key now routes through the shared bearer_get.

Verification

  • cargo test -p mlt-core openrouter — 26 passed
  • make check — green (clippy -D warnings, core-purity, cargo test --workspace, cargo-deny, biome, svelte-check, vitest)
  • make coveragemlt-core 94.97% lines (openrouter.rs 97%)

Summary by cubic

Adds OpenRouter API usage/credit tracking via the stored API key and shows it as a single normalized credit window in the popover. Calls /credits only when the key has no spend cap; operational failures on /credits now surface as errors instead of showing “No spending limit.”

  • New Features

    • OpenRouterStrategy polls GET /api/v1/key for limit/usage; best-effort GET /api/v1/credits only if no per-key cap. Lossy parse_key_info/parse_credits; usage_windows map to one Custom window with percent used and no invented reset.
    • Adapter openrouter_strategy() and openrouter_live example; app wiring routes openrouter via usage_route/fetch_for; reportsUsage('openrouter'); errors (401/403/429/5xx/transport) feed stale/error UI; data stays siloed; no account identity. Meets task 006.
  • Bug Fixes

    • /credits handling: 200 → parsed; 401/403 → graceful fallback; 429 → RateLimited; 5xx/transport → Upstream error. Prevents falsely showing “No spending limit” during outages.

Written for commit f59ddd5. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • OpenRouter provider now fully supported with usage tracking: displays spend cap, usage amount, credit balance, and reset countdown when available.
    • Usage data auto-refreshes within the provider tile with built-in error handling and graceful degradation for failed requests.

Read OpenRouter credit/usage with the key stored by task 003 and render it as
a normalized credit window in the popover (task 006).

- core: OpenRouterStrategy (API-key) reads the key via the SecretStore port and
  polls GET /api/v1/key; pure parse_key_info/parse_credits (lossy, ADR 0015) +
  usage_windows map to one Custom credit window. Prepaid credit has no reset, so
  the window carries no countdown (stated honestly).
- The account-wide /credits call is only the fallback bound: it is fetched solely
  when the key has no usable spend cap, so a normal capped key no longer makes a
  recurring (often 403) round-trip on every poll.
- adapters: openrouter_strategy() builder + hand-run openrouter_live example.
- app: route `openrouter` through usage_route/fetch_for; reportsUsage('openrouter').
  Status/error mapping (401/403/429/5xx/transport) feeds the existing stale/error
  UI; data stays siloed to its own tile (no account identity invented).

Tests: 26 core unit tests (lossy parsing; window selection incl. non-positive
cap and zero-credits fall-through; 100% clamp; no-reset invariant; bearer header;
capped key skips /credits; status mapping). make check green.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR completes OpenRouter usage reading by implementing a full fetch strategy that polls key spending caps and account credits, wiring it through the adapter layer with keychain-backed secret storage, integrating it into the Tauri app's usage routing and dispatch, updating frontend state to track OpenRouter as a reporting provider, and marking all acceptance criteria as complete in the documentation.

Changes

OpenRouter usage fetching and integration

Layer / File(s) Summary
Core usage-reading logic and parsers
crates/core/src/providers/openrouter.rs
KeyInfo and Credits types parse OpenRouter API responses; parse_key_info (strict) and parse_credits (best-effort) handle decoding; usage_windows computes clamped percentages and selects between key-specific caps and account-wide prepaid credits; validate_key refactored to use shared bearer_get helper.
OpenRouterStrategy implementation with comprehensive tests
crates/core/src/providers/openrouter.rs
OpenRouterStrategy implements FetchStrategy, reading secrets, calling /api/v1/key and conditionally /api/v1/credits, mapping errors including 429 rate limiting, and building snapshots. Test infrastructure (scripted HTTP, secret stubs, deterministic clock) plus suites validating parsing, usage-window selection/clamping, and fetch end-to-end behaviors.
Adapter entry point and public module exports
crates/adapters/src/openrouter.rs, crates/adapters/src/lib.rs, crates/adapters/examples/openrouter_live.rs
openrouter_strategy() instantiates OpenRouterStrategy with keychain secrets, Reqwest HTTP, and system clock; module and re-export declarations expose the adapter; live example demonstrates environment-variable-backed secret store usage.
Tauri app routing and dispatch for OpenRouter usage
src-tauri/src/lib.rs
UsageRoute::OpenRouter variant added; openrouter_usage() builds fetch context; usage_route() maps provider id "openrouter" to new route; fetch_for() dispatcher invokes strategy; routing test updated to recognize "openrouter" provider.
Frontend state tracking for OpenRouter usage reporting
src/lib/usageState.ts, src/lib/usageState.test.ts
reportsUsage() includes "openrouter" as a usage-reporting provider; tests updated to expect "Connecting…" UI state on initial fetch and reportsUsage('openrouter') to return true.
Documentation status and acceptance criteria updates
docs/PRD.md, docs/tasks/006-provider-openrouter.md
OpenRouter marked complete in PRD provider coverage; task documentation updated to reflect completion of usage display, auto-refresh, error handling with retained values, and reset countdown honesty acceptance criteria.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ogrodev/MLT#4: Both PRs evolve OpenRouter support in crates/core/src/providers/openrouter.rs, progressing from validate_key alone to a full OpenRouterStrategy with usage/credits fetching, new parsing/math logic, and comprehensive testing.

Poem

🐰 A router once lived in the code so dark,
With secrets in keychains and usage to mark,
Now credits and caps dance in windows so fine,
From API to snapshot, the data does align!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 and concisely summarizes the main change: adding OpenRouter as an API usage/credit provider, with task reference. It accurately reflects the primary scope of the changeset across all modified files.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/006-provider-openrouter

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@crates/core/src/providers/openrouter.rs`:
- Around line 214-223: The code currently collapses any failure from
self.http.send(bearer_get(...)) into None, which hides real fetch/transport
errors; change the credits acquisition to surface operational failures as
FetchError while only mapping an explicit management-key refusal to None.
Specifically, update the logic around the credits variable (the
key_cap(key_info.limit) branch and the match on self.http.send) so that: on
Ok(resp) with status 200 call parse_credits and return Some(parsed); on Ok(resp)
with a management-key refusal status (e.g., 403/401) return None; on any other
non-200 status or transport error return a FetchError variant (propagate the
error) so callers like usage_windows() can mark the provider Stale/Error. Ensure
you reuse parse_credits, bearer_get, and the existing FetchError type to
represent surfaced failures rather than returning None for all errors.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed75c9e4-8834-4a89-8594-3058426ab1ca

📥 Commits

Reviewing files that changed from the base of the PR and between d8df605 and 8b4ae75.

📒 Files selected for processing (9)
  • crates/adapters/examples/openrouter_live.rs
  • crates/adapters/src/lib.rs
  • crates/adapters/src/openrouter.rs
  • crates/core/src/providers/openrouter.rs
  • docs/PRD.md
  • docs/tasks/006-provider-openrouter.md
  • src-tauri/src/lib.rs
  • src/lib/usageState.test.ts
  • src/lib/usageState.ts

Comment thread crates/core/src/providers/openrouter.rs
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

Re-trigger cubic

…ing "no limit"

The credits fallback collapsed every non-200 (and transport) outcome into None,
so a 5xx or network failure on /api/v1/credits rendered a confident "No spending
limit" (Status::Ok) — hiding a real outage and, for a paying no-cap key, asserting
a balance state we never actually read.

Now only the expected management-key refusal degrades gracefully:
- 200            -> parse_credits (best-effort Option, unchanged)
- 401 | 403      -> None (normal inference key can't read /credits; fall back)
- 429            -> FetchError::RateLimited
- other / 5xx    -> FetchError::Upstream; transport errors propagate via `?`

Genuine failures now surface as Stale/Error (last-known retained) per ADR 0015.
Reuses bearer_get/parse_credits/FetchError. Adds tests for the 5xx and transport
cases; the 403-refusal and 200-balance paths are unchanged.
@ogrodev ogrodev merged commit 9322f5e into main Jun 3, 2026
5 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.

1 participant