feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: Multi-Provider support - #2277

Merged
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry
Apr 29, 2026
Merged

feat: Multi-Provider support#2277
juliusmarminge merged 34 commits into
mainfrom
t3code/provider-instance-registry

Conversation

@juliusmarminge

@juliusmarmingejuliusmarminge commented Apr 21, 2026

Copy link
Copy Markdown
Member

Core foundation for multi-providers with some UX improvements to nicely support multi-account codex or custom claude code instances for example


Note

Medium Risk
Changes the core wiring for all git text-generation calls to resolve by provider instance id, so misconfiguration or missing registry entries could break commit/PR/title generation across providers.

Overview
Switches git text-generation from kind-based routing (RoutingTextGenerationLive) to instance-based routing via a new TextGenerationLive that looks up modelSelection.instanceId in ProviderInstanceRegistry and delegates to that instance’s bound textGeneration implementation.

Refactors Codex/Claude/Cursor/OpenCode text-generation implementations into per-instance factories (make*TextGeneration(settings, env)), removes singleton *TextGenerationLive layers and provider-kind validation checks, and updates tests/integration harnesses to use ProviderDriverKind/ProviderInstanceId plus adapter-registry mocks and no-op provider event loggers. Also extends desktop persisted client settings to include providerModelPreferences.

Reviewed by Cursor Bugbot for commit 245ec79. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add multi-provider instance support routing all provider interactions by ProviderInstanceId

  • Replaces the closed ProviderKind string union with open branded ProviderDriverKind and ProviderInstanceId types throughout contracts, server, and web, allowing multiple configured instances of the same driver (e.g. two Codex accounts).
  • Introduces ProviderDriver SPI with built-in implementations for Codex, Claude, Cursor, and OpenCode; each driver's create() produces a ProviderInstance with adapter, text-generation, and snapshot closures bound to per-instance config.
  • Adds ProviderInstanceRegistry and ProviderInstanceRegistryLive to manage live instances; ProviderRegistryLive is refactored to key snapshots and cache files by instanceId and react dynamically to instance add/remove/rebuild events.
  • ModelSelection wire format migrates from { provider, model } to { instanceId, model } with a decode-time transform that promotes legacy provider strings; OrchestrationSession gains an optional providerInstanceId.
  • Web settings UI gains AddProviderInstanceDialog and ProviderInstanceCard for creating and editing provider instances; ModelPickerContent, ModelPickerSidebar, and ChatComposer are refactored to select and display models per-instance.
  • Sensitive provider instance environment variables are stored in a secret store and redacted before returning settings to clients.
  • Adds DB migrations 27 and 28 adding nullable provider_instance_id columns and indexes to provider_session_runtime and projection_thread_sessions.
  • Risk: ModelSelection shape changes from provider to instanceId on the wire; any client or stored event that contains the old shape will be migrated at decode time, but callers constructing raw objects must switch to instanceId.

Macroscope summarized 245ec79.


Open in Devin Review

@coderabbitai

coderabbitaiBot commented Apr 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4436d2e-a060-4b92-a429-f1085c437e1b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/provider-instance-registry

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

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Apr 21, 2026

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: CodexAdapter hardcoded instanceId check breaks custom instances
    • Removed the hardcoded instanceId === "codex" check and replaced it with an unconditional input.modelSelection check, matching the pattern already used in ClaudeAdapter.

Create PR

Or push these changes by commenting:

@cursor push 84b0d74e0a
Preview (84b0d74e0a)
diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts--- a/apps/server/src/provider/Layers/CodexAdapter.ts+++ b/apps/server/src/provider/Layers/CodexAdapter.ts@@ -1368,10 +1368,8 @@
? { resumeCursor: input.resumeCursor }
: {}),
runtimeMode: input.runtimeMode,
- ...(input.modelSelection?.instanceId === "codex"- ? { model: input.modelSelection.model }- : {}),- ...(input.modelSelection?.instanceId === "codex" &&+ ...(input.modelSelection ? { model: input.modelSelection.model } : {}),+ ...(input.modelSelection &&
getModelSelectionOptionValue(input.modelSelection, "fastMode") === true
? { serviceTier: "fast" }
: {}),

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/provider/Layers/CodexAdapter.ts
Comment threadapps/server/src/provider/Layers/ProviderEventLoggers.ts
Comment threadapps/web/src/workspace/store.ts Outdated
Comment threadapps/web/src/hooks/useWorkspaceCommandExecutor.ts Outdated
Comment threadapps/server/src/provider/Layers/ClaudeAdapter.ts Outdated
@macroscopeapp

macroscopeappBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-array-refactor branch from 8a82b53 to 29261cfCompareApril 21, 2026 23:41
Base automatically changed from t3code/provider-array-refactor to mainApril 23, 2026 13:35
Comment threadapps/web/src/modelSelection.ts Outdated
Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/web/src/composerDraftStore.ts Outdated
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from 7a466fe to fb6a229CompareApril 23, 2026 15:51

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Hardcoded "codex" provider in unknown-driver error path
    • Replaced the hardcoded provider: "codex" with provider: threadInstanceId so the error correctly identifies the actual unknown driver slug.
  • ✅ Fixed: Instance-level comparison blocks custom instances of same driver
    • Added a resolveDriverKindForInstanceId helper that maps instance IDs to their underlying driver kind via ServerSettings.providerInstances, so custom instances resolve to their driver for the narrowing check and the provider-switch guard now compares driver kinds instead of instance IDs.

Create PR

Or push these changes by commenting:

@cursor push b84bfe908a
Preview (b84bfe908a)
diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts--- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts@@ -170,6 +170,22 @@
),
);
+ const resolveDriverKindForInstanceId = (+ instanceId: string,+ ): Effect.Effect<ProviderKind | undefined> => {+ if (Schema.is(ProviderKind)(instanceId)) {+ return Effect.succeed(instanceId);+ }+ return serverSettingsService.getSettings.pipe(+ Effect.map((settings) => {+ const instances = settings.providerInstances as Record<string, { driver?: string }>;+ const driver = instances[instanceId]?.driver;+ return driver !== undefined && Schema.is(ProviderKind)(driver) ? driver : undefined;+ }),+ Effect.orElseSucceed(() => undefined),+ );+ };+
const threadModelSelections = new Map<string, ModelSelection>();
const appendProviderFailureActivity = (input: {
@@ -277,29 +293,28 @@
? thread.session.providerName
: undefined;
const requestedModelSelection = options?.modelSelection;
- // The thread's model selection is keyed by `instanceId` (an open slug);- // for built-in drivers the default instance id equals the driver id, so- // narrow back to the closed `ProviderKind` here. Threads persisted- // against a fork/unknown driver are surfaced via a structured error- // rather than silently routed to the wrong provider.- const threadProviderCandidate: string = currentProvider ?? thread.modelSelection.instanceId;- if (!Schema.is(ProviderKind)(threadProviderCandidate)) {+ const threadInstanceId: string = currentProvider ?? thread.modelSelection.instanceId;+ const resolvedThreadDriver: ProviderKind | undefined =+ yield* resolveDriverKindForInstanceId(threadInstanceId);+ if (resolvedThreadDriver === undefined) {
return yield* new ProviderAdapterRequestError({
- provider: "codex",+ provider: threadInstanceId,
method: "thread.turn.start",
- detail: `Thread '${threadId}' references unknown provider driver '${threadProviderCandidate}'. The driver is not installed in this build (rolled-back / fork mismatch).`,+ detail: `Thread '${threadId}' references unknown provider driver '${threadInstanceId}'. The driver is not installed in this build (rolled-back / fork mismatch).`,
});
}
- const threadProvider: ProviderKind = threadProviderCandidate;- if (- requestedModelSelection !== undefined &&- requestedModelSelection.instanceId !== threadProvider- ) {- return yield* new ProviderAdapterRequestError({- provider: threadProvider,- method: "thread.turn.start",- detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,- });+ const threadProvider: ProviderKind = resolvedThreadDriver;+ if (requestedModelSelection !== undefined) {+ const requestedDriver: ProviderKind | undefined = yield* resolveDriverKindForInstanceId(+ requestedModelSelection.instanceId,+ );+ if (requestedDriver !== threadProvider) {+ return yield* new ProviderAdapterRequestError({+ provider: threadProvider,+ method: "thread.turn.start",+ detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.instanceId}'.`,+ });+ }
}
const preferredProvider: ProviderKind = threadProvider;
const desiredModelSelection = requestedModelSelection ?? thread.modelSelection;

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/orchestration/Layers/ProviderCommandReactor.ts Outdated
options: Schema.optionalKey(ProviderOptionSelections),
}),
]);
const ModelSelectionPatch = Schema.Struct({

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.

🟡 Mediumsrc/settings.ts:195

ModelSelectionPatch accepts instanceId but callers still sending provider will have that key silently ignored, leaving instanceId undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps provider to instanceId if present.

Also found in 1 other location(s)

apps/server/src/provider/Layers/ClaudeAdapter.ts:557

The condition input.modelSelection?.instanceId === &#34;claudeAgent&#34; will likely fail for most Claude instances. According to ClaudeDriver metadata, supportsMultipleInstances: true, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked provider === &#34;claudeAgent&#34; which is a constant discriminant on all Claude adapters (as defined in ClaudeAdapterShape). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing rawEffort to be null and claudeModel to be undefined for all other Claude instances.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file packages/contracts/src/settings.ts around line 195:
`ModelSelectionPatch` accepts `instanceId` but callers still sending `provider` will have that key silently ignored, leaving `instanceId` undefined. This breaks backward compatibility for any API calls or persisted patches using the old field name. Consider adding a decode transformation that maps `provider` to `instanceId` if present.
Evidence trail:
packages/contracts/src/settings.ts:195-199 (ModelSelectionPatch schema - no `provider` field), packages/contracts/src/orchestration.ts:75-104 (ModelSelection has provider→instanceId transform), packages/contracts/src/orchestration.test.ts:641 (test confirms ModelSelection migration), packages/contracts/src/ws.ts:112 and packages/contracts/src/rpc.ts:163 (ServerSettingsPatch exposed via API)
Also found in 1 other location(s):
- apps/server/src/provider/Layers/ClaudeAdapter.ts:557 -- The condition `input.modelSelection?.instanceId === "claudeAgent"` will likely fail for most Claude instances. According to `ClaudeDriver` metadata, `supportsMultipleInstances: true`, meaning instances can have arbitrary IDs (e.g., "claude-work", "claude-personal"). The original code checked `provider === "claudeAgent"` which is a constant discriminant on all Claude adapters (as defined in `ClaudeAdapterShape`). After this change, effort and model settings will only be applied when an instance happens to have the exact ID "claudeAgent", causing `rawEffort` to be `null` and `claudeModel` to be `undefined` for all other Claude instances.

Comment threadapps/web/src/workspace/store.ts Outdated
juliusmarmingeand others added 5 commits April 27, 2026 11:22
Introduce two open branded slugs in contracts:
- `ProviderDriverId` — implementation kind (codex, claudeAgent, forks…)
- `ProviderInstanceId` — user-defined routing key. Threads, sessions,
runtime events, and persisted bindings now key by instance id so a
user can run multiple instances of the same driver with independent
config (e.g. `codex_personal` + `codex_work`).
Rewrite `ModelSelection` as `{instanceId, model, options?}`. Legacy
`{provider, model}` wire shapes decode forward via a transform so
persisted thread state, session bindings, and older clients round-trip
without data loss.
Add the instance-config envelope `ProviderInstanceConfig` and
`ServerSettings.providerInstances: Record<InstanceId, Config>` alongside
the legacy per-driver `providers` struct. Both exist side-by-side until
Pass 2 migrates callers.
Scaffold the Pass 2 SPI types (no runtime implementations yet):
- `ProviderDriver<Config, R>` — plain-value driver factory interface
- `ProviderInstance` — the per-instance record bundling snapshot,
adapter, and textGeneration as captured closures
- `ProviderInstanceRegistry` service tag
Migrate ~55 callsites reading `modelSelection.provider` to `.instanceId`
across server, web, and shared. Update test fixtures; fix a latent
runtime bug in `composerDraftStore` where a stale `NormalizedModelSelection`
type claimed a `.provider` field that `createModelSelection` no longer
writes.
All workspaces typecheck; full monorepo tests green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a per-instance card beneath the default driver cards in the
Providers settings panel, letting users rename, toggle, edit, and
delete configured provider instances. A "+" action in the section
header opens the existing `AddProviderInstanceDialog`. Default
cards now also show the driver icon, matching the instance cards.
Extracts shared presentation helpers into `providerStatus.ts`
(status-dot colors, summary headline/detail) and driver metadata
into `providerDriverMeta.ts` (icon + editable-field definitions)
so the default cards, the add dialog, and the instance cards all
use the same keys and visual language. Unknown drivers render a
read-only notice so forked configs round-trip safely.
Introduces `DraftInput`, a thin wrapper over `Input` that buffers
keystrokes in local state and only calls `onCommit` on blur or
Enter. Swaps every provider-config input — binary path, server
URL/password, CODEX_HOME, Claude launch args, Add-project base
directory, display name, and per-driver instance fields — from
the previous per-keystroke `updateSettings` call to a commit-on-
blur flow. This avoids re-rendering the entire settings panel
and firing a server RPC for every character, which made typing
in these fields feel laggy whenever the server hydration echoed
back.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the per-driver `Provider*Registry`/`Provider*Provider` services
with a central `ProviderInstanceRegistry` driven by the settings-level
`providerInstances` map plus a per-driver catalog. Hydration promotes
the legacy `ServerSettings.providers[kind]` blobs into explicit
`providerInstances[defaultInstanceId]` entries on first write so custom
and default slots share the same storage schema.
On the web side, the settings panel renders every built-in default and
every user-authored custom instance through the same
`ProviderInstanceCard` — the only remaining asymmetries are the delete
button (custom only) and the reset-to-factory button (default only).
Custom instances can carry their own `customModels` list; `modelSelection`
prefers the per-instance blob with a legacy per-kind fallback. Adds an
`Early Access` badge plumbing for drivers that need it (Cursor today)
and a contracts-level `PROVIDER_DISPLAY_NAMES` map so default slot
labels pick up canonical brand casing ("OpenCode", not "Opencode").
Includes supporting server refactors (`TextGenerationLive` routing,
session runtime migration 026, scoped safe teardown, provider event
loggers) and parallel work on the workspace feature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- add workspace terminal and pane keybinding commands
- persist workspace document state in browser storage
- tighten provider instance/model selection hydration
Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarmingeforce-pushed the t3code/provider-instance-registry branch from fb6a229 to 0c868cbCompareApril 27, 2026 18:26
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx
- persist provider_instance_id on thread sessions and projections
- route turns by instance-compatible continuation identity
- add Codex shadow-home layout for shared auth continuity
- carry continuation group keys through server provider snapshots
- respect group affinity when selecting locked models in the UI
- update provider instance tests and contract decoding
- Thread per-instance environment state through provider drivers and runtimes
- Add Claude HOME handling and cache key isolation
- Cover new environment behavior with tests

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Disabled instance check missing in text generation routing
    • Added an instance.enabled check in resolveInstance so disabled provider instances now surface a TextGenerationError instead of silently delegating to the underlying text generation closure.
  • ✅ Fixed: Empty registry when initial settings read fails
    • Changed the fallback from an empty ProviderInstanceConfigMap to DEFAULT_SERVER_SETTINGS so that built-in driver default instances are always materialized even when the initial settings read fails.

Create PR

Or push these changes by commenting:

@cursor push 1a12ea9124
Preview (1a12ea9124)
diff --git a/apps/server/src/git/Layers/TextGenerationLive.ts b/apps/server/src/git/Layers/TextGenerationLive.ts--- a/apps/server/src/git/Layers/TextGenerationLive.ts+++ b/apps/server/src/git/Layers/TextGenerationLive.ts@@ -51,7 +51,14 @@
registry.getInstance(instanceId).pipe(
Effect.flatMap((instance) =>
instance
- ? Effect.succeed(instance.textGeneration)+ ? instance.enabled+ ? Effect.succeed(instance.textGeneration)+ : Effect.fail(+ new TextGenerationError({+ operation,+ detail: `Provider instance '${instanceId}' is disabled.`,+ }),+ )
: Effect.fail(
new TextGenerationError({
operation,
diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts@@ -42,6 +42,7 @@
* @module provider/Layers/ProviderInstanceRegistryHydration
*/
import {
+ DEFAULT_SERVER_SETTINGS,
defaultInstanceIdForDriver,
type ProviderInstanceConfig,
type ProviderInstanceConfigMap,
@@ -158,13 +159,10 @@
> = Layer.unwrap(
Effect.gen(function* () {
const serverSettings = yield* ServerSettingsService;
- const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe(- Effect.orElseSucceed(() => undefined),+ const initialSettings: ServerSettings = yield* serverSettings.getSettings.pipe(+ Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS),
);
- const initialConfigMap =- initialSettings === undefined- ? ({} as ProviderInstanceConfigMap)- : deriveProviderInstanceConfigMap(initialSettings);+ const initialConfigMap = deriveProviderInstanceConfigMap(initialSettings);
const mutableLayer = ProviderInstanceRegistryMutableLayer({
drivers: BUILT_IN_DRIVERS,

You can send follow-ups to the cloud agent here.

Comment threadapps/server/src/git/Layers/TextGenerationLive.ts
Comment threadapps/server/src/provider/Layers/CursorProvider.ts
Comment threadapps/web/src/components/settings/ProviderInstanceCard.tsx Outdated
Comment threadapps/server/src/serverSettings.ts
Comment threadapps/web/src/components/settings/SettingsPanels.tsx Outdated
- Stop probing `claude auth status` for account metadata
- Surface Claude instance status in settings with the provider icon badge
- Thread adapter-bound instance ids through Claude, Codex, and OpenCode
- Fix provider registry subscription startup to avoid missed change events
- Add coverage for custom instance model and option mapping
- Attribute provider adapter errors to the resolved instance or driver when lookups fail
- Add tests covering provider error label fallbacks
- Derive Claude auth and slash commands from initialization results
- Keep provider registry tests aligned with the new status flow
- include `enabled` on provider instance snapshots and adapters
- update provider tests and Claude status handling for the new field
juliusmarmingeand others added 4 commits April 29, 2026 10:57
- keep cached providers available when refresh or sync errors occur
- update provider service to rely on instance enablement
- add coverage for refresh recovery and shutdown error handling
- Persist hidden, favorite, and ordered models per provider instance
- Reuse shared sorting for picker and settings views
- Keep composer and selection state aligned with instance-specific preferences
Co-authored-by: codex <codex@users.noreply.github.com>
- make Claude/Codex home resolution effectful and injectable
- tighten shadow-home materialization and provider status caching
- update tests for the new path and filesystem behavior
Comment threadapps/server/src/provider/Layers/OpenCodeAdapter.ts Outdated
- Harden provider instance migrations for partial upgrades
- Update Codex home layout handling and add provider picker UI
Comment threadapps/web/index.html Outdated
- Reject mismatched instance IDs only when a model selection is present
- Preserve turns that do not specify a model selection
- Drop the external ui.sh picker script from `apps/web/index.html`
@juliusmarminge
juliusmarminge merged commit 08e6d4c into mainApr 29, 2026
12 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/provider-instance-registry branch April 29, 2026 23:13
slan48 pushed a commit to slan48/t3code that referenced this pull request Apr 30, 2026
Multi-Provider PR (pingdotgg#2277) rewrote deriveEffectiveComposerModelState to
route through resolveAppModelSelection*, which is provider-instance
aware and naturally rejects cross-provider carry-overs. Our item 3
guard and its tests are now obsolete; remove the test block and
renumber strategy doc items 4-8 accordingly.
Also fix the verification command in the strategy doc — plain `bun
test` invokes Bun's runner, not vitest, and produces spurious failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
Brings the orchestrator/external-session feature in line with upstream
main's multi-provider architecture (pingdotgg#2277):
- ProviderDriverKind is now a branded type. All `provider: "claudeAgent"`
literals updated to `ProviderDriverKind.make(...)`. ModelSelection now
uses `instanceId` (ProviderInstanceId) instead of the legacy `provider`
field; resume/spawn flows go through `defaultInstanceIdForDriver`.
- ClaudeAdapter no longer yields OrchestratorService /
ProviderSessionDirectory / ProjectionSnapshotQuery /
OrchestrationEngineService directly. Instead a new
ClaudeOrchestratorBridge service bundles the four into one tag, kept
narrow on ClaudeDriverEnv. The driver yields the bridge and passes it
to makeClaudeAdapter via the new `orchestrator` adapter option.
- New apps/server/src/orchestrator/ClaudeOrchestratorBridge.ts. Provides
a Live layer composed at the server's runtime layer plus a
NoOpClaudeOrchestratorBridgeLayer for tests that don't exercise the
master/worker path.
- server.ts: collapses the per-service `Layer.provideMerge` calls into a
single `ProjectAuxiliaryServicesLive` group so the runtime pipe stays
under TypeScript's 20-arg overload limit.
- Tests: ClaudeAdapter, ProviderRegistry, ProviderInstanceRegistryLive,
server tests all updated to mock the new bridge / branded provider.
0xpaperhead added a commit to 0xpaperhead/t3code that referenced this pull request May 1, 2026
The multi-provider refactor (pingdotgg#2277) added a `providerInstanceId`
routing key as a required field on every ProviderSessionRuntime row.
Three of our binding-upsert call sites still passed only the legacy
`provider` driver kind, so attempts to spawn a worker, promote a
fresh thread, or resume an external CLI session crashed with:
Provider validation failed in ProviderSessionDirectory.upsert:
providerInstanceId is required for provider session runtime bindings.
Wire `defaultInstanceIdForDriver(driverKind)` through:
- workerMcpServer.spawn_worker — was failing every time the master
tried to spawn a worker; the master typically fell back to Claude
Code's built-in Agent tool after seeing the validation error.
- OrchestratorService.promote/demote — the fresh-binding branch
failed; the existing-binding branch now also forwards the
providerInstanceId from the existing row (or backfills via the
default if a legacy row didn't have one).
- ws.ts externalSessions.bindResume — same fix; was already
computing providerInstanceId for the modelSelection but not
threading it into the binding upsert.
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 2, 2026
Registered feature dependency DAG. Updated skills to v0.6.1.
Impact assessment for upstream Multi-Provider (pingdotgg#2277).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
32 upstream commits analyzed including Multi-Provider (pingdotgg#2277).
ProviderKind replaced with open ProviderDriverKind. New Driver
pattern replaces Service tags. Detailed step-by-step handoff
for creating CopilotDriver.ts and re-wiring all features.
Skills updated to v0.6.1. Feature deps registered.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
Complete reconciliation against upstream Multi-Provider (pingdotgg#2277):
- New CopilotDriver.ts following the Driver pattern (ClaudeDriver template)
- CopilotAdapter adapted: makeCopilotAdapter exported, ProviderDriverKind
- CopilotProvider adapted: ServerProviderDraft return type
- CopilotTextGeneration moved to textGeneration/ directory
- CopilotSettings using makeProviderSettingsSchema with form annotations
- Registered in BUILT_IN_DRIVERS, providerDriverMeta, session-logic
- All model defaults, aliases, display names added
- Build script: asarUnpack, npm force-install for cross-platform
- SDK v0.3.0 dependency
All adapter-internal features preserved (plan-compaction, turn-timing,
skill-discovery, command-events, resource-events, skill-controls).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
unwarlikepoodle pushed a commit to tesseracode/t3code that referenced this pull request May 3, 2026
All features reconciled against upstream Multi-Provider (pingdotgg#2277).
Cross-commit patches expected — will be scoped per-feature on
future individual changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Morphexe pushed a commit to Morphexe/t3code that referenced this pull request May 4, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
NeilTheFisher pushed a commit to NeilTheFisher/t3code that referenced this pull request Aug 18, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
darjss pushed a commit to darjss/t3code that referenced this pull request Aug 26, 2026
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: codex <codex@users.noreply.github.com>
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
filipef101 pushed a commit to filipef101/t3code that referenced this pull request Aug 26, 2026
A logged-out CLAUDE_CONFIG_DIR still initializes through the Claude Agent
SDK and reports {tokenSource: "none", apiProvider: "firstParty"}, which
the provider snapshot read as "authenticated". That was survivable when
every install had exactly one Claude; a second provider instance points
CLAUDE_CONFIG_DIR at a fresh directory that is logged out by
construction, so the provider list claimed "Authenticated" while every
turn died with "Not logged in - Please run /login".
Restore the `claude auth status` probe that pingdotgg#2277 dropped, run it with
the instance's own environment, and name that instance's config directory
in the login hint so the user logs in the failing instance rather than
the default one.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL1,000+ changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@juliusmarminge