Replies: 1 comment
|
Thanks for requesting provider switching with conversation carry-over. The orchestrator V2 work has now merged in #2829. V2 now supports changing provider/model within a thread and carrying a budgeted selection of the conversation into the new provider session. That replaces the old incompatible-provider continuation restriction. Closing this as delivered. If a specific part is still missing in a build containing V2, please open a focused follow-up with the provider/version and the behavior you are seeing. |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
What I hit as a user
I run a Claude instance and an OpenRouter instance (opencode driver) side by side, plus local Ollama. Partway through a thread I often want to hand the same conversation to a different provider — start on Claude, continue on OpenRouter, or drop to a local model for something cheap. Today a thread is pinned to whatever provider it started on:
Thread '…' is bound to driver 'claudeAgent' and cannot switch to 'opencode'.Thread '…' cannot switch from instance '…' to '…' because their provider resume state is incompatible.Thread '…' cannot switch models after the conversation has started. Start a new thread to use '…'.The model picker also disables the entry outright with "Start a new chat to change models." The only workaround is a new thread, which loses the very context I wanted the second provider to have.
Why the guards exist — I don't think they're a bug
Reading
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts, these look deliberate. A thread's conversation lives inside the provider's own session (codex thread id, Claude session, opencode session), andcontinuationIdentity.continuationKeydecides whether resume state can carry across. Two different drivers share no resume state, so allowing the switch naively would silently hand the new provider an empty conversation. Refusing is the safe default. I'm not proposing to just delete the checks.Proposal — treat a cross-provider switch as a re-seat, not a resume
continuationKeydiffers, stop the current provider session instead of erroring.This preserves the invariant the guards protect — no provider is ever asked to resume state it doesn't own — while letting the conversation continue.
A second bug the same mechanism fixes
Providers with no resume cursor (opencode, ollama, llamacpp) lose context without any provider switch. Their history is in-memory only, so after an app relaunch — or after the 30-minute idle session reaper — the next turn starts blank and the model answers that it has no prior context, while the full transcript sits in SQLite. Replaying persisted history whenever a session starts without a resume cursor fixes that too. Providers that resume natively (codex, claudeAgent) are untouched, because they return a cursor.
What I actually changed
Three changes, described against the source locations they correspond to:
ProviderCommandReactor.ts—ensureSessionForThread. The driver-mismatch andcontinuationKey-mismatch branches no longer raise. They set acrossProviderSwitchflag, stop the existing session, and fall through to the existing restart path with no resume cursor.ProviderCommandReactor.ts—rejectStartedThreadModelChangeIfRequired. Instead of raising for providers flaggedrequiresNewThreadForModelChange, the result folds into the existingshouldRestartForModelChangecondition, so those providers get a clean restart rather than an error. Client side,getStartedThreadModelChangeBlockReason(apps/web/src/components/ChatView.logic.ts:408) is what greys the picker out; it needs to stop returning a block reason for the same case.resumeCursorwhile the thread has persisted messages. On the nextbuildSendTurnRequestForThread, the stored transcript is rendered oldest-first into a<conversation_history>prefix (~24k char budget, oldest dropped first, the in-flight user message excluded) and prepended to the outgoing turn text. The flag is consumed on use, so it applies once per fresh session.How it was validated, and on what build
Originally built and run against 0.0.28 (build commit
ecb35f758399, 2026-07-15). I have since re-applied the same changes to v0.0.34-nightly.20260817.1120 on macOS arm64 and re-tested there. The changes were applied to the packaged bundle rather than a source build, so the descriptions above are the equivalent source-level changes rather than a diff I can paste verbatim.What I can state from the 0.0.34 run: switching a started thread between two provider instances no longer raises. I moved one live thread across instances four times (
opencode->openrouter->opencode->openrouter) and never hitis bound to driver,resume state is incompatible, orcannot switch models after the conversation has started. Each switch restarted the session and the turn was dispatched to the newly selected provider.What I could not re-verify on 0.0.34: the history replay actually landing in the second provider's context. Every turn after the switch failed for reasons outside T3 Code — my OpenRouter account restricts allowed upstream providers and then ran out of credits, and the
opencodeinstance wants a billing method. So on this build I have verified the switch path but not the recall. On 0.0.28 the recall did work (the second provider answered using earlier turns), which is what motivated the change.For completeness: the native Claude driver was unavailable on my machine during the test (
Claude Agent CLI is installed but failed to run. Timed out while running command.), so the cross-driver case specifically was exercised on 0.0.28 rather than 0.0.34. The cross-instance case on 0.0.34 takes the same code path, since two instances of one driver have differentcontinuationKeys.Open questions
Raising this here rather than as a PR, per CONTRIBUTING — it's a design change, not a small fix. Happy to leave it as a suggestion, or to put up a scoped PR if it's a direction you'd want.
All reactions