feat: global (user-level) provider connections — slice 1 of #2061#2139
feat: global (user-level) provider connections — slice 1 of #2061#2139guillaumegay13 wants to merge 31 commits into
Conversation
…gent_id null, slim list endpoint, 100% coverage)
…user-scoped unique index)
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 Report❌ Patch coverage is Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
Bundle ReportChanges will increase total bundle size by 28.12kB (2.41%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: manifest-frontend-esmAssets Changed:
|
There was a problem hiding this comment.
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
…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.
There was a problem hiding this comment.
3 issues found across 26 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…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
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
user_providers.agent_idmade nullable (one column change). Connections are written user-scoped (insert omitsagent_id) and read byuser_id.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.)getProviders/getProviderApiKey/getModelsForAgent/etc. re-keyedagent_id → user_id; routing cache split into agent- vs user-scoped withinvalidateUser; tier recalculation fans across all of the user's agents.agentName(now used only as tier-recompute context); the backend makes the row global. Noscopeflag.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_accessjunction / toggle), provider analytics & charts, rate-limit snapshots, global Playground, custom-provider lifting. Those are follow-up slices of #2061.Testing
tsc --noEmitclean (backend + frontend); frontend Vitest green.Notes for review
xai-oauth.controller.tshas no dedicated unit spec (pre-existing) — its user-scoping change is exercised via e2e but may show on Codecov patch coverage.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_providersread/write byuser_id; connect via one agent, others see the models.GET /api/v1/providersreturns a slim list (status, label, key prefix, model counts); shared serializer reused across controllers.initialTab, deep links, and clearer local/status messaging.userId; cache split into agent- vs user-scoped withinvalidateUser;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.listForUserso names/recording match the global pool.Migration
user_providers.agent_idis nullable; unique index moved to(user_id, provider, auth_type, lower(label)).Written for commit ce449b1. Summary will update on new commits.