feat(ai): OpenRouter managed AI — model switching + client wiring (0208) - #223
Merged
Conversation
…client wiring Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a 'managed' AI tier so the app's existing AI surfaces route through XNet
Cloud's metered gateway (exploration 0208, Phase 1):
- ManagedProvider (packages/plugins): keyless AIProvider that posts to the hub's
/ai/chat, surfaces the live budget (onBudget), and maps 402 -> AiBudgetError.
- 'managed' ConnectorTier + detection (probeManaged -> /ai/health), preferred
when available, hides off-cloud. Connector mapping + USABLE tiers updated.
- aiForwarderFeature (packages/hub): authed proxy that injects the per-tenant
credential (x-internal-secret + x-tenant-id) to the control plane; /ai/health
advertises availability. Generic over injected fetch, no hub->cloud edge.
- AiChatPanel: managed controls + budget gauge ('used / included / cap').
Tests: managed-provider (7), ai-forwarder (6), detect (+managed), connector map.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make model switching a real product feature, not an env var (exploration 0208,
Phase 2):
- Per-plan model gating: PlanEntitlements gains aiModels ('all' | id list) +
aiDefaultModel; cheap plans get a cheap subset, bigger plans the whole catalog.
withAiModels/aiModelAllowed helpers (packages/entitlements).
- Live model catalog: apps/cloud/src/ai/models.ts proxies OpenRouter /models into
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. wiring threads aiModels/defaultModel + the catalog from env.
- Model picker UI: data-driven dropdown grouped by family with / + context
badges; preselects the plan default; pure parse/format/group helpers + tests.
Tests: models (7), route +5 (gating/default/catalog), entitlements +3, web +4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reliability for managed AI (exploration 0208, Phase 3):
- ChatRequest.fallbackModels: OpenRouterGatewayClient sends models:[primary,...]
so a single provider outage degrades instead of 502-ing; ChatResult.model
already reports which model actually served.
- /ai/chat forwards only plan-permitted fallback models (defense-in-depth).
- Drop the deprecated usage:{include:true} flag — OpenRouter always returns
usage.cost now.
The 'managed' AIProviderType is first-class in createAIProvider, so AiSurface /
editor /ai / agent runner can target managed AI by config (chat panel is the
first concrete consumer).
Tests: openrouter-gateway +1 (fallback), route +1 (fallback filtering).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…client wiring Mark 0208 implemented (Phases 1-3): managed connector tier + ManagedProvider, hub aiForwarderFeature, live model catalog + GET /ai/models, per-plan model gating, model picker, and model fallbacks. Document the managed-AI hub env (XNET_CLOUD_URL / XNET_CLOUD_INTERNAL_SECRET / XNET_TENANT_ID) in cloud SETUP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve import conflict in packages/hub/src/server.ts (keep both data-usage and ai-forwarder imports).
Contributor
|
✓ Changelog fragment found — thanks! |
Contributor
🖼️ UI changes in this PRNo visual differences detected in the changed UI. |
Contributor
|
Preview removed for PR #223. |
| } | ||
|
|
||
| /** The "used / included / cap" gauge a managed call reports back. */ | ||
| function BudgetGauge({ budget }: { budget: ManagedBudgetSnapshot }) { |
crs48
added a commit
that referenced
this pull request
Jun 20, 2026
…doc/script (#224) Answers "what do I need to configure for managed AI?" from [#223](#223) (exploration 0208): makes it a **one-time control-plane config** and ships an operator doc + preflight script. ## The gap this closes The hub's `aiForwarderFeature` reads `XNET_CLOUD_URL` / `XNET_CLOUD_INTERNAL_SECRET` / `XNET_TENANT_ID`, but `ControlPlane.hubEnv` only injected `HUB_PLAN`/`XNET_PLAN_SECRET` — so managed AI would have required setting those on **every hub by hand**. Now the control plane **injects the forwarder env into every AI-enabled hub** at provision / plan-change / recovery time, gated on a new optional `deps.managedAi` (built in `index.ts` from `XNET_CLOUD_BASE_URL` + `XNET_CLOUD_INTERNAL_SECRET` when AI keys are configured). AI-off tenants (e.g. demo) get nothing, so `/ai/health` stays `managed:false` and the app's managed tier hides. **Hubs need zero AI config.** ## What an operator sets (control plane only) ```bash AI_GATEWAY_PROVIDER=openrouter AI_GATEWAY_BASE_URL=https://openrouter.ai/api/v1 OPENROUTER_MANAGEMENT_KEY=sk-or-... # OpenRouter Provisioning API key AI_MARKUP=1.3 ``` …plus the `XNET_CLOUD_BASE_URL` / `XNET_CLOUD_INTERNAL_SECRET` they already have. Per-plan model gating ships defaulted in entitlements (no env). ## Also in this PR - **`scripts/cloud-openrouter-setup.mjs`** — preflight: validates the Provisioning key, checks credit balance, fetches the live model catalog (and that plan default models exist), optional `--probe-mint` mint→delete round-trip, prints/`--write`s the env block. - **`docs/cloud/MANAGED_AI_SETUP.md`** — full walkthrough (diagram, steps, verify, billing, troubleshooting); linked from `SETUP.md`. - **`cloud-env-schema.mjs`** — adds `AI_GATEWAY_PROVIDER` + `OPENROUTER_MANAGEMENT_KEY`, groups the managed-AI vars, fixes the `AI_MARKUP` default (1.3). ## Tests `control-plane.test.ts` +3: forwarder env injected for an AI tenant, omitted for an AI-off tenant, and omitted when `managedAi` is unset. (Local `wiring.test.ts` needs `stripe` installed — pre-existing on main, unrelated; CI has it.) 🤖 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 0208: 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): keylessAIProviderthat posts to the hub's/ai/chat, surfaces the live budget (onBudget), and maps402→ typedAiBudgetError. Wired intocreateAIProvideras 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; mirrorsconnectorSyncFeature.Phase 2 — model switching (
c6ddd4de)PlanEntitlements.aiModels('all' | id[]) +aiDefaultModel; cheap plans get a cheap subset, bigger plans the whole catalog.withAiModels/aiModelAllowedhelpers.apps/cloud/src/ai/models.tsproxies OpenRouter/models→ pricedModelCard[]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/chatnow defaults to the plan model and enforces the plan policy.Phase 3 — reliability (
c511eeca)ChatRequest.fallbackModels→ OpenRoutermodels:[primary,…];/ai/chatforwards only plan-permitted fallbacks; served model reported back.usage:{include:true}flag (OpenRouter always returnsusage.costnow).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
managedtier hides, BYO-key works./ai/chatand deeper editor/agent wiring are explicitly deferred in the doc.docs/cloud/SETUP.md.🤖 Generated with Claude Code