v0.0.1-beta.5
Pre-release[v0.0.1-beta.5] - 2026-08-26
Breaking
-
Changing or resetting a password now revokes all of that user's existing JWTs immediately (they get 401 and must log in again) (spec 2026-08-25-jwt-password-revocation-design); previously tokens stayed valid until their natural ≤24 h expiry. Mechanism:
usersgains atoken_versioncolumn (default 0; column ADD auto-applies at startup migrate),Issuestamps the current version into the JWT (verclaim) at login/registration, and both/api/user(RequireJWT) and the admin JWT path (adminAuth) reject when the in-memory user snapshot's version differs from the claim — no Redis denylist, no refresh tokens. Both password-write paths (self-service change-password and email-code reset) bump the version in a single atomic statement plus the standard invalidate+NOTIFY pair. Existing tokens keep working after upgrade until each user's first password change (missingverdecodes to 0 = DB default). -
Email verification codes moved from PostgreSQL to Redis (spec 2026-08-25-emailcode-redis-migration): the
email_codestable is removed (internal/entregenerated; a leftoveremail_codestable on an old database is dead data — not dropped, not migrated, per the fresh-setup policy). Codes now live as one Redis HASH per(purpose, email)underc3api:emailcode:<purpose>:<email>(internal/verification, injected viaService.SetEmailCodeStore) with a native TTL replacing theexpires_atcolumn, so an expired code now answers "code invalid" instead of "code expired" (same 400 sentinel class, handler contract unchanged). Unconsumed codes do not survive a restart (compose Redis keepsappendonly = no) — users simply request a new code; resend rate limiting (60 s window) is preserved via theupdated_atfield. -
Redis is now a required dependency alongside PostgreSQL (spec 2026-08-25-redis-foundation-design):
[redis].addris mandatory (empty = startup fatal; unreachable at startup = fatal via a fail-fast Ping), withC3API_REDIS_ADDR/C3API_REDIS_PASSWORD/C3API_REDIS_DBenv overrides and placeholder-password rejection reusing the existing secret check. Upgrading = provisioning a Redis instance (redis:8-alpinejoins the compose stack as a required service). Redis carries only discardable ephemeral coordination state — never a cache layer or system of record. -
The manual
cluster.instancessetting is removed (spec 2026-08-25-redis-instance-discovery-design): multi-instance budget sharing now auto-discovers the live instance count via Redis ZSET heartbeats (internal/discovery, 1 s tick / 15 s member TTL with clock-skew margin, graceful-stop ZREM for immediate scale-down; on Redis outage the last count freezes instead of failing closed). The admin Settings "Cluster" tab is gone. -
InputTokens ledger semantics changed for OpenAI-family upstreams (chat / responses REST+WS / codex):
usage_logs.input_tokensnow carries billable input (wire value =input_tokens − cached_tokens; OpenAI semantics have cached ⊆ input — previously the cached-hit portion was double-billed at both the input rate and the cache-read rate); cached reads continue to be billed separately viacache_read_tokens× CacheReadPerM. Historical rows keep the old semantics (no backfill — Beta has no migration path);total_tokensis numerically unchanged everywhere (quota deduction and stats totals unaffected); Anthropic upstreams are unaffected (theircache_readis not part ofinput_tokensto begin with). -
Billing ledger rewritten as a cursor consumer over
usage_logs: the in-memory billing queue (which lost 4.16 M ledger rows under load — usage logs survived, deductions never happened) is gone.usage_logsgains abilledboolean with a partial index (WHERE NOT billed) that acts as the durable cursor; the usage flusher is now the table's single writer and stamps each row's fate at birth (billing.capture=falseor anonymous requests are born absorbed). The billing worker sweeps unbilled rows in batches (session-scoped advisory lock, per-user grouped concurrent deduction, poison-row quarantine, zero-cost bulk marking) with crash-safe exactly-once semantics: deduction + marking commit atomically, uncommitted work replays. Settlement executes as per-lane set-based SQL statements (balance lane / FEFO temp lane / zero-cost sweep) instead of per-user round trips; measured sustained drain 11 k+ ledger rows/s on the reference storm (vs ~100 rows/s for the retired in-memory queue path), and the legacy per-group deduction surface has been fully retired. -
Ops overview alerts contract changed: the billing trio is now
billing_lag_ms/billing_unbilled_rows/billing_quarantined_rows(replacingbilling_pending_waterline_ms, which measured an in-memory queue depth that no longer exists). -
Pricing storage unified: the three price tables (
pricing/image_prices/function_prices) are retired in favor of the two-tableprice_entries+price_variantsmodel; the admin endpointsGET /api/admin/pricing,GET /api/admin/image-prices,GET /api/admin/function-pricesand theirPUT/DELETEcounterparts are removed and consolidated intoGET /api/admin/prices(list filterspage/page_size/mode/source/model/sort/order) plusGET|PUT|DELETE /api/admin/prices/entry?model=andGET|PUT|DELETE /api/admin/prices/variants?model=(modelas a required query parameter);POST /pricing/syncstays and gains a preview counterpartPOST /pricing/sync/preview;above_thresholdtiered-pricing semantics are retired in favor of whole-entry variant switching (first match wins); resp-detect requests carrying images but no per-image price now bill as pure tokens (previously the whole request zeroed out asno_price). -
Stats API redesigned around bounded query shapes:
GET /api/admin/stats(unbounded raw-bucket dump) is removed and replaced byGET /api/admin/stats/trend(time series),/stats/top(entity leaderboard),/stats/entity-trend(single-entity drill-down) and/stats/ttft(TTFT percentile card — histogram sketch platform-wide, exactpercentile_contwhen entity-filtered)./api/user/statsnow returns aggregated trend points instead of raw buckets (optionalmodelfilter gained), and/api/user/stats/ttftis new. Storage:usage_statsslimmed to hour × group × model keys (account/template/user/is_error dimensions dropped,is_errordemoted to a column) and a new daily-partitionedusage_entity_statshourly rollup now backs every entity-scoped view.
Added
-
Cross-instance concurrency consensus for all three limit layers (user / API-key / upstream account): each instance admits against a local share
max(1, floor(limit/N))with N auto-discovered via Redis heartbeats; overflow borrows through a cluster view built from per-instance in-flight reports reconciled every 500 ms into a locally-cached snapshot. The request path stays 100 % local memory — zero Redis round-trips, and N=1 short-circuits mathematically (no code path touches Redis at all). Redis outages degrade fail-open to pre-consensus per-instance behavior instead of rejecting traffic; measured drift under 2× oversubscription stays within the documented N × tick-window bound. -
Ops observability for the two concurrency-view sync workers (
conc-sync/account-conc-sync:last_tick_ok/consecutive_errors/tracked_entries) — the fail-open degradation above is silent by design, so these counters are its only visible trace; worker cards on the ops page now show localized display names. -
Redis infrastructure (foundation for the required-dependency model):
pkg/redisxis the repo-wide sole client construction point (Open= construct + fail-fast Ping; no command-level wrapper by design),internal/configgains a strictly-validated[redis]section, andinternal/discoveryimplements ZSET-membership instance discovery wired as a worker (registered between billFlusher and listener/authSync so graceful shutdown removes the member before the final billing-cursor sweep, observable viaGET /api/admin/ops/workersasinstances/last_tick_ok/consecutive_errors). Future consumers on the roadmap: JWT revocation denylist, email verification codes (both since shipped — see above). -
Billing lag observability: worker stats expose cursor lag (
LagMs/UnbilledRows/QuarantinedRows/LastCycleUnixMs) with a guardrail warning when unpaid backlog exceeds 80 % of the retention window; benchmark-adapted deduction path sustains ~50 k ledger rows/s (mark step ~98 k rows/s measured). -
Email service: registration email verification codes and password reset by emailed code — SMTP relay configured through runtime settings (
mail.*, admin console → Settings → Mail tab, disabled by default; TLS defaults to implicit SMTPS on port 465), editable email templates with built-in English defaults and fallback. Delivery runs on a dedicated background mail worker (bounded queue, 3-attempt retry with backoff) observable via/api/admin/ops/workers. -
Admin settings page reorganized into category tabs (signup / defaults / pricing sync / tier policy / cluster / mail), with a two-column Mail tab covering SMTP config and template editors; new user-facing pages for register code entry and forgot-password flow.
-
Loadtest tooling for full-surface hammering:
-mode api-admin(26 weighted scenarios incl. the new stats shapes, redemption codes handed off via-codes-out) and-mode api-user(JWT pool + code redemption,-codes-in),-format images,-api-reads-only, plus a fake-upstream images endpoint.
Changed
- Statistics reads are fully pushed down to PostgreSQL: dashboards aggregate via
GROUP BY date_truncserver-side instead of pulling whole dimension cubes into gateway memory (the old endpoint materialized up to millions of rows per call and was OOM-killed at 33.6 GB under load). Measured on the high-cardinality stress dataset: stats endpoints 24–27 s average → hundreds of ms, stats-related live heap 20 GB (87.7 % of total) → ~5 MB (<0.1 %), process RSS now plateaus at the known stream watermark. - Entity-scoped views (user self-service stats, account drill-down, leaderboards) are served from the new hourly entity rollup — row count scales with active entities, not request volume; exact TTFT percentiles are computed from raw logs only within entity-filtered windows, while platform-wide cards read the retained per-bucket histogram sketch (~2 ms over 24 M samples).
- Admin/user console statistics pages consume the new endpoints directly; the client-side cross-dimension bucket merge (and its "pN of the largest row" approximation) is retired — TTFT p50/p95/p99 shown in the UI are server-computed. A per-model filter was added to user stats.
- TTFT percentile cards are TTL-cached (30 s, per-key request deduplication): concurrent identical dashboard queries share one database round-trip, and leader cancellation no longer poisons waiting requests.
- Settlement batch size is now adaptive: the billing worker's per-statement batch limit self-tunes between 500 and 64 000 rows (seeded at 8 000 — startup behavior unchanged) from measured statement duration against an 8 s budget (0.8 × the repository-side 10 s settlement timeout): fast statements double, slow or timed-out statements halve, other errors hold. This replaces the fragile fixed constant whose oversized value stalled settlement permanently on production-scale dirty visibility maps. Worst-case drain-cycle overrun is now bounded by a single statement up to the 10 s settlement timeout (two lanes sequential per cycle) instead of the prior fixed ~2.6 s.
- Loadtest setup supports multi-run campaigns on one database (
-reuse-template-ids,-run-tag) without deterministic-name 409 collisions.