Skip to content

feat(ai): OpenRouter managed AI — model switching + client wiring (0208) - #223

Merged
crs48 merged 7 commits into
mainfrom
feat/openrouter-managed-ai-model-switching
Jun 20, 2026
Merged

feat(ai): OpenRouter managed AI — model switching + client wiring (0208)#223
crs48 merged 7 commits into
mainfrom
feat/openrouter-managed-ai-model-switching

Conversation

@crs48

@crs48 crs48 commented Jun 20, 2026

Copy link
Copy Markdown
Owner

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): 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

xNet Test and others added 6 commits June 19, 2026 20:48
…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).
@crs48
crs48 temporarily deployed to pr-223 June 20, 2026 04:25 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

@crs48
crs48 temporarily deployed to pr-223 June 20, 2026 04:29 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

No visual differences detected in the changed UI.

CI run

github-actions Bot added a commit that referenced this pull request Jun 20, 2026
@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #223.

github-actions Bot added a commit that referenced this pull request Jun 20, 2026
@crs48
crs48 merged commit 7d72733 into main Jun 20, 2026
13 of 14 checks passed
@crs48
crs48 deleted the feat/openrouter-managed-ai-model-switching branch June 20, 2026 04:36
github-actions Bot added a commit that referenced this pull request Jun 20, 2026
}

/** 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)
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