Skip to content

feat(llmusage): tenant spend reads the hourly rollup plus the edge hour (W06-4) - #74

Merged
GottZ merged 1 commit into
rootfrom
feat/oai-w06-4
Nov 3, 2026
Merged

GottZ merged 1 commit into
rootfrom
feat/oai-w06-4

Conversation

@GottZ

@GottZ GottZ commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

Wave W06-4 of the OpenAI-compatible API plan (RC-A, telemetry foundation; design/06 §4.7 / §4.5, masterplan K17). No migration, no /v1 surface.

What this changes

  • New leaf package internal/llmusage (imports only the pgxdb handle atoms; depguard rule llmusage-leaf pins that): Window(ctx, q, Spec{Now, Span, APIKeyIDs, Locality}) reads one rolling window as hourly rollup from the first full UTC hour after now − span plus the raw context_llm_log rows of that first, partial hour — the exact semantics of the raw created_at > now() - interval queries it replaces, with at most one hour of raw rows per window. It also returns the earliest relief of each figure (oldest counted row + span), microsecond-exact.
  • backends.QuotaAccountant.spend reads its three figures (24 h calls, 24 h external cost, 30-day rolling external cost) through llmusage.Window instead of a 30-day scan over the hypertable. The filter stays the tenant's knowledge keys (api_key_id): a /v1 consumer row never counts here (K17, B-06-18) — consumer traffic cannot exhaust daily_calls or the cost budget and lock a tenant's people out of /api/query and the web chat. Relief timestamps are stored on the tenant state for W06-14 (no reader yet).
  • Docs: docs/multi-tenancy.md (spend source, asymmetry), docs/operations.md (Migration 159 rollback now coupled to the reader), docs/architecture.md.

Behaviour changes (intended)

  • Spend figures are byte-equal to the old query for the same clock (parity property below); the windows now end at the accountant's clock instead of the database's now() (same host, same clock in production).
  • Three index probes per tenant refresh (one per window) instead of one 30-day scan.

Deviations from design/06 §4.7, with reasons

  • The raw edge always ends at the next full hour, also when since lies exactly on one: the bucket starting at since holds a row stamped exactly at since, which the strict lower bound excludes; the design formula counted such a row twice (90 parity deviations with it, 0 with the fix).
  • Keys are probed one by one (LATERAL … OFFSET 0) instead of api_key_id = ANY($1): with = ANY the planner preferred the time-leading indexes (uq_llm_usage_hourly, the log's created_at index) and read every tenant's rows in the range — under the generic plan the whole instance's edge hour. The locality condition sits in the aggregates' FILTER so both renderings share one access path. Pinned by TestQuotaSpendPlan under the custom and the forced-generic plan.
  • Relief is exact (oldest raw row of the oldest counted rollup hour, one bounded lookup) rather than bucket_start: the bucket variant reports relief too early (11:00 for a row at 11:05:00.000007); a safe conservative variant would have been bucket_start + 1 h.

Consumers of the changed contracts

Changed symbol Callers / readers Covered by
QuotaAccountant.spend (signature, now fills tenantState incl. relief) refresh (quota.go) TestQuotaAccountant_* (backends 80 → 83), TestChatQuota, TestSynthesizeQuota, handler status/quota suites
NewQuotaAccountant(q quotaDB, …) (needs Querier + Rower) cmd/ctxd, tests compile; existing callers pass *pgxpool.Pool
context_llm_usage_hourly (first reader) llmusage.Window TestWindowParity (500 seeds vs the old raw SQL), TestWindowBounds (unit, hour-exact edges), TestQuotaSpendPlan
.golangci.yml depguard CI lint golangci-lint run 0

Overlap search

Open PRs #55/#57/#63/#67 on 2026-11-03: no shared files. Based on #73 (43041709); root unchanged since.

Cost

Per policy tenant every 30 s: three statements, each ≤ 1 h of raw rows through idx_llm_log_apikey plus the rollup through idx_llm_usage_key; measured in TestQuotaSpendPlan: raw rows touched 3–5 (base: 1 441, with no upper bound on created_at and no rollup). Planner edge: on a rollup of a few dozen tenants the custom plan may still pick the time-leading uq_llm_usage_hourly for the 30-day window (≈ 1.5× the probe's cost); from ~100 tenants on it moves to idx_llm_usage_key. Candidate for later: INCLUDE columns on idx_llm_usage_key, one batched statement over all tenants.

Verification

Gates, each red on 43041709 first: (a) existing quota suites green before and after (backends 80/83, chat 1, llm 4, handler 52); (b) parity property llmusage.Window vs the old spend SQL over 500 random seeds with rows around both window edges — 425 deviations without the edge rows, 90 with the design's ceilHour, 0 final; (c) EXPLAIN under custom and generic plan: raw rows 1 441 → 3–5, rollup via idx_llm_usage_key; (d) asymmetry pin: a spend-union variant (OR consumer_id = ANY) counts 5 consumer calls and the gate refuses on daily_calls, the shipped code counts 0 and lets the tenant through, the control with 5 knowledge-key calls refuses; (e) relief with a fixed clock: compile error on base, bucket_start variant 11:00 vs exact 11:05:00.000007.

Suites on the head: go build ./..., go vet -tags=integration ./..., golangci-lint run with and without --build-tags=integration (0), go test -short -race ./... (65 ok), deadcode.sh + deadcode.sh testonly (ok; the new package is reached from spend), integration -p 1 -race for internal/llmusage + internal/backends, and -run 'Quota|Status|LLMLog' for internal/chat, internal/llm, internal/handler. Independent review by a fresh agent during the build: 4 minor + 5 nit, all fixed or documented in the wave report.

Not verified

  • Clock skew between the daemon and the database shifts the windows by that skew (the old query used the database clock); in production both run on one host.
  • The planner edge above is observed at ~30 synthetic tenants, not measured on the live rollup.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Quota Improvements

    • Tenant quota calculations now use rolling 24-hour and 30-day windows for attributed calls and provider-reported costs. Consumer-only activity is excluded from tenant quota totals.
    • Quota status now includes the earliest time counted calls or costs are expected to leave each window.
    • Usage totals combine hourly summaries with raw records from the current partial hour, preserving accurate rolling-window figures.
  • Documentation

    • Updated quota and operations guidance to explain usage windows, deleted-log behavior, and migration rollback considerations.

…ur (W06-4)

New leaf package internal/llmusage. Window(ctx, q, spec) sums one
rolling window of key-attributed context_llm_log rows from the hourly
rollup context_llm_usage_hourly (migration 159) - every hour from the
first full UTC hour after now - span on - plus the raw rows of the
partial hour before it, and reports the earliest relief (oldest counted
row + span) for calls and for cost. The sums read at most one hour of raw
rows whatever the span; each relief the edge cannot answer reads one
more hour.

QuotaAccountant.spend reads its three windows (external cost over 24 h
and 30 d, attributed calls over 24 h) through Window instead of a 30-day
raw scan per policy tenant and refresh. The figures are unchanged; the
filter stays on the tenant's knowledge keys, so rows of /v1 consumers
(consumer_id set, api_key_id NULL) never count against daily_calls or
the cost budget (K17, B-06-18). The three reliefs are kept per tenant in
the generation for the gateway's Retry-After (W06-14); nothing reads
them yet.

Deliberate deviations: the edge ends at the next full hour even when
since lies exactly on one (a row stamped exactly at since is outside
created_at > since but inside the bucket that starts there); each key is
probed on its own behind OFFSET 0 and the locality sits in the
aggregates' FILTER, because = ANY and a locality WHERE let custom and
generic plans pick the time-leading indexes and read every tenant's
rows; the windows end at the accountant's clock, taken once per refresh.

Gates, red then green:
(a) existing suites, base -> after: backends 80 -> 83 (+3 new), chat
    -Quota 1 -> 1, llm -Quota 4 -> 4, handler -Status|Quota 52 -> 52
(b) parity vs the old spend SQL, 500 seeds: overlay without the edge
    425/500 seeds differ, overlay with the design's ceilHour 90/500,
    final 0/500
(c) EXPLAIN of every spend statement, custom and generic plan: base
    touches 1441 raw rows (ceiling 6), created_at unbounded above, no
    rollup; final 3-5 raw rows, idx_llm_log_apikey and idx_llm_usage_key
(d) asymmetry pin: spend-union overlay counts 5 consumer calls and Gate
    refuses with daily_calls; final 0 and Gate passes, control with 5
    key calls refuses
(e) relief: compile error against base; bucket_start overlay reports
    11:00 instead of 11:05:00.000007; final exact to the microsecond

Docs: multi-tenancy.md (spend source, consumer usage never counts,
deleted rows keep counting), operations.md (migration 159 now has a
reader; dropping it needs the revert), architecture.md (the rollup's
reader). Lint: depguard rule llmusage-leaf.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 3, 2026 12:00
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The change adds a reader that combines hourly usage rollups with raw rows from the partial hour. Tenant quota accounting now uses this reader for rolling call and cost totals, and stores earliest-relief timestamps. Tests and documentation cover the reader and its quota integration.

Changes: Tenant usage accounting

Layer / File(s) Summary
Usage window calculation
go/internal/llmusage/window.go, go/internal/llmusage/window_test.go, go/internal/llmusage/window_integration_test.go, .golangci.yml
The new llmusage package calculates rolling totals and relief times from hourly rollups and raw rows in the partial hour. Unit and integration tests cover window bounds, totals, relief times, locality filters, and parity with raw-row queries. The package receives a depguard rule with a restricted import allowlist.
Quota accounting and verification
go/internal/backends/quota.go, go/internal/backends/export_test.go, go/internal/backends/quota_integration_test.go, go/internal/backends/quota_plan_integration_test.go, docs/architecture.md, docs/multi-tenancy.md, docs/operations.md
The quota accountant uses three windows for daily calls, daily external cost, and monthly external cost, and stores their relief times. Tests cover consumer-row exclusion, quota totals, and query plans. The documentation describes the rollup reader, quota behavior, and downgrade guidance.

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

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant QuotaAccountant
  participant llmusage.Window
  participant PostgreSQL
  participant tenantState
  QuotaAccountant->>llmusage.Window: request call and cost windows
  llmusage.Window->>PostgreSQL: query hourly rollups and partial-hour log rows
  PostgreSQL-->>llmusage.Window: return totals and oldest qualifying timestamps
  llmusage.Window-->>QuotaAccountant: return totals and relief times
  QuotaAccountant->>tenantState: store spend totals and relief times
Loading

Possibly related PRs

  • GottZ/ctx#72: Adds the exclusive consumer_id and api_key_id caller identity model used to distinguish consumer rows from tenant-key rows.
  • GottZ/ctx#73: Adds the hourly rollup and trigger that this change reads alongside raw rows from the partial hour.

Suggested reviewers: w4-nerf

Merge Risk: Minimal · up to e0a39

Tenant call and cost quotas now read from hourly rollups plus the partial-hour raw rows, with tests for window boundaries, consumer-row exclusion and query plans. No concrete merge-blocking risk was identified. The one disclosed caveat is that quota windows now end at the daemon clock rather than the database clock.

Security architecture risk: Low · up to e0a39

Tenant attribution and safe publication of refreshed limits are preserved. The main added risk is operational: an incomplete application and database rollback can weaken spending-limit enforcement.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — Failure or corruption of the shared rollup can affect spending-limit accuracy for every configured policy tenant using that database. This is a shared accounting dependency, not a new public authority grant; the identified disruptive rollback operations require database administrative access.

Trust Boundaries and Controls

  • observed — The production caller supplies tenant-resolved knowledge-key IDs rather than accepting a consumer identity as an accounting substitute. SQL parameters carry key lists and locality values. Consumer-only rows cannot satisfy the reader's api_key_id predicates, preventing consumer traffic from exhausting these knowledge-key quotas through this path.

Resilience and Maintainability Implications

  • observed — Partial query failure or cancellation propagates before a new generation is published, retaining the previous policy and spend snapshot. Background refresh remains single-flight and time-bounded. These mechanisms contain transient failures, but do not detect a disabled rollup trigger because stale aggregate reads can still succeed. Cold-cache fail-open behavior is unchanged from the base.

Hardening Proposals

  • proposed — A deployment or maintenance check could validate reader/schema compatibility and continued rollup-trigger operation before schema teardown or application restart. This would reinforce the documented rollback requirement; it is a hardening proposal, not an observed vulnerability.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: tenant spend now reads the hourly rollup plus the partial edge hour.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 30 functions across 7 files. (4 skipped: 4 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 432e238d-f63a-4d36-902e-b67295bb5194
Commits

Reviewing files that changed from the base of the PR and between 4304170 and e0a3906.

Files selected for processing: 11
  • .golangci.yml
  • docs/architecture.md
  • docs/multi-tenancy.md
  • docs/operations.md
  • go/internal/backends/export_test.go
  • go/internal/backends/quota.go
  • go/internal/backends/quota_integration_test.go
  • go/internal/backends/quota_plan_integration_test.go
  • go/internal/llmusage/window.go
  • go/internal/llmusage/window_integration_test.go
  • go/internal/llmusage/window_test.go

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Copilot AI 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.

Note

Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.

🟡 Changes recommended

The change still has an active moderate issue in go/internal/llmusage/window_integration_test.go where tenant key membership is checked via substring matching, risking false positives and making the edge coverage assertion unreliable until replaced with exact set membership.

1 open issue
What changed in this PR

This PR introduces a new rollup-backed LLM usage window reader (internal/llmusage) and refactors tenant quota spend accounting to use hourly rollups plus a bounded “edge hour” of raw context_llm_log rows, aiming to preserve prior window semantics while eliminating per-tenant 30‑day raw scans. It also updates related integration/unit tests, documentation, and depguard rules.

Changes:

  • Add internal/llmusage.Window(...) to compute rolling spend windows from context_llm_usage_hourly plus at most one partial-hour slice of raw logs, including relief timestamps.
  • Refactor internal/backends.QuotaAccountant spend computation to use llmusage.Window for 24h calls, 24h external cost, and 30d external cost windows (tenant knowledge keys only).
  • Add/adjust plan-parity, bounds, and integration tests; update docs and .golangci.yml depguard configuration.
File Review Notes
go/​internal/​llmusage/​window.go Reviewed
go/​internal/​llmusage/​window_test.go Reviewed
go/​internal/​llmusage/​window_integration_test.go Reviewed — Moderate: edgeRows uses substring matching via strings.Contains(strings.Join(keys, \",\"), *r.key) which can produce false positives; replace with exact set-membership.
go/​internal/​backends/​quota.go Reviewed
go/​internal/​backends/​quota_plan_integration_test.go Reviewed — Moderate: walkPlan() undercounts touched rows because it multiplies only Actual Rows by Actual Loops but not Rows Removed by Filter / Rows Removed by Index Recheck (JSON EXPLAIN reports per-loop averages); can let plan regressions slip past the ceiling check.
go/​internal/​backends/​quota_integration_test.go Reviewed
go/​internal/​backends/​export_test.go Reviewed
docs/​operations.md Reviewed
docs/​multi-tenancy.md Reviewed
docs/​architecture.md Reviewed
.golangci.yml Reviewed

Review effort: Balanced

Comment on lines +185 to +197
func edgeRows(sc scenario, keys []string, span time.Duration) int {
since, edgeEnd := llmusage.Bounds(sc.now, span)
n := 0
for _, r := range sc.rows {
if r.key == nil || !strings.Contains(strings.Join(keys, ","), *r.key) {
continue
}
if r.at.After(since) && r.at.Before(edgeEnd) {
n++
}
}
return n
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The keys are full 36-character UUIDs and the joined list is comma-separated, so a substring match across a boundary would need a comma inside a UUID — there is no false positive today. Agreed that an exact set check says what it means; the next wave (W06-5, status rollups on this package) carries the one-line change rather than a comment-only CI round here.

@GottZ
GottZ merged commit 14b9963 into root Nov 3, 2026
23 checks passed
@GottZ
GottZ deleted the feat/oai-w06-4 branch November 3, 2026 12:15
GottZ added a commit that referenced this pull request Nov 3, 2026
…-5) (#75)

Wave W06-5 of the OpenAI-compatible API plan (RC-A, telemetry
foundation; design/06 §4.7). No migration, no `/v1` surface.

## What this changes

- `/api/status` `llm_24h` (server view, `queryLLM24h`) and the
per-tenant `llm_24h` generation (`buildTenantRollupInto`) read the
hourly rollup `context_llm_usage_hourly` from the first full UTC hour
after `now − 24 h` plus the raw `context_llm_log` rows of that first,
partial hour, through two new readers in `internal/llmusage` (`Groups`,
`TenantGroups`), instead of scanning 24 h of raw rows per tick. The
tenant reader joins both parts through `context_api_keys` →
`context_tenant_scopes` exactly as the raw query did; a consumer's row
(`api_key_id` NULL) counts for no tenant.
- `llm_24h_complete` comes from the rollup's `unattributed_ok`
(successful rows without a backend) plus the same count over the edge
rows: a failed row without a backend keeps `complete` true, as before.
- Output is byte-identical to the raw aggregate (golden below); the one
deliberate difference is a deterministic tie order.
- Docs: `docs/operations.md` (rollup readers now quota + status;
rollback coupling), `docs/architecture.md`.

## Behaviour changes (intended)

- Rows with the same `calls` are now ordered by `backend`, then
`pipeline` (the raw query ordered by `calls` alone and left ties to the
plan; live has 16/16 ties).
- Backend and pipeline names longer than 100 characters show cut to 100
— the rollup's dimension width since migration 159 (the longest live
name has 24 characters). Pinned by a test.
- The window ends at the daemon's clock, not the database's `now()`
(same host in production; tests pin the anchor on the boundaries).
- On an error in the middle of the result the server view returns an
empty list and `complete=false` instead of the rows read so far plus
`false`.

## Consumers of the changed contracts

| Changed symbol | Callers / readers | Covered by |
|---|---|---|
| `queryLLM24h`, `buildTenantRollupInto` (same signatures, new source) |
status tick (`assemble`), `SnapshotForTenant`, SSE `/api/events` |
`TestStatusLLM24hGolden` (byte-equal to the base),
`TestStatusLLM24hParity` (300 random seeds vs the raw SQL),
`TestStatusLLM24hPlan`, `TestStatusPerTenantView`,
`TestStatusGoldenKeys`, `TestStatusEventCarriesEveryDualPathField`, 58
handler top-level tests |
| `llmusage.Groups`, `TenantGroups` (new) | the two readers above |
`internal/llmusage` suite (9/9), the handler suites |
| `promptguard` call-site census | `internal/handler:g.Pipeline`
passthrough (a copied string, no prompt) |
`promptguard_callsite_test.go` |

## Overlap search

Open PRs #55/#57/#63/#67 on 2026-11-03: no shared files. Based on #74
(`14b99639`); root unchanged since.

## Cost

Per status tick: the edge is at most one hour of raw rows bounded on
both sides (23 rows in the plan test vs 1 079 for the raw 24 h scan),
the rest through `idx_llm_usage_bucket`; pinned under the custom and the
forced-generic plan. Tenant view: the same two parts joined to the key
and scope tables.

## Verification

Gates, each red on `14b99639` first: (a) existing suites green before
and after (handler 52 → 58, llmusage 9/9); (b) golden: a 16 027-byte
JSON of both views generated on the base tree from the base SQL (only
`now()` → `$1` and the tie-break added; the base functions match it up
to tie order), new code byte-equal (same sha256, empty diff); red on
base by compile error, red against overlays: no edge hour 253/300 seeds,
truncated mean 262/300, no tie-break 298/300; (c) `complete` probe: red
when the rollup read omits `unattributed_ok` and when the edge does not
count it; an error row without a backend keeps `complete` true; (d)
`EXPLAIN` custom + generic: 23 raw rows (exactly the edge) vs 1 079,
rollup via `idx_llm_usage_bucket`; the base SQL fails the same check;
(e) consumer rows absent from the tenant view — pinned, red against an
overlay that credits them to a tenant.

Suites on the head: `go build ./...`, `go vet -tags=integration ./...`,
`golangci-lint run` with and without `--build-tags=integration` (0), `go
test -short -race ./...`, `deadcode.sh` + `deadcode.sh testonly`,
integration `-p 1` for `internal/llmusage` and `-run
'Status|Events|LLMLog|Quota'` for `internal/handler` +
`internal/backends`. Independent review by a fresh agent during the
build: 1 minor (the 100-character cut — documented and pinned), 4 nits
(3 fixed, the error-path change documented above).

## Not verified

- Clock skew between daemon and database shifts the window by that skew
(same host in production).
- The 100-character cut is only observable with names ctx does not
produce today.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Updates**
* Server-wide and tenant-level 24-hour LLM usage summaries now use
hourly usage data alongside records from the current partial hour.
* Summaries indicate when usage data is incomplete because successful
calls could not be attributed.
* Backend and pipeline names are limited to 100 characters in summaries;
names that match after truncation are grouped together.
* **Documentation**
* Updated architecture and operations guidance for usage summaries,
attribution, retries, and the effects of removing or disabling rollup
data collection.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
W4-NERF pushed a commit to W4-NERF/ctx that referenced this pull request Nov 3, 2026
The status page's llm_24h rows - the server view (queryLLM24h) and the
per-tenant generation (buildTenantRollupInto) - no longer scan 24 hours of
context_llm_log on every tick. They read through two new readers of
internal/llmusage, Groups and TenantGroups: the hourly rollup
context_llm_usage_hourly from the first full UTC hour after now - 24 h,
plus the raw rows of the partial hour before it (llmusage.Bounds), each
side aggregated by backend and pipeline (the tenant view by scope as
well) and added in NUMERIC. The edge applies the rollup trigger's
normalisation, so edge and rollup groups meet in one row.
llm_24h_complete is false exactly when unattributed_ok > 0. The tenant
view keeps the join through context_api_keys and context_tenant_scopes,
so a consumer row (api_key_id NULL) counts for no tenant. The error
behaviour is the same, except that a mid-stream server error now returns
no rows instead of the rows read so far.

What changes on the wire (hence feat, docs updated):
- Rows with as many calls are now ordered by backend, then pipeline. The
  raw query ordered by calls alone and left ties to the plan; the base
  code returned the golden seed's ties in another order than the
  tie-broken reference.
- A backend or pipeline name longer than 100 characters shows cut to
  100, the rollup's dimension width. The longest live name has 24.
- The window anchor is the Go clock instead of the database's now().
  Both run on the same host.
For names of up to 100 characters the figures are byte-equal.

Gates (red first, then green):
- (a) Existing suites before and after:
  - -run 'Status|Events|LLMLog' ./internal/handler/: 52 -> 58 top-level
    passes, 0 fail.
  - ./internal/llmusage/: 9 passes, 0 fail.
- (b) Golden: the raw-row SQL of 14b9963 (now() -> $1, plus the
  tie-break) over a mixed seed at 3 anchors wrote
  testdata/llm24h_golden.json (16027 bytes) on the base tree. The base
  functions match it apart from tie order. The rollup readers are
  byte-equal to it (sha256 6ed75ed3, empty diff).
  - Parity property: 300 random seeds, 0 differ.
  - Red against the base: compile error (llm24hNow, llm24hSpan,
    llmusage.Groups undefined).
  - Red against overlays: no edge (golden + 253/300 seeds), truncating
    avg_ms (262/300), no tie-break (298/300).
- (c) complete probe: red against a rollup read without unattributed_ok
  (oc1, rollup-hour case) and an edge without it (oc2, edge case). A
  failed row without backend_name keeps complete true.
- (d) EXPLAIN, custom and generic plan: the raw edge is bounded on both
  sides and touches 23 rows (edge 23, 24 h 1079); the rollup is read via
  idx_llm_usage_bucket. The base SQL fails the same check (1079 rows,
  lower bound only).
- (e) Tenant view without consumer rows: pinned. Red against a
  consumer-union overlay.

Suites: build, vet -tags=integration, go test -short -race ./... (all
ok), golangci-lint in both forms (0 issues), deadcode.sh and deadcode.sh
testonly (ok).

Also:
- The promptguard census lists llm24hRowOf's Pipeline: g.Pipeline as a
  passthrough.
- llmusage edgeRows uses an exact set (Copilot nit on GottZ#74).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants