feat(cloud): managed AI metering, billing & cost truth, run-in-public metrics (0200) - #181
Merged
Conversation
…ic dashboard Two-part exploration (0200): (1) get xNet Cloud billing into a really good place — managed AI with metered usage billing, accurate per-tenant cost calculation, and self-serve account management; (2) a public "run the company in public" dashboard on the marketing site (customer/revenue growth WoW + honest cost breakdown + break-even). Key finding: the @xnetjs/cloud AI/billing/cost spine (LiteLLM gateway, MeteredGateway, usage ledger, token pricing, Stripe meters, COGS model) is already built and tested but DORMANT — zero consumers — so this is mostly wiring + productization. Recommends three slices (Managed AI → cost truth → public dashboard). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Usd) Managed AI needs an included monthly spend and a hard cap per plan, carried in the signed HUB_PLAN token so hub/control-plane enforce it without a round-trip (exploration 0200, slice A). aiEnabled stays as the on/off flag; the new numbers drive the dashboard "used / included / cap" display, the metered gateway's hard stop, and the Stripe metered Price's free first tier. Adds withAiBudget() flip (cap must be >= included) + tests; the token round-trip already carries new fields since it serializes the whole entitlements object. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AI usage ledger is authoritative for accrued spend, so it must survive a control-plane restart — back it with the same DocStore port as tenants/bindings (usageLedgerFromDocs, keyed by the idempotency key). Extend UsageLedger with an optional sinceMs so totalChargeUsd/entries can scope to a billing period: this is how a *monthly* AI budget resets each period instead of accruing for life. MemoryUsageLedger honors it too; Stripe's identifier dedup remains the backstop against a read-then-write race. (Exploration 0200, slice A.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each aiEnabled tenant gets its own LiteLLM virtual key carrying a hard budget the proxy enforces per window. VirtualKeyManager port with FakeVirtualKeyManager (deterministic keys, no proxy) and LiteLLMKeyManager (thin wrapper over /key/generate|update|delete with an injected fetch + master key). The control plane creates one at provision time and stores it server-side as the gateway's Bearer credential — it never reaches the client. (Exploration 0200, slice A.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wires the dormant MeteredGateway into a live control-plane route (exploration 0200, slice A). A tenant's hub forwards a chat request; the route runs it through the budget hard-stop → meter → Stripe pipeline and returns the answer plus spend-this-period for the "used / included / cap" display. Over-budget tenants get a 402 with no provider call. resolveTenant is injected so hub→control-plane auth is pluggable and the route is testable with fakes. Also extends MeteredGateway with an optional periodStartMsFor so the budget scopes to the billing month. Mounted in createControlPlaneApp only when AI deps are configured. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the managed-AI provisioning seam (exploration 0200, slice A): - TenantRecord gains aiKeyRef (LiteLLM virtual key, server-side) + stripeCustomerId. - ControlPlane mints a budgeted virtual key for an aiEnabled tenant at provision time, updates the budget on an in-tier plan flip, and revokes on delete; budget + included come from the entitlement token. currentPeriodStartMs gives the monthly budget window. - Env wiring: aiChatDepsFromEnv assembles the metered route (GatewayClient + durable usageLedgerFromEnv + Stripe meters + a markup pricing table + internal-secret tenant resolver) only when LITELLM_BASE_URL is set; aiKeysFromEnv selects the LiteLLM key manager; both null-fallback like PR #175. - Refactor firestoreFromEnv out so the usage ledger shares one Firestore client. 109 apps/cloud tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lf-serve plan change Slice B (exploration 0200) — billing & cost truth, server side: - @xnetjs/cloud/cost reconcile: measuredCogs/reconcileTenantMargin/aggregateMargin turn estimateCogs's *assumed* inputs into *measured* per-tenant margin from what a tenant actually used (storage bytes, active hours, AI provider cost, real Stripe fees) vs the revenue it produced; flags negative-margin tenants. Pure + tested. - Dashboard: a Managed-AI usage meter (used / included / cap, near-cap warning) fed by the shared usage ledger, plus a self-serve "Change plan" card. - POST /account/plan (session-authed): an in-tier change flips live; a tier crossing returns a migration notice instead of silently moving data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a "Billing & AI usage" row to the existing xNet Cloud group in Network settings, deep-linking to the control-plane dashboard's billing + managed-AI usage view (kept server-side so no payment UI/secrets enter the client bundle). Reinforces the two-identity model in the copy. (Exploration 0200, slice B.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice C (exploration 0200): - apps/cloud metrics rollup: buildCompanyMetrics joins weekly tenant/MRR/COGS with opex, SUPPRESSES any week below a k-anonymity cohort floor, and computes cumulative break-even. Pure + tested. Exported for the control plane to emit. - site /open page: "Running the company in the open" — inline-SVG charts (no charting lib) for customer growth, MRR growth, weekly cost stack, and an honest break-even framing, fed by a committed metrics.json (git history = transparency log) + a hand-maintained opex.ts. Footer link added. - scripts/cloud-metrics-rollup.mjs: publishes the snapshot with a defense-in-depth k-anon re-check + a ban on any per-customer field, then prompts for a review PR. Verified: site builds, /open renders all charts with no console errors. Sample figures are clearly flagged until the live P&L is wired. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reconcile the chat-path env var to the canonical AI_GATEWAY_BASE_URL (LiteLLM or OpenRouter); LITELLM_BASE_URL/MASTER_KEY drive the LiteLLM-specific key admin. - Add LITELLM_MASTER_KEY / AI_MARKUP / AI_ALLOWED_MODELS to the env schema. - SETUP.md Part 3: deploy the LiteLLM proxy, wire managed-AI pricing/budget, the Stripe metered Price, and the run-in-public metrics publish step. - Check off the implemented 0200 checklist items; mark the LiteLLM deploy, hub proxy + client managed-provider, live usage collector, and the metrics cron as the remaining operator/follow-up steps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
|
✓ Changelog fragment found — thanks! |
Contributor
🖼️ UI changes in this PRScreensAuto-captured by CI · run. Informational — not a blocking check. |
Contributor
|
Preview removed for PR #181. |
| { label: 'Overhead', usd: latest.costs.otherUsd, color: '#9ca3af' } | ||
| ] | ||
|
|
||
| export const updated = metrics.updated |
| note?: string | ||
| } | ||
|
|
||
| export const updated = 'June 2026' |
| nowMs: () => number | ||
| ): (c: Context) => Promise<AiTenantContext | null> { | ||
| const secret = env.XNET_CLOUD_INTERNAL_SECRET | ||
| return async (c) => { |
| costs: { infraUsd: number; payrollUsd: number; saasUsd: number; otherUsd: number } | ||
| } | ||
|
|
||
| export interface CompanyMetrics { |
| w.costs.infraUsd + w.costs.payrollUsd + w.costs.saasUsd + w.costs.otherUsd | ||
|
|
||
| /** Weekly revenue ≈ MRR / 4.345 (matches the rollup's break-even math). */ | ||
| export const weekRevenue = (w: CompanyMetricsWeek): number => w.mrrUsd / 4.345 |
| export const weekRevenue = (w: CompanyMetricsWeek): number => w.mrrUsd / 4.345 | ||
|
|
||
| /** Percent change between the two most recent weeks for a numeric selector. */ | ||
| export function wow(select: (w: CompanyMetricsWeek) => number): number { |
| note?: string | ||
| } | ||
|
|
||
| export const updated = 'June 2026' |
| export const updated = 'June 2026' | ||
|
|
||
| /** Recurring monthly operating costs. Infra here is the fixed floor; usage-based infra rides the weekly COGS. */ | ||
| export const OPEX: OpexLine[] = [ |
| export const monthlyOpexTotal = OPEX.reduce((sum, l) => sum + l.monthlyUsd, 0) | ||
|
|
||
| /** Recurring opex grouped by category (for the breakdown chart). */ | ||
| export const opexByCategory: { category: OpexCategory; monthlyUsd: number }[] = ( |
The footer was missing the cloud surfaces entirely. Add a dedicated "Cloud" column (xNet Cloud /cloud · Pricing /cloud/pricing · Open metrics /open) and put Changelog under Product, so every public section the nav exposes also has a home in the footer. Grid widened to 6 columns on desktop, 2 on small screens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crs48
added a commit
that referenced
this pull request
Jun 18, 2026
## Summary Turns the OpenRouter key into working **metered managed AI** for xNet Cloud, and adds the billing-model pieces from exploration **0201**. Builds directly on the 0200/#181 metering spine — the gateway and key adapters slot into the existing `ChatGateway` / `VirtualKeyManager` ports, so the route, ledger, Stripe meter, dashboard, and margin reconciliation are untouched. ### Phase 1 — OpenRouter gateway + exact cost - **`OpenRouterGatewayClient`** (`ChatGateway`): OpenAI-compatible `/chat/completions`, requests usage accounting, and reads **`usage.cost`** → `ChatResult.providerCostUsd` (the exact USD billed, caching/reasoning tokens baked in). - **`OpenRouterKeyManager`** (`VirtualKeyManager`): per-tenant keys via the Provisioning API with a monthly USD `limit`. The secret is the Bearer; update/delete address the key by `hash`, carried as `VirtualKey.manageId` and stored on `TenantRecord.aiKeyManageRef`. - **Exact-cost metering:** `meterUsage` charges off `providerCostUsd × markup` (rounded up) when present, else the static-table estimate — so margin reconciliation becomes *measured*, not modeled. New `computeChargeFromCostUsd`. - **Env selection:** `aiGatewayProvider()` picks `openrouter | litellm` from `AI_GATEWAY_PROVIDER` or a base-URL sniff; `aiKeysFromEnv` builds the OpenRouter key manager from `OPENROUTER_MANAGEMENT_KEY`. - Default `AI_MARKUP` 1.25 → **1.3** (absorbs OpenRouter's ~5.5% buy-in + Stripe fees). SETUP.md §3a rewritten with OpenRouter as the recommended path. ### Phase 2 — billing model (server pieces) - **Self-serve spend cap:** `ControlPlane.setAiCap` (clamped to ≤ the plan's `aiMonthlyBudgetUsd`); the tenant resolver enforces `min(cap, plan cap)`. - **Threshold logic:** `aiBudgetStatus` (`included|overage|near-cap|over-cap`) + `crossedThresholds` (50/80/95/100%); `/ai/chat` now returns `budgetState` for the client gauge. ### Deferred (honest follow-ups, consistent with how #181 deferred its Phase 3) - Prepaid **credit packs** (Stripe Credit Grants + burn-down + checkout UI) - Notification **delivery** for threshold crossings (calculation lands here; no transport yet) - Per-seat included AI for Team+ - **Phase 3** wire-into-product: hub `/ai/chat` forwarder + `managed` client `AIProvider` + budget gauge UI - Operator: deploy + live smoke; mirror the gitignored `.env.staging` key to Secret Manager ## Decision notes - **OpenRouter-direct over self-hosted LiteLLM** for our scale: no proxy to run, exact cost, managed budgets. LiteLLM stays a documented swap-in behind the same ports. Stacking LiteLLM in front of OpenRouter is explicitly avoided (double fee + diverging cost numbers). - The user's "pass-through per token" vs "prepaid credits with a max" are the same pattern at two points on one line — included allotment → overage. Default = PAYG-metered-to-a-hard-cap (already built); credits are the deferred add-on. ## Testing - `255` unit tests pass across `packages/cloud` + `apps/cloud` (new: OpenRouter gateway/keys, exact-cost metering, budget status/thresholds, provider/key-manager selection, `setAiCap`). - `@xnetjs/cloud` and `xnet-cloud` typecheck clean; prettier + eslint clean on touched files. Exploration: `docs/explorations/0201_[_]_OPENROUTER_LITELLM_METERED_AI_AND_CREDITS_BILLING.md` (phase 1–2 items checked off). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
crs48
added a commit
that referenced
this pull request
Jun 20, 2026
…08) (#223) Implements exploration [0208](docs/explorations/0208_[x]_OPENROUTER_MANAGED_AI_MODEL_SWITCHING_AND_CLIENT_WIRING.md): make OpenRouter **managed AI** a first-class client experience — route the app's existing AI surfaces through XNet Cloud's metered gateway, and add real **model switching**. The metered-billing half already shipped (0200 #181, 0201 #187: gateway, exact `usage.cost`, per-tenant keys, budget cap). This PR closes the two pieces those docs deferred: **client wiring** and **model switching**. ## Phase 1 — managed client path (`1c928717`) - **`ManagedProvider`** (`@xnetjs/plugins`): keyless `AIProvider` that posts to the hub's `/ai/chat`, surfaces the live budget (`onBudget`), and maps `402` → typed `AiBudgetError`. Wired into `createAIProvider` as a new `'managed'` `AIProviderType`. - **`'managed'` connector tier** + detection (`probeManaged` → `/ai/health`), preferred when available, hides off-cloud (BYO stays the OSS path). Connector mapping + USABLE tiers updated. - **`aiForwarderFeature`** (`@xnetjs/hub`): authed proxy that injects the per-tenant credential (`x-internal-secret` + `x-tenant-id`) to the control plane — the client never holds a key. Generic over injected fetch, no hub→cloud edge; mirrors `connectorSyncFeature`. - **Chat panel**: managed controls + "used / included / cap" budget gauge. ## Phase 2 — model switching (`c6ddd4de`) - **Per-plan model gating**: `PlanEntitlements.aiModels` (`'all' | id[]`) + `aiDefaultModel`; cheap plans get a cheap subset, bigger plans the whole catalog. `withAiModels` / `aiModelAllowed` helpers. - **Live model catalog**: `apps/cloud/src/ai/models.ts` proxies OpenRouter `/models` → priced `ModelCard[]` with a TTL cache (single-flight + stale-while-revalidate). - **`GET /ai/models`**: cached catalog ∩ plan policy + default; id-only fallback when the catalog is unavailable. `/ai/chat` now defaults to the plan model and enforces the plan policy. - **Model picker UI**: data-driven dropdown grouped by family with $in/$out + context badges; preselects the plan default. ## Phase 3 — reliability (`c511eeca`) - **Model fallbacks**: `ChatRequest.fallbackModels` → OpenRouter `models:[primary,…]`; `/ai/chat` forwards only plan-permitted fallbacks; served model reported back. - Dropped the deprecated `usage:{include:true}` flag (OpenRouter always returns `usage.cost` now). ## Tests 132 new/changed tests across the touched files (managed provider, hub forwarder, connector detection/mapping, model catalog + cache, route gating/default/fallback, entitlements gating, model-picker helpers). All green locally. ## Notes - Self-host degrades gracefully: no control plane ⇒ `managed` tier hides, BYO-key works. - Streaming `/ai/chat` and deeper editor/agent wiring are explicitly deferred in the doc. - Managed-AI hub env documented in `docs/cloud/SETUP.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Implements exploration 0200 — getting xNet Cloud billing into a really good place (with a managed, metered AI offering) and a public run-the-company-in-public metrics dashboard. The key insight from the exploration: the AI/billing/cost engine in
@xnetjs/cloudwas already built but dormant (zero consumers) — this mostly wires + productizes it.Slice A — Managed AI (the money-making piece)
@xnetjs/entitlements:includedAiUsd+aiMonthlyBudgetUsdper plan, carried in the signedHUB_PLANtoken (withAiBudget).UsageLedgerover the PR feat(cloud): integrate real M1+M2 credentials (Stripe, Firestore, Cloud Run) #175DocStore, with an optionalsinceMsso the budget resets monthly.LiteLLMKeyManager(port + fake + real adapter) mints eachaiEnabledtenant a budgeted virtual key at provision time; updated on plan flip, revoked on delete.POST /ai/chatover the dormantMeteredGateway: budget hard-stop → meter → Stripe. Over-budget tenants get a 402 with no provider call. Env-wired viaAI_GATEWAY_BASE_URL(LiteLLM or OpenRouter upstream); a 1.25× markup pricing table.Slice B — Billing & cost truth
POST /account/plan(in-tier flip live; tier cross → migration notice).@xnetjs/cloud/costmargin reconciliation (measuredCogs/reconcileTenantMargin/aggregateMargin) turns the assumed COGS model into measured per-tenant margin and flags money-losing tenants.Slice C — Run-in-public dashboard
buildCompanyMetricsrollup with a k-anonymity floor + break-even; a/openAstro page with dependency-light inline-SVG charts (customer growth, MRR, weekly cost stack), fed by a committedmetrics.json(git history = transparency log) + a hand-maintainedopex.ts.scripts/cloud-metrics-rollup.mjspublishes with a defense-in-depth k-anon re-check + a ban on per-customer fields.Verification
turbo buildtypechecks clean; eslint/prettier clean; lockfile untouched (no new deps)./openpreview-verified (charts render, no console errors). Sample figures are clearly flagged until the live P&L is wired.Operator/follow-up (documented in
docs/cloud/SETUP.mdPart 3): deploy the LiteLLM proxy, the hub→control-plane AI forwarder + clientmanagedprovider, the live usage collector, and the metrics cron. Doc checklist updated.🤖 Generated with Claude Code