feat(opencode): session-level model switching API#22044
feat(opencode): session-level model switching API#22044v1truv1us wants to merge 7 commits intoanomalyco:devfrom
Conversation
Adds first-class session model switching that allows plugins to dynamically change the active model for a session. Model changes persist across turns, update the UI, and are inherited by subagents. Key changes: - Add model/modelVariant fields to Session.Info with database migration - Add Session.setModel() backend API for programmatic model changes - Add PATCH /session/:sessionID/model HTTP route - Chat.message hook mutations flow through Session.setModel() - TUI syncs session model via reactive effect - Session.fork copies model from original session - Session.create inherits parent model for child sessions - Add adversarial-review plugin example using model routing - Add model-rewrite plugin for env var based model overrides - Remove redundant ModelChanged event (use Event.Updated instead) - Remove dead session.model hook type Tests: 19 new tests for model routing and model-rewrite plugin
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Remove hard-coded local plugin path from .opencode/opencode.jsonc - Fix unreachable concurrent session guard in adversarial-review plugin - Fix config load failure message (disabled -> running with defaults) - Add model validation to Session.setModel (logs warning if not found) - Sync modelVariant in TUI session model effect
Remove adversarial-review and model-rewrite example plugins along with related config, specs, tests, and state files. These should live in a dedicated plugins repository.
Merge dev's Effect architecture changes with session model switching feature. Key resolutions: - package.json: take dev's newer dependency versions - session.sql.ts: keep model/model_variant columns, use dev's Permission type - session/index.ts: integrate setModel into Effect layer with model inheritance in create/fork, convert setModel to Effect-based implementation - session/prompt.ts: add model persistence via setModel after user message save in createUserMessage Effect, keep dev's Effect-based insertReminders
There was a problem hiding this comment.
Pull request overview
Adds a session-level “active model” concept to OpenCode so plugins and clients can switch which provider/model a session uses for future turns, with persistence and a small HTTP/TUI surface area.
Changes:
- Persist
model+modelVariantonSession.Info(SQLite columns + migration) and expose aSession.setModel()API. - Trigger session model updates after saving a user message when
message.modelis present (enabling plugin-driven routing viachat.messagemutation). - Add
PATCH /session/:sessionID/modelendpoint and TUI syncing of model changes from session state.
Reviewed changes
Copilot reviewed 8 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/opencode/src/session/index.ts | Adds model/modelVariant to Session mapping + introduces Session.setModel() with validation/warn behavior. |
| packages/opencode/src/session/session.sql.ts | Extends session table schema with model (json text) and model_variant (text). |
| packages/opencode/migration/20260401004113_add_session_model/migration.sql | Adds SQLite columns for session model + variant. |
| packages/opencode/migration/20260401004113_add_session_model/snapshot.json | Updates migration snapshot to include the new columns. |
| packages/opencode/src/session/prompt.ts | After persisting a user message, calls Session.setModel() if the message includes a model. |
| packages/opencode/src/server/routes/session.ts | Adds PATCH /:sessionID/model route to update the session’s active model. |
| packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | Adds reactive syncing of local model selection from session state changes. |
| packages/opencode/test/session/set-model.test.ts | Adds tests for persistence and “unknown model” behavior of Session.setModel(). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { Config } from "../config/config" | ||
| import { Flag } from "../flag/flag" | ||
| import { Installation } from "../installation" | ||
| import { Provider } from "../provider/provider" | ||
| import { ModelID, ProviderID } from "../provider/schema" |
There was a problem hiding this comment.
The new relative imports for Provider/ProviderID/ModelID duplicate existing imports later in this file (via the @/ alias). This will cause TS "duplicate identifier"/conflicting import errors and can also result in two distinct module instances depending on TS path mapping. Remove one set and use a single import path consistently (prefer the existing @/ imports used elsewhere in this file).
| const modelKey = `${sessionModel.providerID}/${sessionModel.modelID}/${sessionVariant ?? ""}` | ||
| if (modelKey === lastSessionModelID) return | ||
|
|
||
| lastSessionModelID = modelKey | ||
| local.model.set(sessionModel) | ||
| if (sessionVariant !== undefined && sessionVariant !== null) { | ||
| local.model.variant.set(sessionVariant) | ||
| } |
There was a problem hiding this comment.
When the session model changes to a value with no modelVariant (null/undefined), this effect never clears the local variant, so the UI can keep showing/sending a stale variant from a previous model selection. Update the effect to explicitly clear/reset local.model.variant when sessionVariant is nullish (and consider handling the case where sessionModel becomes undefined if that’s possible).
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
639c78f to
f51bc9e
Compare
|
This is my first real open source contribution, so I hope this is helpful! If there is anything I can do to improve it, please let me know! |
Issue for this PR
Closes #18793
Type of change
What does this PR do?
This adds session-level model persistence so plugins can change the active session model for future turns. The main change is
Session.setModel(), plus session storage formodelandmodelVariant, aPATCH /session/:sessionID/modelroute, and TUI sync from session state. Thechat.messagehook remains the integration point for plugins: if a plugin mutatesoutput.message.model, that model is saved on the user message and then persisted onto the session.How did you verify your code works?
Session.setModel()persistence and variant persistenceupstream/devScreenshots / recordings
No UI recording. The UI change is limited to syncing the selected model/variant from persisted session state.
Checklist