feat: implement api endpoints to know usage per ip/s #143

Merged
nabil_salah merged 1 commit from develop_usage_api into main 2026-06-07 13:26:59 +00:00
Member

The request_logs table already had an ip column (indexed) and captured
model / tokens / cost per request — but every row was written with an empty
ip
(ip: String::new() at all billing sites), and there was no endpoint
to query usage per IP
. So we could see total and per-model spend, but not
which caller spent how much.

This wires up the missing piece: attribute each request to its source IP (the
Mycelium IPv6 when reached over the overlay) and expose it as a queryable,
admin-gated API.

What changed

  • Capture the caller IP at the TCP accept loop (main.rs): the peer
    address (previously discarded as let (stream, _)) is stamped into request
    extensions via a new ClientIp type / extractor (middleware/client_ip.rs).
    The UDS accept loop is unchanged — same-host calls have no peer address and
    fall back to "unknown".
  • Thread it through to the billing rows: added CallContext.client_ip,
    populated in the 5 REST handlers (chat, embedding, tts, stt,
    rerank), and written into request_logs.ip at every persist site
    (streaming + non-streaming + error paths).
  • Query methods (middleware/request_log.rs): usage_by_ip(since, until)
    and usage_for_ip(ip, since, until) (GROUP BY aggregations), plus
    IpUsage / ModelUsage result types. Enabled WAL so the read-heavy
    usage queries don't block request logging.
  • Two REST endpoints (api_openrpc/mod.rs), gated by the existing
    admin_auth middleware
    (same gate as admin /rpc; open in dev-mode when
    no ADMIN_TOKEN / HERO_SECRET is set):
Endpoint Returns
GET /billing/usage every IP — requests, tokens, total cost (USD), first/last seen — ranked by spend
GET /billing/usage/{ip} one IP — per-model breakdown + totals

Both accept optional ?since=&until= (unix-epoch-second bounds on
started_at).

Example

curl http://[<mycelium-ip>]:33850/billing/usage \
  -H "Authorization: Bearer $ADMIN_TOKEN"
# {"data":[{"ip":"...","requests":1,"input_tokens":40,"output_tokens":3,
#           "total_tokens":43,"total_cost_usd":0.00002597,
#           "first_seen":...,"last_seen":...}]}

Testing

  • cargo clippy -p hero_aibroker_server -- -D warnings — clean.
  • cargo test — 94 passing, incl. 3 new aggregation tests
    (usage_by_ip_aggregates_and_ranks_by_spend,
    usage_by_ip_respects_time_bounds,
    usage_for_ip_breaks_down_by_model).
  • Manual: ran the broker on a TCP listener, fired a chat, confirmed
    GET /billing/usage returns the caller IP with correct cost, and verified
    the raw row (request_logs.ip = 127.0.0.1).

Out of scope / notes

  • No X-Forwarded-For resolution. Source IP = TCP peer, which is correct
    for the direct-Mycelium deployment. Fronting with a reverse proxy (e.g.
    hero_proxy) would need an XFF path gated on trusted_proxy_ips.
  • Rate limiting still uses its existing shared bucket (not switched to per-IP).
  • Billing DB path unchanged (CWD-relative request_logs.db).
The `request_logs` table already had an `ip` column (indexed) and captured model / tokens / cost per request — but every row was written with an **empty `ip`** (`ip: String::new()` at all billing sites), and there was **no endpoint to query usage per IP**. So we could see total and per-model spend, but not *which caller spent how much*. This wires up the missing piece: attribute each request to its source IP (the Mycelium IPv6 when reached over the overlay) and expose it as a queryable, admin-gated API. ## What changed - **Capture the caller IP** at the TCP accept loop (`main.rs`): the peer address (previously discarded as `let (stream, _)`) is stamped into request extensions via a new `ClientIp` type / extractor (`middleware/client_ip.rs`). The UDS accept loop is unchanged — same-host calls have no peer address and fall back to `"unknown"`. - **Thread it through to the billing rows**: added `CallContext.client_ip`, populated in the 5 REST handlers (`chat`, `embedding`, `tts`, `stt`, `rerank`), and written into `request_logs.ip` at every persist site (streaming + non-streaming + error paths). - **Query methods** (`middleware/request_log.rs`): `usage_by_ip(since, until)` and `usage_for_ip(ip, since, until)` (`GROUP BY` aggregations), plus `IpUsage` / `ModelUsage` result types. Enabled **WAL** so the read-heavy usage queries don't block request logging. - **Two REST endpoints** (`api_openrpc/mod.rs`), gated by the **existing `admin_auth` middleware** (same gate as admin `/rpc`; open in dev-mode when no `ADMIN_TOKEN` / `HERO_SECRET` is set): | Endpoint | Returns | |---|---| | `GET /billing/usage` | every IP — requests, tokens, total cost (USD), first/last seen — ranked by spend | | `GET /billing/usage/{ip}` | one IP — per-model breakdown + totals | Both accept optional `?since=&until=` (unix-epoch-second bounds on `started_at`). ## Example ```bash curl http://[<mycelium-ip>]:33850/billing/usage \ -H "Authorization: Bearer $ADMIN_TOKEN" # {"data":[{"ip":"...","requests":1,"input_tokens":40,"output_tokens":3, # "total_tokens":43,"total_cost_usd":0.00002597, # "first_seen":...,"last_seen":...}]} ``` ## Testing - `cargo clippy -p hero_aibroker_server -- -D warnings` — clean. - `cargo test` — 94 passing, incl. 3 new aggregation tests (`usage_by_ip_aggregates_and_ranks_by_spend`, `usage_by_ip_respects_time_bounds`, `usage_for_ip_breaks_down_by_model`). - Manual: ran the broker on a TCP listener, fired a chat, confirmed `GET /billing/usage` returns the caller IP with correct cost, and verified the raw row (`request_logs.ip = 127.0.0.1`). ## Out of scope / notes - **No `X-Forwarded-For` resolution.** Source IP = TCP peer, which is correct for the direct-Mycelium deployment. Fronting with a reverse proxy (e.g. `hero_proxy`) would need an XFF path gated on `trusted_proxy_ips`. - Rate limiting still uses its existing shared bucket (not switched to per-IP). - Billing DB path unchanged (CWD-relative `request_logs.db`).
feat: implement api endpoints to know usage per ip/s
All checks were successful
Build and Test / build (pull_request) Successful in 16m39s
3c8c131a52
Signed-off-by: Nabil-Salah <nabil.salah203@gmail.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_aibroker!143
No description provided.