Skip to content

feat: global (user-level) provider connections — slice 1 of #2061#2139

Open
guillaumegay13 wants to merge 31 commits into
mnfst:mainfrom
guillaumegay13:feat/global-providers
Open

feat: global (user-level) provider connections — slice 1 of #2061#2139
guillaumegay13 wants to merge 31 commits into
mnfst:mainfrom
guillaumegay13:feat/global-providers

Conversation

@guillaumegay13

@guillaumegay13 guillaumegay13 commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

What

First, self-contained slice of the subscription-maximization pivot (#2061): provider connections become a user-level (global) resource shared across all of a user's agents. Connect a provider/subscription once, and every agent can use it.

How

  • Schema: user_providers.agent_id made nullable (one column change). Connections are written user-scoped (insert omits agent_id) and read by user_id.
  • Migration LiftAgentProvidersToGlobal: non-destructive — relabels colliding rows (Default → agent name; custom → "<label> - <agent>"), never deletes, swaps to a (user_id, provider, auth_type, LOWER(label)) unique index. (Ports Seb's relabel logic from feat: subscription maximization pivot — provider pages, usage charts, agent provider access #2061, minus the access junction.)
  • Service layer: getProviders/getProviderApiKey/getModelsForAgent/etc. re-keyed agent_id → user_id; routing cache split into agent- vs user-scoped with invalidateUser; tier recalculation fans across all of the user's agents.
  • Connect path unchanged: OAuth controllers and the connect API keep agentName (now used only as tier-recompute context); the backend makes the row global. No scope flag.
  • New GET /api/v1/providers (slim list) + frontend Subscriptions / BYOK / Local pages and a sidebar section, reusing the existing connect components.

Out of scope (deferred, not in this PR)

Per-agent access control (the agent_provider_access junction / toggle), provider analytics & charts, rate-limit snapshots, global Playground, custom-provider lifting. Those are follow-up slices of #2061.

Testing

  • Backend unit: 6150 passing; e2e: 211 passing — including a new e2e proving connect via agent A → agent B sees the models.
  • tsc --noEmit clean (backend + frontend); frontend Vitest green.

Notes for review

  • xai-oauth.controller.ts has no dedicated unit spec (pre-existing) — its user-scoping change is exercised via e2e but may show on Codecov patch coverage.
  • Includes the design spec + implementation plan under docs/superpowers/ — happy to drop those from the diff if you'd prefer a leaner PR.

🤖 Generated with Claude Code


Summary by cubic

Make provider connections global at the user level so you connect a provider/subscription once and all of the user’s agents can use it. This slice of #2061 adds a user-scoped providers API/UI and threads user scope through discovery, routing, and tiers.

  • New Features

    • User-scoped connections: user_providers read/write by user_id; connect via one agent, others see the models.
    • New endpoint: GET /api/v1/providers returns a slim list (status, label, key prefix, model counts); shared serializer reused across controllers.
    • Frontend: global Subscriptions, BYOK, and Local pages with a sidebar section; connect modal supports initialTab, deep links, and clearer local/status messaging.
    • User-scoped discovery/recording and routing: services now take userId; cache split into agent- vs user-scoped with invalidateUser; getTiers(agentId, userId) required; refreshes fan out tier recalculation to all of a user’s agents; connect API stays agent-addressed but writes global rows.
    • Custom providers: CRUD/cache remain agent-scoped; discovery and message recording read via a user-scoped listForUser so names/recording match the global pool.
  • Migration

    • Schema: user_providers.agent_id is nullable; unique index moved to (user_id, provider, auth_type, lower(label)).
    • Migration “LiftAgentProvidersToGlobal”: non-destructive relabel of colliding rows (Default → agent name; custom → " - "); no deletions.

Written for commit ce449b1. Summary will update on new commits.

Review in cubic

…gent_id null, slim list endpoint, 100% coverage)
Adds GlobalProvidersController under api/v1/providers with a single
GET / route that returns user-scoped connection rows (no analytics or
sparklines). 100% line coverage enforced by TDD spec; registered in
RoutingModule.
…components

- ProviderDeepLink gains authType?, closeOnBack?, addKey? optional fields
- ProviderSelectModal gains initialTab? prop forwarded to content
- ProviderSelectContent reads initialTab (defaults active tab signal),
  deepLink.authType (seeds selectedAuthType), deepLink.addKey (seeds
  addKeyIntent), deepLink.closeOnBack (detailBack closes modal instead of
  going back to list); uses detailBack for all detail-view onBack props
- ProviderDetailView adds isLocalConnected/isLocalMode helpers and local
  branch in connected(); header restyled with inline close button (calls
  onBack) replacing the separate modal-back-btn
- Tests updated to reflect new Close aria-label (was 'Back to providers');
  new assertions cover initialTab, authType deeplink, closeOnBack, local
  branch in ProviderDetailView
Final review + a first-arg sweep found callers still passing agent.id into the
now-userId-first read methods (silently returned empty, masked by green CI):
- playground.service (hasActiveProvider/getAuthType/getProviderKeys/getProviderApiKey)
- header-tier.service+controller (getModelsForAgent userId,agentId; thread userId)
- specificity.service+controller (setOverride body, setFallbacks/buildFallbackRoutes; thread userId)
- overview.controller, proxy-message-recorder (canonicalize+computeBaseline), custom-provider.controller, model.controller -> getProviders/list(user.id)
- CustomProviderService.list lifted to user_id + user-keyed cache (invalidateUser clears it)
Also: e2e specs updated for recalculate(agentId,userId); spec arg-level assertions added.
Unit suite green (6150). NOTE: 8 e2e assertions still failing in proxy/routing/recorder/multi-key
paths from the B+C port (global-pool model-pick + reorder/recorder) — triage pending.
Providers are user-global after the user-scoping re-key, so the prior
deactivate-all cleanup left inactive same-key rows behind. The connect
path's same-key reactivation then resurrected them with their old labels
(e.g. a renamed "Office" row reappearing instead of a fresh "Work"),
breaking the reorder and delete-one-key assertions. Align the beforeEach
to Seb's c3dd81b hard-DELETE of user_providers (minus the out-of-scope
agent_provider_access table) so each test starts from a truly clean
user-scoped state.
The user-scoping re-key reads providers and tiers by user_id (agent_id
omitted on insert), but these e2e suites still seeded user_providers /
tier_assignments with TEST_TENANT_ID and pinned cached_models updates by
agent_id. Those rows were invisible to the proxy/resolver, so the seeded
models and fallback chain never loaded:

- routing-flow: cached_models UPDATE matched zero rows -> cheapest-pick
  fell back to defaults (gpt-5-nano instead of gpt-4o-mini).
- proxy-fallback-auth & messages-cache-tokens: providers/tier seeded
  under the tenant id were unreadable, so fallback + cache-token paths
  never engaged.

Align seed rows to user_id (TEST_USER_ID) and invalidate the user-scoped
routing cache alongside the agent cache, matching Seb's c3dd81b e2e
(minus the out-of-scope agent_provider_access inserts). Assertions are
unchanged.
@codecov

codecov Bot commented Jun 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.01587% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.44%. Comparing base (4ff43b8) to head (ce449b1).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2139      +/-   ##
==========================================
- Coverage   99.48%   99.44%   -0.04%     
==========================================
  Files         190      193       +3     
  Lines       18303    18790     +487     
  Branches     7256     7479     +223     
==========================================
+ Hits        18209    18686     +477     
- Misses         92      102      +10     
  Partials        2        2              
Flag Coverage Δ
frontend 99.48% <98.01%> (-0.05%) ⬇️
shared 97.92% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codecov

codecov Bot commented Jun 6, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 28.12kB (2.41%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
manifest-frontend-esm 1.2MB 28.12kB (2.41%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: manifest-frontend-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/ProviderSelectModal-*.js 149 bytes 110.62kB 0.13%
assets/Routing-*.js 72 bytes 107.23kB 0.07%
assets/index-*.js 1.12kB 62.9kB 1.82%
assets/routing-*.css (New) 61.15kB 61.15kB 100.0% 🚀
assets/MessageLog-*.js 37 bytes 50.84kB 0.07%
assets/vendor-*.js 152 bytes 48.21kB 0.32%
assets/Playground-*.js 72 bytes 41.85kB 0.17%
assets/provider-*.js (New) 39.08kB 39.08kB 100.0% 🚀
assets/overview-*.js 42 bytes 26.82kB 0.16%
assets/Overview-*.js 42 bytes 25.41kB 0.17%
assets/ModelPickerModal-*.js 42 bytes 22.11kB 0.19%
assets/ModelPrices-*.js 42 bytes 14.18kB 0.3%
assets/Subscriptions-*.js (New) 8.96kB 8.96kB 100.0% 🚀
assets/Byok-*.js (New) 8.86kB 8.86kB 100.0% 🚀
assets/Local-*.js (New) 8.35kB 8.35kB 100.0% 🚀
assets/api-*.js 71 bytes 4.59kB 1.57%
assets/routing-*.js -3.93kB 1 bytes -99.97%
assets/routing-*.js -37.61kB 3.93kB -90.54%
assets/routing-*.js (New) 2.51kB 2.51kB 100.0% 🚀
assets/model-*.js 47 bytes 535 bytes 9.63% ⚠️
assets/ModelPickerModal-*.css (Deleted) -61.15kB 0 bytes -100.0% 🗑️

@cubic-dev-ai cubic-dev-ai Bot 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.

10 issues found across 109 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/backend/src/routing/provider.controller.ts">

<violation number="1" location="packages/backend/src/routing/provider.controller.ts:47">
P2: `hasActiveProvider` can be true for subscription connections without a usable credential, so status/picker behavior treats non-routable providers as available.

(Based on your team's feedback about usable credentials for subscription providers.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/backend/src/routing/routing-core/tier-auto-assign.service.ts">

<violation number="1" location="packages/backend/src/routing/routing-core/tier-auto-assign.service.ts:65">
P1: Tier auto-assign now considers tokenless subscription providers, so it can auto-pick routes that cannot authenticate at request time. Filter discovery results to only routable credentials before scoring/assignment.</violation>
</file>

<file name="packages/backend/src/routing/proxy/proxy.service.ts">

<violation number="1" location="packages/backend/src/routing/proxy/proxy.service.ts:408">
P2: Heartbeat fast-path is no longer token-budget gated; any request with `HEARTBEAT_OK` can bypass size-aware routing. This can misroute oversized requests to the simple tier and degrade reliability/cost behavior.

(Based on your team's feedback about heartbeat token-budget gating.) [FEEDBACK_USED].</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic

Comment thread packages/backend/src/routing/model.controller.ts Outdated
Comment thread packages/backend/src/routing/proxy/proxy.service.ts
Comment thread packages/frontend/src/components/ProviderDetailView.tsx Outdated
Comment thread packages/backend/src/routing/routing-core/tier.service.ts Outdated
Comment thread packages/backend/src/routing/provider.controller.ts
Comment thread packages/backend/src/routing/custom-provider/custom-provider.service.ts Outdated
Comment thread packages/backend/src/playground/playground.service.ts
Comment thread packages/backend/src/routing/global-providers.controller.ts Outdated
…sForUser in refresh handlers

Both refreshModels and refreshProviderModels now fan-out tier recalculation
to all the user's agents rather than a single agent, matching the user-scoped
provider model after the global-providers refactor.
The header × button now calls onClose to dismiss the entire modal rather
than onBack (which was overloaded as both "go to list" and "close modal"
depending on context). This gives the button a single, unambiguous role.
Update the modal integration tests to match the new behavior.
…r cleanup

getTiers(agentId, userId?) → getTiers(agentId, userId: string) so that
getProviders(userId) is always called, ensuring stale user_providers rows
are cleaned up on every tier resolution. All callers (proxy, resolve,
message-recorder) now thread userId through explicitly.
… list

list(userId) → list(agentId): query, cache key, and
canonicalizeAgentMessageKeys all keyed by agentId.
invalidateAgent now clears the customProviders slot; invalidateUser does not.
Cache invalidation in remove() happens after the DB delete, not before.
…this user'

Providers are user-scoped; the no-provider error message was misleadingly
suggesting agent scope.
…ider-response.ts

Eliminates duplicate inline .map(p => ({...})) in GlobalProvidersController
and ProviderController. The shared serializer has its own unit spec covering
all null/undefined fallbacks so the shape is tested independently of the
controllers that use it.

@cubic-dev-ai cubic-dev-ai Bot 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.

3 issues found across 26 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/backend/src/routing/model.controller.ts Outdated
Comment thread packages/backend/src/routing/routing-core/tier.service.ts
…d discovery (recording + model names)

Model discovery is user-scoped but canonicalizeAgentMessageKeys and the
model-name display map were agent-scoped, so cross-agent custom-provider
rows were invisible to message recording and the model picker.

- Add listForUser(userId) to CustomProviderService for user-global reads
- Change canonicalizeAgentMessageKeys first param to userId; call listForUser
- Change all canonicalizeAgentMessageKeys calls in proxy-message-recorder
  from ctx.agentId to ctx.userId (recordProviderError, recordFailedFallbacks
  primary + failures map, recordPrimaryFailure, recordFallbackSuccess x2,
  recordSuccessMessage)
- Change model.controller getAvailableModels display-name map from
  customProviderService.list(agent.id) to listForUser(user.id)
- Update specs: canonicalize tests use userId/user_id, add listForUser
  coverage, add toHaveBeenCalledWith(userId) scoping assertions in
  proxy-message-recorder and model.controller specs so an agentId/userId
  swap fails
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.

1 participant