Skip to content

feat(usage): self-service usage and limit status endpoint#498

Merged
SantiagoDePolonia merged 5 commits into
mainfrom
feat/self-usage-endpoint
Jul 8, 2026
Merged

feat(usage): self-service usage and limit status endpoint#498
SantiagoDePolonia merged 5 commits into
mainfrom
feat/self-usage-endpoint

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Closes #466.

What

Adds GET /v1/usage so callers can read their own usage, budget, and rate limit status with the same API key they use for inference — no admin access needed. This is the building block for tools like OpenCode plugins that want to show consumption and remaining budget.

User-visible behavior

The response is scoped to the caller's effective user path — the path bound to the managed API key, or the X-GoModel-User-Path header for master-key callers (/ when omitted):

{
  "user_path": "/team/alpha",
  "server_time": "2026-07-06T17:00:00Z",
  "usage": { "start_date": "2026-06-07", "end_date": "2026-07-06", "total_requests": 128, "total_tokens": 91234, "total_cost": 1.73, ... },
  "budgets": [ { "user_path": "/team/alpha", "period_label": "monthly", "amount": 100, "spent": 41.2, "remaining": 58.8, "exceeded": false, ... } ],
  "rate_limits": [ { "user_path": "/team/alpha", "period_label": "minute", "max_requests": 60, "requests_used": 3, "requests_remaining": 57, ... } ]
}
  • usage: summary over a date window (start_date/end_date/days query params, default last 30 days UTC, 365-day cap — same semantics as the dashboard). null when no storage backend records usage.
  • budgets: every budget covering the path, including ones inherited from ancestor paths, with spent/remaining/period bounds and an exceeded flag mirroring enforcement (budgets without usage never block).
  • rate_limits: live counters for user-path rules covering the path (subtree match). Provider/model rules are excluded — they describe shared infrastructure capacity, not the consumer.
  • Polling the endpoint does not consume rate limit quota.
  • Missing subsystems degrade gracefully (empty arrays / null); genuine store failures return 503.

Design

Per-user-path (not per-key) is the identity on purpose: usage entries, budgets, and rate limits are all keyed by user path, and a future users system maps naturally to user → user_path, keeping this endpoint stable. Deliberate tradeoff: ancestor budgets (including a global / budget) are shown with full spent/remaining to any covered key holder, since "when will I be cut off" is exactly what the caller needs.

Implementation

  • New non-enforcing status methods: budget.Service.StatusesForPath (evaluates all matching budgets, no early return at the first exceeded one) and ratelimit.Service.StatusesForUserPath.
  • The server reaches them via optional type-assertion upgrades of the existing BudgetChecker/RateLimiter fields, so enforcement-only fakes and extensions keep working.
  • The user-path header is read explicitly by the handler: request snapshots only capture it for ingress-managed model routes, which /v1/usage deliberately is not.
  • Shared plumbing: date-range parsing moved from admin to usage.BuildDateRange (now used by both), and the usage reader is created once in app wiring and shared by the server and admin handlers.
  • Docs: user-path feature page section, endpoint reference row, swagger/openapi regen. The regen also picks up the /admin/rate-limits endpoints from feat(ratelimit): scoped request, token, and concurrency limits (user paths, providers, models) #482 that were never regenerated into the checked-in docs — that's the unrelated-looking part of docs/openapi.json / cmd/gomodel/docs/docs.go.

Testing

  • Unit tests for both new service methods and full-server handler tests (managed key path binding, master-key header scoping, auth required, invalid dates/header, degraded and error paths).
  • Verified end-to-end against a running gateway: master key + header, managed key with bound path seeing an ancestor budget and its own env-seeded rate limit rule, 401/400 paths, and window params.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added self-service GET /v1/usage to return your usage totals plus applicable budget and rate-limit status, scoped to your effective user path, with a configurable UTC date window.
  • Bug Fixes
    • Improved date-window parsing/validation (UTC boundaries, sensible defaults, and clamping to a maximum range) for consistent behavior.
  • Documentation
    • Updated OpenAPI and advanced/feature documentation to describe the new endpoint, response fields, and error/status semantics.

Add GET /v1/usage so callers can read their own usage, budget, and rate
limit status without admin access (#466). The response is scoped to the
caller's effective user path: the path bound to the managed API key, or
the X-GoModel-User-Path header for master-key callers.

- usage summary over a date window (start_date/end_date/days, default
  last 30 days UTC), null when no storage backend records usage
- status of every budget covering the path (spent/remaining/period/
  exceeded), including budgets inherited from ancestor paths
- live counters for every user-path rate limit rule covering the path;
  provider/model rules are excluded as shared infrastructure
- polling the endpoint does not consume rate limit quota

New non-enforcing service methods back the endpoint:
budget.Service.StatusesForPath evaluates all matching budgets without
stopping at the first exceeded one, and
ratelimit.Service.StatusesForUserPath reports user-path rules by
subtree. The server reaches them through optional type-assertion
upgrades of the existing enforcement interfaces.

Shared plumbing: the date-range parser moved from admin to
usage.BuildDateRange, and the usage reader is created once in app
wiring and shared by the server and admin handlers. Swagger regen also
picks up the admin rate-limit endpoints (#482) that were never
regenerated into the checked-in docs.

Closes #466

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mintlify

mintlify Bot commented Jul 6, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 6, 2026, 9:55 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a self-service GET /v1/usage endpoint for caller-scoped usage, budget, and rate-limit status. It also adds shared date-range parsing, new budget/rate-limit status helpers, app and server wiring, and matching OpenAPI/Swagger plus documentation updates.

Changes

Usage status feature

Layer / File(s) Summary
Shared date-range parsing
internal/usage/date_range.go, internal/admin/handler.go, internal/admin/handler_usage.go, internal/usage/date_range_test.go
Adds shared usage date-range defaults and parsing helpers, then routes admin date-range handling through them.
Budget and rate-limit status queries
internal/budget/service.go, internal/budget/service_test.go, internal/ratelimit/service.go, internal/ratelimit/service_test.go
Adds path-scoped status lookup methods plus tests for exhaustive budget reporting and subtree-filtered rate-limit status.
App and server wiring
internal/app/app.go, internal/server/handlers.go, internal/server/http.go
Adds usage summarizer wiring, handler dependencies, and the /v1/usage route.
UsageStatus handler and tests
internal/server/usage_status_handler.go, internal/server/usage_status_handler_test.go
Implements the usage-status response flow and covers user-path, date-window, and error handling cases.
Docs and API specs
CLAUDE.md, docs/advanced/api-endpoints.mdx, docs/features/user-path.mdx, docs/advanced/usage-api.mdx, docs/docs.json, docs/openapi.json, cmd/gomodel/docs/docs.go
Documents the endpoint and extends generated specs and schemas for usage, budgets, and rate limits.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler as Handler.UsageStatus
  participant Usage as UsageSummarizer
  participant Budget as budget.Service
  participant RateLimit as ratelimit.Service

  Client->>Handler: GET /v1/usage
  Handler->>Handler: resolve userPath and usage window
  Handler->>Usage: GetSummary(params)
  Usage-->>Handler: UsageSummary
  Handler->>Budget: StatusesForPath(userPath, now)
  Budget-->>Handler: []CheckResult
  Handler->>RateLimit: StatusesForUserPath(userPath, now)
  RateLimit-->>Handler: []Status
  Handler-->>Client: 200 usageStatusResponse
Loading

Possibly related PRs

  • ENTERPILOT/GoModel#189: The new GET /v1/usage endpoint and per-path budget status methods build on the hierarchical user_path semantics introduced there.
  • ENTERPILOT/GoModel#479: This PR extends audit/schema fields with tokens_saved, which matches the rewrite-savings additions here.
  • ENTERPILOT/GoModel#482: The new endpoint depends on the rate-limit status plumbing added there.

Poem

I hopped to fetch the numbers neat,
With paths and budgets in one seat.
The windowed days now flow just right,
And limits gleam in morning light.
Thump thump, the usage story sings,
A tiny route with helpful things. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a self-service usage and limit-status endpoint.
Linked Issues check ✅ Passed The PR delivers the requested endpoint for users to view their own usage and status information without admin access.
Out of Scope Changes check ✅ Passed The changes stay focused on the usage/status endpoint, wiring, schemas, docs, and tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 feat/self-usage-endpoint

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.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

Mostly safe to merge after fixing the explicit date-window cap.

The endpoint uses existing auth and user-path scoping, and the new service status methods match existing enforcement rules. One contained bug remains: explicit date ranges can bypass the documented 365-day limit and drive oversized usage queries.

internal/usage/date_range.go

T-Rex T-Rex Logs

What T-Rex did

  • The Go harness reproduced a missing explicit usage date range cap by calling usage.BuildDateRange with start_date 2020-01-01 and end_date 2026-01-01, and it returned nil error for a 2193-day range, bypassing the 365-day MaxDateRangeDays limit.
  • Captured and preserved the reproduction artifacts: the Go harness source and the harness execution output that demonstrate the oversized range was accepted without error.
  • Ran and compared TestUsageStatus before and after the endpoint change; the after-change run executed all TestUsageStatus tests and passed.
  • Observed endpoint responses for the usage endpoints after the change, including 200 OK for valid paths, 401 Unauthorized for unauthenticated access, 400 Bad Request for invalid inputs, and 503 Service Unavailable for store/provider failures.

View all artifacts

T-Rex Ran code and verified through T-Rex

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant Auth as Auth middleware
participant Handler as GET /v1/usage handler
participant Usage as UsageSummarizer
participant Budget as Budget service
participant RateLimit as RateLimit service

Client->>Auth: GET /v1/usage + bearer token
Auth->>Auth: Resolve master key or managed key
Auth->>Handler: Context includes managed user_path when bound
Handler->>Handler: Resolve effective user_path and date window
Handler->>Usage: GetSummary(user_path, start_date, end_date)
Usage-->>Handler: Usage summary or nil
Handler->>Budget: StatusesForPath(user_path, now)
Budget-->>Handler: Applicable budget statuses
Handler->>RateLimit: StatusesForUserPath(user_path, now)
RateLimit-->>Handler: User-path rate-limit counters
Handler-->>Client: usageStatusResponse
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant Client
participant Auth as Auth middleware
participant Handler as GET /v1/usage handler
participant Usage as UsageSummarizer
participant Budget as Budget service
participant RateLimit as RateLimit service

Client->>Auth: GET /v1/usage + bearer token
Auth->>Auth: Resolve master key or managed key
Auth->>Handler: Context includes managed user_path when bound
Handler->>Handler: Resolve effective user_path and date window
Handler->>Usage: GetSummary(user_path, start_date, end_date)
Usage-->>Handler: Usage summary or nil
Handler->>Budget: StatusesForPath(user_path, now)
Budget-->>Handler: Applicable budget statuses
Handler->>RateLimit: StatusesForUserPath(user_path, now)
RateLimit-->>Handler: User-path rate-limit counters
Handler-->>Client: usageStatusResponse
Loading

Reviews (2): Last reviewed commit: "docs(usage): add dedicated Usage API pag..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@docs/features/user-path.mdx`:
- Around line 90-122: Update the “Self-service usage and limits” section in
user-path.mdx to document two missing behaviors for the `/v1/usage` endpoint:
polling it does not consume rate-limit quota, and `rate_limits` only returns
user_path-scoped rules. Add concise one-line bullets near the existing
usage/rate-limit explanation so callers like OpenCode-style pollers know they
can check frequently and understand why provider/model-scoped throttles are
absent. Keep the wording aligned with the existing descriptions of `/v1/usage`,
`usage`, and `rate_limits`.

In `@internal/app/app.go`:
- Around line 491-505: `usageReader` is not being cleared when `usage.NewReader`
fails, so a typed-nil can still slip through to `serverCfg.UsageSummarizer`.
Update the `usageReader` setup in `app.go` to mirror the `pricingRecalculator`
nil-reset pattern: in the `usage.NewReader` error path, explicitly set
`usageReader` back to nil before the later `if usageReader != nil` check. Use
the `usage.NewReader`, `usageReader`, and `serverCfg.UsageSummarizer` symbols to
locate the fix.

In `@internal/budget/service.go`:
- Around line 236-269: StatusesForPath currently drops already computed
CheckResult values when evaluateBudget returns an error, unlike CheckWithResults
which preserves partial results. Update the error path in
Service.StatusesForPath to return the accumulated results alongside the error,
and keep its loop/accumulator behavior aligned with CheckWithResults so both
methods handle partial failures consistently.

In `@internal/server/usage_status_handler_test.go`:
- Around line 192-204: Add a test that covers the documented 365-day cap for the
usage endpoint, since TestUsageStatusRejectsInvalidDates only checks malformed
and inverted ranges. Update internal/server/usage_status_handler_test.go by
adding a case around getUsageStatus with a fakeUsageSummarizer and Config that
requests an oversized window (for example days=400 or a start_date/end_date span
beyond 365 days) and assert the handler still returns OK while clamping the
translated range to the maximum window. Use the existing test helpers and
summarize parameters captured by fakeUsageSummarizer to verify the capped dates.

In `@internal/server/usage_status_handler.go`:
- Around line 170-188: `usageStatusWindow` currently ignores malformed or
non-positive `days` values and falls back to `usage.DefaultDateRangeDays`, which
makes validation inconsistent with `start_date` and `end_date`. Update the
`days` parsing branch in `usageStatusWindow` to return an error when
`c.QueryParam("days")` is present but cannot be parsed as a positive integer, so
the handler can respond with 400 instead of silently using the default window;
keep the existing `usage.BuildDateRange` flow for valid input.

In `@internal/usage/date_range.go`:
- Around line 1-68: The new shared date-range utility in BuildDateRange and
NormalizeDateRangeDays has no dedicated tests, so add a table-driven
date_range_test.go covering both bounds provided, only start, only end, neither
bound, start-after-end, invalid YYYY-MM-DD formats, and day clamping/defaulting
behavior. Make sure the tests exercise the exported helpers in
internal/usage/date_range.go so regressions in the reused /v1/usage and admin
dashboard date-window logic are caught.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: fb3bd537-2267-445d-a9d0-1028f1608a96

📥 Commits

Reviewing files that changed from the base of the PR and between f157d01 and ea705d5.

📒 Files selected for processing (17)
  • CLAUDE.md
  • cmd/gomodel/docs/docs.go
  • docs/advanced/api-endpoints.mdx
  • docs/features/user-path.mdx
  • docs/openapi.json
  • internal/admin/handler.go
  • internal/admin/handler_usage.go
  • internal/app/app.go
  • internal/budget/service.go
  • internal/budget/service_test.go
  • internal/ratelimit/service.go
  • internal/ratelimit/service_test.go
  • internal/server/handlers.go
  • internal/server/http.go
  • internal/server/usage_status_handler.go
  • internal/server/usage_status_handler_test.go
  • internal/usage/date_range.go

Comment thread docs/features/user-path.mdx Outdated
Comment on lines +90 to +122
## Self-service usage and limits

Callers can check their own consumption without admin access:

```bash
curl http://localhost:8080/v1/usage \
-H "Authorization: Bearer sk_gom_..."
```

The response covers the caller's effective user path: recorded usage over a
date window (default: the last 30 days, UTC), plus the status of every budget
and rate limit rule gating that path.

```json
{
"user_path": "/team/alpha",
"server_time": "2026-07-06T17:00:00Z",
"usage": { "start_date": "2026-06-07", "end_date": "2026-07-06", "total_requests": 128, "total_tokens": 91234, "total_cost": 1.73, ... },
"budgets": [ { "user_path": "/team/alpha", "period_label": "monthly", "amount": 100, "spent": 41.2, "remaining": 58.8, "exceeded": false, ... } ],
"rate_limits": [ { "user_path": "/team/alpha", "period_label": "minute", "max_requests": 60, "requests_used": 3, "requests_remaining": 57, ... } ]
}
```

- With a managed API key, the key's bound `user_path` is used; the endpoint
needs no extra parameters, so tools like OpenCode plugins can poll it with
the same key they use for inference.
- With the master key (or in unsafe mode), pass `X-GoModel-User-Path` to pick
the path, or omit it to see `/` (everything).
- `start_date`, `end_date` (YYYY-MM-DD), and `days` query parameters narrow
the usage window (365-day maximum).
- `usage` is `null` when no storage backend records usage; `budgets` and
`rate_limits` list only rules that cover the path, including ones inherited
from ancestor paths such as `/`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document two behaviors from the PR objectives that are missing here.

This section doesn't mention that:

  1. Polling /v1/usage does not itself consume rate-limit quota — worth stating explicitly so callers (e.g., OpenCode-style pollers) don't hesitate to check status frequently.
  2. rate_limits only includes user_path-scoped rules; provider/model-scoped rate limit rules are excluded — otherwise a caller could reasonably expect provider throttling to show up here and be confused by its absence.

Both are one-line additions that materially change how the endpoint should be interpreted.

📝 Suggested doc addition
 - `usage` is `null` when no storage backend records usage; `budgets` and
   `rate_limits` list only rules that cover the path, including ones inherited
   from ancestor paths such as `/`.
+- `rate_limits` includes only `user_path`-scoped rules; provider- and
+  model-scoped rate limit rules are not reported here.
+- Polling this endpoint does not consume rate limit quota.
As per coding guidelines: "Documentation should be concise, practical, user-focused, and include defaults plus minimal examples when useful. Document when users should override defaults and explain the impact of configuration changes in user-facing docs."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Self-service usage and limits
Callers can check their own consumption without admin access:
```bash
curl http://localhost:8080/v1/usage \
-H "Authorization: Bearer sk_gom_..."
```
The response covers the caller's effective user path: recorded usage over a
date window (default: the last 30 days, UTC), plus the status of every budget
and rate limit rule gating that path.
```json
{
"user_path": "/team/alpha",
"server_time": "2026-07-06T17:00:00Z",
"usage": { "start_date": "2026-06-07", "end_date": "2026-07-06", "total_requests": 128, "total_tokens": 91234, "total_cost": 1.73, ... },
"budgets": [ { "user_path": "/team/alpha", "period_label": "monthly", "amount": 100, "spent": 41.2, "remaining": 58.8, "exceeded": false, ... } ],
"rate_limits": [ { "user_path": "/team/alpha", "period_label": "minute", "max_requests": 60, "requests_used": 3, "requests_remaining": 57, ... } ]
}
```
- With a managed API key, the key's bound `user_path` is used; the endpoint
needs no extra parameters, so tools like OpenCode plugins can poll it with
the same key they use for inference.
- With the master key (or in unsafe mode), pass `X-GoModel-User-Path` to pick
the path, or omit it to see `/` (everything).
- `start_date`, `end_date` (YYYY-MM-DD), and `days` query parameters narrow
the usage window (365-day maximum).
- `usage` is `null` when no storage backend records usage; `budgets` and
`rate_limits` list only rules that cover the path, including ones inherited
from ancestor paths such as `/`.
## Self-service usage and limits
Callers can check their own consumption without admin access:
🤖 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 `@docs/features/user-path.mdx` around lines 90 - 122, Update the “Self-service
usage and limits” section in user-path.mdx to document two missing behaviors for
the `/v1/usage` endpoint: polling it does not consume rate-limit quota, and
`rate_limits` only returns user_path-scoped rules. Add concise one-line bullets
near the existing usage/rate-limit explanation so callers like OpenCode-style
pollers know they can check frequently and understand why provider/model-scoped
throttles are absent. Keep the wording aligned with the existing descriptions of
`/v1/usage`, `usage`, and `rate_limits`.

Source: Coding guidelines

Comment thread internal/app/app.go
Comment thread internal/budget/service.go
Comment thread internal/server/usage_status_handler_test.go
Comment thread internal/server/usage_status_handler.go
Comment on lines +1 to +68
package usage

import (
"time"

"gomodel/internal/core"
)

const (
// DefaultDateRangeDays is the usage window applied when a query gives no
// explicit range.
DefaultDateRangeDays = 30
// MaxDateRangeDays caps a requested usage window.
MaxDateRangeDays = 365
)

// BuildDateRange resolves an inclusive [start, end] day range from optional
// YYYY-MM-DD strings. When only one bound is given the other defaults (start:
// 30 days before end, end: today); when neither is given the range covers the
// last days days ending today.
func BuildDateRange(startStr, endStr string, days int, location *time.Location, today time.Time) (time.Time, time.Time, error) {
var start, end time.Time
var startParsed, endParsed bool

if startStr != "" {
t, err := time.ParseInLocation("2006-01-02", startStr, location)
if err != nil {
return time.Time{}, time.Time{}, core.NewInvalidRequestError("invalid start_date format, expected YYYY-MM-DD", nil)
}
start = t
startParsed = true
}
if endStr != "" {
t, err := time.ParseInLocation("2006-01-02", endStr, location)
if err != nil {
return time.Time{}, time.Time{}, core.NewInvalidRequestError("invalid end_date format, expected YYYY-MM-DD", nil)
}
end = t
endParsed = true
}

if startParsed || endParsed {
if !startParsed {
start = end.AddDate(0, 0, -(DefaultDateRangeDays - 1))
}
if !endParsed {
end = today
}
} else {
days = NormalizeDateRangeDays(days)
end = today
start = today.AddDate(0, 0, -(days - 1))
}

if start.After(end) {
return time.Time{}, time.Time{}, core.NewInvalidRequestError("start_date must be on or before end_date", nil)
}
return start, end, nil
}

// NormalizeDateRangeDays clamps days to [1, MaxDateRangeDays], defaulting to
// DefaultDateRangeDays when not positive.
func NormalizeDateRangeDays(days int) int {
if days <= 0 {
return DefaultDateRangeDays
}
return min(days, MaxDateRangeDays)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No dedicated tests for the new shared date-range utility.

This new package centralizes date-window logic reused by the self-service /v1/usage endpoint and the admin dashboard, but no date_range_test.go is included. Given the edge-case bug above, table-driven tests covering both-given/one-given/neither-given branches, boundary clamping, and invalid formats would help catch regressions.

As per coding guidelines: "Add or update tests for behavior changes, especially request translation, response normalization, error handling, default configuration, and provider-specific parameter mapping."

🤖 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 `@internal/usage/date_range.go` around lines 1 - 68, The new shared date-range
utility in BuildDateRange and NormalizeDateRangeDays has no dedicated tests, so
add a table-driven date_range_test.go covering both bounds provided, only start,
only end, neither bound, start-after-end, invalid YYYY-MM-DD formats, and day
clamping/defaulting behavior. Make sure the tests exercise the exported helpers
in internal/usage/date_range.go so regressions in the reused /v1/usage and admin
dashboard date-window logic are caught.

Source: Coding guidelines

Full GET /v1/usage walkthrough (quick example, path selection, field
reference, date window, errors) under Advanced, linked from the endpoint
reference and the user-path page, whose inline section now defers to it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +55 to +58
if start.After(end) {
return time.Time{}, time.Time{}, core.NewInvalidRequestError("start_date must be on or before end_date", nil)
}
return start, end, nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Missing range cap
BuildDateRange never enforces MaxDateRangeDays after explicit start_date and end_date parsing, so GET /v1/usage?start_date=2020-01-01&end_date=2026-01-01 is accepted even though the endpoint documents a 365-day cap. Because /v1/usage passes caller-supplied dates directly into usage aggregation, any authenticated key can trigger oversized historical scans instead of receiving a bounded window.

Context Used: CLAUDE.md (source)

Artifacts

Repro: Go harness source calling BuildDateRange with an oversized explicit range

  • Contains supporting evidence from the run (text/x-go; charset=utf-8).

Repro: harness execution output showing the 2193-day range was accepted with nil error

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

SantiagoDePolonia and others added 2 commits July 7, 2026 13:26
Review follow-ups on #498:

- BuildDateRange now rejects explicit start_date/end_date ranges longer
  than 365 days (Greptile P1: the documented cap only clamped the days
  param, so any key could request unbounded historical scans). AddDate
  keeps the check correct across DST transitions.
- A malformed or non-positive days value returns 400 instead of
  silently falling back to the default window, matching the
  start_date/end_date validation on the same endpoint.
- app wiring resets the usage reader to nil on constructor errors so a
  typed-nil can never reach the UsageSummarizer nil check.
- budget.StatusesForPath returns partial results alongside the error,
  matching its sibling CheckWithResults.
- Table-driven tests for the shared date-range helper, handler tests
  for the cap and days validation, docs and swagger updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jul 7, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 89.42308% with 22 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/app/app.go 0.00% 18 Missing ⚠️
internal/budget/service.go 90.90% 1 Missing and 1 partial ⚠️
internal/server/usage_status_handler.go 98.26% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@SantiagoDePolonia

Copy link
Copy Markdown
Contributor Author

@coderabbitai continue

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@SantiagoDePolonia I don't see a specific pending task or question queued up in this thread beyond the Codecov report — there's no in-progress plan or review I was blocked on. I'll go ahead and continue by running a fresh review of the PR to pick up where things left off.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Ecosystem research showed the one idiom every consumer-facing limits
surface converged on (Codex, Claude Code, OpenCode) is percent-of-window
plus time-to-reset. Expose both as derived fields so status displays
need no client-side math or clock-skew handling:

- budgets: usage_ratio (spent/amount, unclamped like the admin API) and
  resets_in_seconds counting down to period_end
- rate limits: requests_usage_ratio / tokens_usage_ratio per limited
  dimension (in-flight feeds the concurrent ratio), an exhausted flag
  mirroring admission, and resets_in_seconds to window_end (omitted for
  concurrent rules)

Relative resets are computed against server_time, so clients can render
countdowns without trusting their own clocks. Also covers the error
branches of StatusesForPath/StatusesForUserPath flagged by codecov.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@internal/server/usage_status_handler_test.go`:
- Around line 147-224: TestUsageStatusDerivedFields only covers zero remaining
values, so add a new assertion case for a negative overshoot remaining value to
lock in the corrected Exhausted behavior. Extend the existing setup in
TestUsageStatusDerivedFields around the fakeRateLimiterWithStatus/status checks
to include a rule where RequestsRemaining or TokensRemaining is below zero, then
verify the derived response from usage status still marks the rule exhausted and
preserves the expected usage ratios.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 3a58e6b7-bcb5-40da-9eb6-6283d1b2e96e

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff5d3a and fce8326.

📒 Files selected for processing (7)
  • cmd/gomodel/docs/docs.go
  • docs/advanced/usage-api.mdx
  • docs/openapi.json
  • internal/budget/service_test.go
  • internal/ratelimit/service_test.go
  • internal/server/usage_status_handler.go
  • internal/server/usage_status_handler_test.go

Comment on lines +147 to +224
func TestUsageStatusDerivedFields(t *testing.T) {
maxRequests := int64(10)
maxTokens := int64(100)
requestsLeft := int64(7)
tokensLeft := int64(0)
concurrentMax := int64(2)
concurrentLeft := int64(0)
now := time.Now().UTC()
windowEnd := now.Add(45 * time.Second)
periodEnd := now.Add(90 * time.Minute)

budgets := &fakeBudgetStatusChecker{results: []budget.CheckResult{{
Budget: budget.Budget{UserPath: "/team", PeriodSeconds: budget.PeriodDailySeconds, Amount: 10},
PeriodStart: periodEnd.Add(-24 * time.Hour),
PeriodEnd: periodEnd,
Spent: 12,
HasUsage: true,
Remaining: -2,
}}}
limiter := &fakeRateLimiterWithStatus{statuses: []ratelimit.Status{
{
Rule: ratelimit.Rule{Scope: ratelimit.ScopeUserPath, Subject: "/team", PeriodSeconds: 60, MaxRequests: &maxRequests, MaxTokens: &maxTokens},
WindowStart: windowEnd.Add(-time.Minute),
WindowEnd: windowEnd,
RequestsUsed: 3,
RequestsRemaining: &requestsLeft,
TokensUsed: 120,
TokensRemaining: &tokensLeft,
},
{
Rule: ratelimit.Rule{Scope: ratelimit.ScopeUserPath, Subject: "/team", PeriodSeconds: ratelimit.PeriodConcurrent, MaxRequests: &concurrentMax},
InFlight: 2,
RequestsRemaining: &concurrentLeft,
},
}}

rec, body := getUsageStatus(t, &Config{BudgetChecker: budgets, RateLimiter: limiter}, "/v1/usage", nil)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200 (body: %s)", rec.Code, rec.Body.String())
}

if len(body.Budgets) != 1 {
t.Fatalf("budgets = %d, want 1", len(body.Budgets))
}
b := body.Budgets[0]
if b.UsageRatio != 1.2 {
t.Fatalf("budget usage_ratio = %v, want 1.2 (unclamped)", b.UsageRatio)
}
if b.ResetsInSeconds <= 85*60 || b.ResetsInSeconds > 90*60 {
t.Fatalf("budget resets_in_seconds = %d, want ~90 minutes", b.ResetsInSeconds)
}

if len(body.RateLimits) != 2 {
t.Fatalf("rate_limits = %d, want 2", len(body.RateLimits))
}
windowed, concurrent := body.RateLimits[0], body.RateLimits[1]
if windowed.RequestsUsageRatio == nil || *windowed.RequestsUsageRatio != 3.0/10.0 {
t.Fatalf("windowed requests_usage_ratio = %v, want 0.3", windowed.RequestsUsageRatio)
}
if windowed.TokensUsageRatio == nil || *windowed.TokensUsageRatio != 1.2 {
t.Fatalf("windowed tokens_usage_ratio = %v, want 1.2 (unclamped)", windowed.TokensUsageRatio)
}
if !windowed.Exhausted {
t.Fatal("windowed rule with zero tokens remaining must be exhausted")
}
if windowed.ResetsInSeconds == nil || *windowed.ResetsInSeconds <= 0 || *windowed.ResetsInSeconds > 45 {
t.Fatalf("windowed resets_in_seconds = %v, want within (0, 45]", windowed.ResetsInSeconds)
}
if concurrent.RequestsUsageRatio == nil || *concurrent.RequestsUsageRatio != 1.0 {
t.Fatalf("concurrent requests_usage_ratio = %v, want 1.0 (from in-flight)", concurrent.RequestsUsageRatio)
}
if !concurrent.Exhausted {
t.Fatal("concurrent rule at capacity must be exhausted")
}
if concurrent.ResetsInSeconds != nil || concurrent.TokensUsageRatio != nil {
t.Fatalf("concurrent rule resets/tokens ratio = %v/%v, want both omitted", concurrent.ResetsInSeconds, concurrent.TokensUsageRatio)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for negative/overshoot remaining once Exhausted is fixed.

TestUsageStatusDerivedFields exercises RequestsRemaining/TokensRemaining at exactly 0, but not negative overshoot values. Once the Exhausted strict-equality issue in usage_status_handler.go is addressed, add a case with a negative remaining value to lock in the corrected behavior.

🤖 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 `@internal/server/usage_status_handler_test.go` around lines 147 - 224,
TestUsageStatusDerivedFields only covers zero remaining values, so add a new
assertion case for a negative overshoot remaining value to lock in the corrected
Exhausted behavior. Extend the existing setup in TestUsageStatusDerivedFields
around the fakeRateLimiterWithStatus/status checks to include a rule where
RequestsRemaining or TokensRemaining is below zero, then verify the derived
response from usage status still marks the rule exhausted and preserves the
expected usage ratios.

@SantiagoDePolonia SantiagoDePolonia merged commit e0eb0ee into main Jul 8, 2026
20 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.

Endpoint so users can get their own usage (limit/status)

2 participants