Commit d518218
authored
feat(llm-gateway): report org credit-bucket spend on the usage endpoint (#71404)
## Problem
PostHog Code is cutting over to usage-based billing, and the desktop app wants to show real org spend ("used $12.40 of your $50 limit") in the titlebar and on the Plan & usage page. Today `GET /v1/usage/{product}` carries only per-user percentages against internal rate valves plus an `ai_credits.exhausted` boolean, so clients have nothing to render a dollar amount from. The numbers already exist in `organization.usage`, synced from billing.
## Changes
- `GET /api/projects/{id}/quota_limits/` now returns `usage` and `limit` per resource, read from the org's synced billing numbers. `usage` is the same `usage + todays_usage` sum the quota limiter compares against the limit. The `limited` boolean stays authoritative for gating (grace periods and refund offsets live only in the limiting decision). Both fields are null when billing hasn't synced the resource.
- The gateway's quota resolver parses those numbers, converts credit buckets to USD (1 credit = $0.01) and carries them through its Redis cache. Fail-open paths keep them as null, meaning unknown, never $0.
- `GET /v1/usage/{product}` exposes them as `ai_credits.used_usd` and `ai_credits.limit_usd`. Additive and optional, so old clients are unaffected.
The per-user burst/sustained buckets still expose only percentages (`test_response_has_no_usd_fields` continues to guard that). The valve limits stay internal; the org's own bucket spend is the user's billing data.
> [!NOTE]
> The wire change is additive on both hops. Old gateway against new Django ignores the new keys; new gateway against old Django reads null and reports unknown.
## How did you test this code?
- Gateway: full unit suite passes locally (1240 tests). New regressions covered that no existing test did: Django responses without numbers read as unknown rather than $0, USD numbers survive the Redis cache round trip (a one-sided cache would flash the numbers in and out between hit and miss), fail-open cache entries carry null numbers, and the usage endpoint forwards the resolver's numbers onto `ai_credits`.
- Django: extended `test_quota_limits.py` with a synced-org case (1500 + 200 credits of 2000 reports usage 1700, a synced-but-unlimited bucket reports a null limit, a never-synced resource reports nulls, not zeros). I could not run the Django suite in this sandbox; CI covers it. Ruff (pinned version) is clean.
- mypy on the gateway files adds no new errors (two pre-existing ones in `quota_resolver.py` are untouched).
## Automatic notifications
- [ ] Publish to changelog?
- [ ] Alert Sales and Marketing teams?
## 🤖 Agent context
**Autonomy:** Human-driven (agent-assisted)
Authored by Claude (PostHog Code cloud session) at Adam's request, as the backend half of the usage-based billing client work in [PostHog/code#3487](PostHog/code#3487), which wants to render org spend for both free-tier and subscribed orgs. Main decision: Django reports raw native units generically for every resource, and the credits-to-USD conversion lives in the gateway where the client contract is defined, rather than special-casing credit buckets in Django. Absent-means-unknown is preserved end to end so a resolver blip or unsynced org never renders as $0 spent.
---
*Created with [PostHog Code](https://posthog.com/code?ref=pr)*1 parent a9bbb06 commit d518218
7 files changed
Lines changed: 193 additions & 16 deletions
File tree
- ee/api
- test
- services
- llm-gateway
- src/llm_gateway
- api
- services
- tests
- mcp/src/api
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
30 | 58 | | |
31 | 59 | | |
32 | 60 | | |
| |||
62 | 90 | | |
63 | 91 | | |
64 | 92 | | |
65 | | - | |
66 | | - | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
67 | 98 | | |
68 | 99 | | |
69 | 100 | | |
70 | 101 | | |
71 | 102 | | |
| 103 | + | |
| 104 | + | |
72 | 105 | | |
73 | | - | |
74 | | - | |
75 | 106 | | |
76 | 107 | | |
77 | 108 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | | - | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
74 | 97 | | |
75 | 98 | | |
76 | 99 | | |
77 | 100 | | |
78 | 101 | | |
79 | | - | |
| 102 | + | |
80 | 103 | | |
81 | 104 | | |
82 | 105 | | |
83 | 106 | | |
84 | 107 | | |
85 | 108 | | |
86 | | - | |
| 109 | + | |
87 | 110 | | |
88 | 111 | | |
89 | 112 | | |
| |||
102 | 125 | | |
103 | 126 | | |
104 | 127 | | |
105 | | - | |
| 128 | + | |
106 | 129 | | |
107 | 130 | | |
108 | 131 | | |
| |||
177 | 200 | | |
178 | 201 | | |
179 | 202 | | |
180 | | - | |
181 | | - | |
| 203 | + | |
| 204 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
40 | 44 | | |
41 | 45 | | |
42 | 46 | | |
| |||
139 | 143 | | |
140 | 144 | | |
141 | 145 | | |
142 | | - | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
143 | 151 | | |
144 | 152 | | |
145 | 153 | | |
| |||
Lines changed: 30 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
62 | 67 | | |
63 | 68 | | |
64 | 69 | | |
65 | 70 | | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
66 | 86 | | |
67 | 87 | | |
68 | 88 | | |
| |||
187 | 207 | | |
188 | 208 | | |
189 | 209 | | |
| 210 | + | |
| 211 | + | |
190 | 212 | | |
191 | 213 | | |
192 | 214 | | |
| |||
202 | 224 | | |
203 | 225 | | |
204 | 226 | | |
| 227 | + | |
| 228 | + | |
205 | 229 | | |
206 | 230 | | |
207 | 231 | | |
| |||
212 | 236 | | |
213 | 237 | | |
214 | 238 | | |
215 | | - | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
216 | 245 | | |
217 | 246 | | |
218 | 247 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
| 226 | + | |
| 227 | + | |
226 | 228 | | |
227 | 229 | | |
228 | 230 | | |
229 | 231 | | |
230 | 232 | | |
231 | 233 | | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
232 | 286 | | |
233 | 287 | | |
234 | 288 | | |
| |||
328 | 382 | | |
329 | 383 | | |
330 | 384 | | |
331 | | - | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
332 | 391 | | |
333 | 392 | | |
334 | 393 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
378 | 378 | | |
379 | 379 | | |
380 | 380 | | |
381 | | - | |
| 381 | + | |
382 | 382 | | |
383 | 383 | | |
384 | 384 | | |
| |||
404 | 404 | | |
405 | 405 | | |
406 | 406 | | |
407 | | - | |
| 407 | + | |
408 | 408 | | |
409 | 409 | | |
410 | 410 | | |
| |||
420 | 420 | | |
421 | 421 | | |
422 | 422 | | |
423 | | - | |
| 423 | + | |
424 | 424 | | |
425 | 425 | | |
426 | 426 | | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
427 | 444 | | |
428 | 445 | | |
429 | 446 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
0 commit comments