Skip to content

feat(gateway): sticky session routing via x-session-id - #2445

Merged
steebchen merged 12 commits into
mainfrom
denver-v3
May 31, 2026
Merged

steebchen merged 12 commits into
mainfrom
denver-v3

Conversation

@steebchen

@steebchen steebchen commented May 29, 2026 •

Copy link
Copy Markdown
Member

What

Adds sticky session routing so a logical session is pinned to a single upstream provider, keeping provider-side prompt caches warm across a multi-turn conversation. The session only moves to another provider when its pinned provider leaves the available list (health filtering or retry-fallback exclusion after a failure) — i.e. stick unless the provider is effectively down.

How the session key is resolved

Chat completions (/v1/chat/completions), in priority order:

  1. x-session-id header
  2. prompt_cache_key (OpenAI body field)
  3. user (OpenAI body field — newly added to the schema)

Anthropic Messages (/v1/messages): the key is derived from metadata.user_id (Claude Code embeds the session UUID as …_session_<uuid>), then forwarded to the internal chat/completions call as x-session-id. An explicit x-session-id header on the incoming request still wins.

Routing mechanism

When a session id is present, getCheapestFromAvailableProviders picks the provider/region deterministically via rendezvous (highest-random-weight) hashing, bypassing price/uptime scoring and epsilon-greedy exploration. Rendezvous hashing means removing one provider only reassigns the sessions pinned to that provider — every other session keeps its provider. "Unless down" is handled for free: down providers are excluded from the available list upstream / by the retry-fallback loop, so the hash naturally re-pins to the next provider.

When no session id is present, behavior is unchanged.

Tests

  • packages/actions/src/models.spec.ts — sticky selection is deterministic and survives removal of an unrelated provider.
  • apps/gateway/src/lib/session-id.spec.ts — Anthropic metadata.user_id extraction.

pnpm format, pnpm build, and the targeted unit tests all pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Sticky session routing to pin multi-turn conversations to a single provider/region for deterministic routing.
    • Chat completions accept an optional user fallback identifier; Anthropic requests can derive a session id from message metadata when no session header is provided.
    • Activity logs and UI gain sessionId filtering and display with copy support.
  • Tests

    • Added tests verifying deterministic sticky routing and stability when providers change.
  • Documentation

    • Added Sessions docs and updated routing docs (sticky routing and exploration configurability).
  • Chores

    • DB migration to add session_id column and index for log queries.

Pin a logical session to a single upstream provider so provider-side
prompt caches stay warm across a multi-turn conversation, falling back
only when the pinned provider leaves the available list.

- Resolve a sticky session key from the x-session-id header, then the
  OpenAI-native prompt_cache_key, then user (added to the chat schema).
- For the Anthropic /messages route, derive the key from
  metadata.user_id (Claude Code embeds the session id there) and forward
  it to chat/completions as x-session-id.
- Select the provider/region deterministically via rendezvous hashing
  when a session id is present, bypassing price/uptime scoring.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 29, 2026 08:31
@coderabbitai

coderabbitai Bot commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Derive session id (header/body/Anthropic metadata), thread it through routing and provider-selection calls, deterministically pin sessions via rendezvous hashing in provider selection, add DB/log observability and UI filters, and document Sessions and Sticky Session Routing.

Changes

Sticky Session Routing Implementation

Layer / File(s) Summary
Session ID extraction utility
apps/gateway/src/lib/session-id.ts, apps/gateway/src/lib/session-id.spec.ts
New extractAnthropicSessionId trims input, extracts session_<id> from Anthropic metadata.user_id or falls back to the trimmed value; tests cover extraction, fallback, empty/whitespace handling.
Anthropic schema and session forwarding
apps/gateway/src/anthropic/anthropic.ts
Adds optional metadata.user_id to Anthropic request schema, imports extractAnthropicSessionId, computes sessionId (prefer x-session-id header, else extract from metadata), and conditionally forwards x-session-id to internal /v1/chat/completions.
Chat completions schema and handler initialization
apps/gateway/src/chat/schemas/completions.ts, apps/gateway/src/chat/chat.ts
Adds optional user to completions schema; extends selection plumbing with sessionId?: string; computes sessionId in the /completions handler using header → prompt_cache_key → user.
Thread sessionId through provider selection
apps/gateway/src/chat/chat.ts
Passes computed sessionId into collapseProvidersToBestRegionPerProvider and getCheapestFromAvailableProviders across auto-routing, explicit-provider mapping, rate-limit fallback, low-uptime fallback, candidate scoring, and weighted scoring paths; includes sessionId in log payloads.
Sticky routing provider selection logic
packages/actions/src/get-cheapest-from-available-providers.ts, packages/actions/src/models.spec.ts
Adds sessionId option, deterministic hashing helpers, and an early-exit sticky branch that selects a pinned provider with selectionReason: "session-sticky"; tests verify determinism and stability when providers change.
API: logs filtering and schema
apps/api/src/routes/logs.ts
Expose optional sessionId on returned logs, accept sessionId query param on GET /logs, sanitize and apply it to DB query whereConditions.
UI: Recent logs session filter
apps/ui/src/components/activity/recent-logs.tsx
Add sessionId URL filter + input, include in query params and initial-data reuse checks, update URL on change while preserving scroll position.
DB migration & schema
packages/db/migrations/1780223951_condemned_jack_flag.sql, packages/db/migrations/meta/_journal.json, packages/db/src/schema.ts
Add nullable session_id column to log, create partial composite index (project_id, session_id, created_at) for non-null session_id, and append migration journal entry.
Log card UI
packages/shared/src/components/log-card.tsx
Add sessionId to LogCardData and render Session ID row with optional copy button in expanded metadata.
Docs: Sessions & Routing
apps/docs/content/features/sessions.mdx, apps/docs/content/features/routing.mdx
Add Sessions page describing session resolution order and Anthropic extraction; update Routing docs with configurable epsilon-greedy exploration and Sticky Session Routing section (resolution order, Anthropic handling, rendezvous hashing, fallback/re-pin semantics, session-sticky metadata).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Gateway_ChatHandler
  participant ProviderSelectionService
  participant ProviderRegion
  Client->>Gateway_ChatHandler: POST /completions (x-session-id or prompt_cache_key/user)
  Gateway_ChatHandler->>ProviderSelectionService: getCheapestFromAvailableProviders(options {sessionId})
  ProviderSelectionService->>ProviderRegion: selectStickyProvider(sessionId) / rendezvous-hash
  ProviderRegion-->>ProviderSelectionService: return provider mapping & metrics
  ProviderSelectionService-->>Gateway_ChatHandler: selection (selectionReason: "session-sticky")
  Gateway_ChatHandler->>ProviderRegion: Forward request (with x-session-id header)
  ProviderRegion-->>Gateway_ChatHandler: response
  Gateway_ChatHandler-->>Client: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • smakosh
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'feat(gateway): sticky session routing via x-session-id' directly and specifically describes the main feature added: sticky session routing using x-session-id, which is the primary change across the entire changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch denver-v3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

Pull request overview

Adds sticky session routing to gateway provider selection so chat/Anthropic requests can pin a logical session to a deterministic upstream provider and improve prompt-cache locality.

Changes:

  • Adds sessionId routing support using rendezvous hashing in provider selection.
  • Resolves session IDs from x-session-id, prompt_cache_key, user, and Anthropic metadata.user_id.
  • Adds unit tests for sticky routing determinism and Anthropic session extraction.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/actions/src/get-cheapest-from-available-providers.ts Adds sticky session provider selection and routing metadata.
packages/actions/src/models.spec.ts Adds sticky routing behavior tests.
apps/gateway/src/lib/session-id.ts Adds Anthropic session ID extraction helper.
apps/gateway/src/lib/session-id.spec.ts Tests Anthropic session ID extraction behavior.
apps/gateway/src/chat/schemas/completions.ts Adds OpenAI user field to chat request schema.
apps/gateway/src/chat/chat.ts Resolves and forwards sticky session IDs into provider selection.
apps/gateway/src/anthropic/anthropic.ts Extracts Anthropic session metadata and forwards it to chat completions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}),
metadata: z
.object({
user_id: z.string().optional(),
return {
providerId: provider.providerId,
region: provider.region,
score: 0,

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/actions/src/models.spec.ts (1)

577-612: ⚡ Quick win

Add the complementary re-pinning case.

This covers stability when an unrelated provider is removed, but the core documented behavior — a session moves to a new provider only when its pinned provider leaves the list — isn't asserted. Consider dropping the pinned provider and verifying the session deterministically re-pins to a remaining one (and that two different sessions don't all collapse onto the same provider).

Want me to draft this additional test case?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/actions/src/models.spec.ts` around lines 577 - 612, Add a
complementary test that verifies re-pinning when the pinned provider is removed:
using modelWithMultipleProviders and getCheapestFromAvailableProviders, create a
sessionId, compute the initial pinned provider, then remove that pinned provider
from the availableProviders list and call getCheapestFromAvailableProviders
again to assert the session now deterministically pins to a different provider;
also create a second distinct sessionId and verify it does not collapse onto the
same provider as the first (i.e., two different sessions should not both re-pin
to the same single provider unless deterministic hashing dictates different
results).
packages/actions/src/get-cheapest-from-available-providers.ts (1)

415-446: ⚡ Quick win

Extract the duplicated providerScores builder.

This mapping is identical to the random-exploration block (Lines 463-494). Pull it into one helper so both early-exit paths stay in sync.

♻️ Proposed extraction

Add a shared helper (e.g. near the other private helpers):

function buildZeroScoreProviderScores<T extends AvailableModelProvider>(
	stableProviders: T[],
	modelWithPricing: ModelWithPricing & { id: string },
	metricsMap: Map<string, ProviderMetrics> | undefined,
	videoPricing: VideoPricingContext | undefined,
): RoutingMetadata["providerScores"] {
	return stableProviders.map((provider) => {
		const providerInfo = findProviderMapping(
			modelWithPricing.providers,
			provider,
		);
		const priority = getProviderDefinition(provider.providerId)?.priority ?? 1;
		const metrics = metricsMap?.get(
			metricsKey(modelWithPricing.id, provider.providerId, provider.region),
		);
		return {
			providerId: provider.providerId,
			region: provider.region,
			score: 0,
			uptime: metrics?.uptime,
			latency: metrics?.averageLatency,
			throughput: metrics?.throughput,
			price: getProviderSelectionPrice(providerInfo, videoPricing).toNumber(),
			priority,
			cacheSupported: providerSupportsCaching(
				providerInfo as ProviderModelMapping | undefined,
			),
		};
	});
}

Then both branches collapse to:

-				providerScores: stableProviders.map((provider) => {
-					const providerInfo = findProviderMapping(
-						modelWithPricing.providers,
-						provider,
-					);
-					const providerDef = getProviderDefinition(provider.providerId);
-					const priority = providerDef?.priority ?? 1;
-					const metrics = metricsMap?.get(
-						metricsKey(
-							modelWithPricing.id,
-							provider.providerId,
-							provider.region,
-						),
-					);
-
-					return {
-						providerId: provider.providerId,
-						region: provider.region,
-						score: 0,
-						uptime: metrics?.uptime,
-						latency: metrics?.averageLatency,
-						throughput: metrics?.throughput,
-						price: getProviderSelectionPrice(
-							providerInfo,
-							videoPricing,
-						).toNumber(),
-						priority,
-						cacheSupported: providerSupportsCaching(
-							providerInfo as ProviderModelMapping | undefined,
-						),
-					};
-				}),
+				providerScores: buildZeroScoreProviderScores(
+					stableProviders,
+					modelWithPricing,
+					metricsMap,
+					videoPricing,
+				),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/actions/src/get-cheapest-from-available-providers.ts` around lines
415 - 446, Extract the duplicated providerScores mapping into a single helper
(e.g. buildZeroScoreProviderScores) and call it from both early-exit branches so
they stay in sync. The helper should accept stableProviders, modelWithPricing
(with id), metricsMap, and videoPricing and return
RoutingMetadata["providerScores"]; inside it reuse findProviderMapping,
getProviderDefinition (for priority), metricsKey, getProviderSelectionPrice
(call .toNumber()), and providerSupportsCaching (cast to ProviderModelMapping |
undefined) to build each entry (providerId, region, score: 0, uptime, latency,
throughput, price, priority, cacheSupported). Replace the inline
stableProviders.map(...) blocks in get-cheapest-from-available-providers.ts with
calls to this new helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 1105-1109: The sessionId selection currently trims only the header
value but not the body-derived values, so prompt_cache_key and user can contain
leading/trailing whitespace and break sticky routing; update the sessionId
assignment to normalize (trim) prompt_cache_key and user the same way as
c.req.header does, ensuring the ternary/OR chain that builds sessionId (using
variables sessionId, prompt_cache_key, user, and c.req.header) calls .trim() or
an equivalent normalization on each non-header branch before falling back to
undefined.

---

Nitpick comments:
In `@packages/actions/src/get-cheapest-from-available-providers.ts`:
- Around line 415-446: Extract the duplicated providerScores mapping into a
single helper (e.g. buildZeroScoreProviderScores) and call it from both
early-exit branches so they stay in sync. The helper should accept
stableProviders, modelWithPricing (with id), metricsMap, and videoPricing and
return RoutingMetadata["providerScores"]; inside it reuse findProviderMapping,
getProviderDefinition (for priority), metricsKey, getProviderSelectionPrice
(call .toNumber()), and providerSupportsCaching (cast to ProviderModelMapping |
undefined) to build each entry (providerId, region, score: 0, uptime, latency,
throughput, price, priority, cacheSupported). Replace the inline
stableProviders.map(...) blocks in get-cheapest-from-available-providers.ts with
calls to this new helper.

In `@packages/actions/src/models.spec.ts`:
- Around line 577-612: Add a complementary test that verifies re-pinning when
the pinned provider is removed: using modelWithMultipleProviders and
getCheapestFromAvailableProviders, create a sessionId, compute the initial
pinned provider, then remove that pinned provider from the availableProviders
list and call getCheapestFromAvailableProviders again to assert the session now
deterministically pins to a different provider; also create a second distinct
sessionId and verify it does not collapse onto the same provider as the first
(i.e., two different sessions should not both re-pin to the same single provider
unless deterministic hashing dictates different results).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d006bd6d-e9e6-4e2f-8bef-20bfbfe89106

📥 Commits

Reviewing files that changed from the base of the PR and between 4bd46b0 and 503e3cf.

📒 Files selected for processing (7)
  • apps/gateway/src/anthropic/anthropic.ts
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/chat/schemas/completions.ts
  • apps/gateway/src/lib/session-id.spec.ts
  • apps/gateway/src/lib/session-id.ts
  • packages/actions/src/get-cheapest-from-available-providers.ts
  • packages/actions/src/models.spec.ts

Comment on lines +1105 to +1109
const sessionId =
c.req.header("x-session-id")?.trim() ||
prompt_cache_key ||
user ||
undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize body-derived session IDs before sticky routing.

prompt_cache_key and user are not trimmed, so whitespace variants can pin to different providers unexpectedly. Normalize them like the header path.

💡 Proposed fix
 const sessionId =
 	c.req.header("x-session-id")?.trim() ||
-	prompt_cache_key ||
-	user ||
+	prompt_cache_key?.trim() ||
+	user?.trim() ||
 	undefined;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sessionId =
c.req.header("x-session-id")?.trim() ||
prompt_cache_key ||
user ||
undefined;
const sessionId =
c.req.header("x-session-id")?.trim() ||
prompt_cache_key?.trim() ||
user?.trim() ||
undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/gateway/src/chat/chat.ts` around lines 1105 - 1109, The sessionId
selection currently trims only the header value but not the body-derived values,
so prompt_cache_key and user can contain leading/trailing whitespace and break
sticky routing; update the sessionId assignment to normalize (trim)
prompt_cache_key and user the same way as c.req.header does, ensuring the
ternary/OR chain that builds sessionId (using variables sessionId,
prompt_cache_key, user, and c.req.header) calls .trim() or an equivalent
normalization on each non-header branch before falling back to undefined.

steebchen and others added 11 commits May 29, 2026 11:20
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	apps/gateway/src/chat/chat.ts
#	packages/actions/src/get-cheapest-from-available-providers.ts
# Conflicts:
#	apps/docs/content/features/routing.mdx
Persist the resolved session id (x-session-id / prompt_cache_key / user,
or Anthropic metadata.user_id) on each request log. Surface it in the
activity log card metadata and add a Session ID filter next to the custom
metadata search. Document the sessions feature in a dedicated docs page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /v1/messages hybrid message schema accepted OpenAI-style tool/function
roles but not system/developer, so clients (e.g. Claude Code) that place a
system message in the messages array got a 400. The handler already maps
roles straight through to valid OpenAI messages, so allow them in the enum.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude Code sends metadata.user_id as a JSON string
({"device_id":..,"account_uuid":..,"session_id":"<uuid>"}). The old
regex matched the literal "session_id" key instead of the UUID value, so
every Claude Code request logged the session id as "session_id". Parse the
JSON and use its session_id field, keeping the structured-string fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a session-stickiness setting to the per-project custom routing config
(default enabled). When disabled, the gateway ignores session ids for
provider selection and scores every request independently; the session id
is still recorded for observability. Exposed as a toggle in the routing
settings UI, validated/persisted via the routing-config API, and gated in
getCheapestFromAvailableProviders on cfg.session.enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
opencode sends its session id in the x-session-affinity header (and x-source:
opencode), which the session-key resolver didn't read, so opencode sessions
weren't tracked or pinned. Add x-session-affinity to the resolution chain
below the explicit x-session-id header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@steebchen
steebchen enabled auto-merge May 31, 2026 14:53
@steebchen
steebchen added this pull request to the merge queue May 31, 2026
Merged via the queue into main with commit d53db7e May 31, 2026
16 checks passed
@steebchen
steebchen deleted the denver-v3 branch May 31, 2026 15:08
analogpvt pushed a commit to analogpvt/llmgateway that referenced this pull request Jun 2, 2026
## Why

Two issues raised by a DevPass user (see support email):
1. Clicking **Switch to Pro** charged the card immediately with no
confirmation step.
2. DevPass billing doesn't appear in the standard LLM Gateway billing
dashboard, and it wasn't clear where to manage the subscription /
payment method.

This PR adds a confirmation flow before switching plans, makes
downgrades non-punitive, and restructures the DevPass dashboard into
clear Usage / Billing / Settings sections behind a sidebar.

## Changes

### Confirm before switching plans
- Every **Switch to X** button now opens an `AlertDialog` first, with
distinct copy:
- **Upgrade** → charged a prorated amount today, then $X/mo, allowance
increases right away (green ↑ Upgrade badge).
- **Downgrade** → keep your current allowance until next renewal, **no
refund** for the current period (↓ Downgrade badge).

### Downgrade backend (safe tweak)
- `change-tier`: downgrade `proration_behavior` `create_prorations` →
**`none`** (no refund/credit issued).
- On downgrade we **no longer lower the credit allowance immediately** —
the user keeps what they paid for and the existing renewal webhook
resets it to the lower tier next cycle, so a downgrade never claws back
paid credits. Upgrades unchanged.
- _Deferred:_ scheduling the price swap exactly at period end via Stripe
subscription schedules (needs a pending-downgrade state + webhook);
flagged as follow-up.

### Dashboard sidebar + route split
- New `(main)` route group with a `layout.tsx` → `DashboardShell`
(header + sidebar: **Usage / Billing / Settings**, active state via
`usePathname`, responsive rail/row).
- Split the old monolithic `DashboardClient` into `/dashboard` (Usage),
`/dashboard/billing`, `/dashboard/settings`.
- Billing page adds a current-plan summary + an info note explaining
DevPass billing is separate from pay-as-you-go credits and managed
there.
- Shared `plans.ts` + `useDevPlanStatus()` hook (React Query dedupes the
status query); `ApiKeySection` extracted.
- `agents/[agentId]` and `feedback/…` intentionally stay **outside** the
shell (the feedback page must work for users without an active plan).

## Validation
- `pnpm format` ✓, code app lint ✓, code `tsc --noEmit` ✓, API lint ✓
- Full `next build` ✓ — route map confirms `/dashboard`,
`/dashboard/billing`, `/dashboard/settings` resolve under the shell
while agents/feedback stay standalone.
- Pre-existing `routing-config.ts` tsc errors (PR theopenco#2445) are unrelated
and untouched.

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

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added dedicated billing page to manage subscription and view billing
details
* Added ability to upgrade or downgrade subscription tiers with clear
visual indicators
  * Added subscription cancellation and resumption options
  * Added API key rotation with double-confirmation flow
  * Added settings page for plan configuration

* **Improvements**
* Downgrade behavior now preserves your current credit allowance through
the rest of your billing period

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.8 (1M context) <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