diff --git a/bun.lock b/bun.lock
index b6318af40ed..9cd79e5d542 100644
--- a/bun.lock
+++ b/bun.lock
@@ -255,18 +255,17 @@
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57",
"@ai-sdk/anthropic": "2.0.56",
- "@ai-sdk/azure": "2.0.73",
+ "@ai-sdk/azure": "2.0.82",
"@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.21",
"@ai-sdk/deepinfra": "1.0.30",
"@ai-sdk/gateway": "2.0.23",
- "@ai-sdk/google": "2.0.44",
+ "@ai-sdk/google": "2.0.49",
"@ai-sdk/google-vertex": "3.0.81",
"@ai-sdk/groq": "2.0.33",
- "@ai-sdk/mcp": "0.0.8",
"@ai-sdk/mistral": "2.0.26",
"@ai-sdk/openai": "2.0.71",
- "@ai-sdk/openai-compatible": "1.0.27",
+ "@ai-sdk/openai-compatible": "1.0.29",
"@ai-sdk/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.19",
@@ -541,7 +540,7 @@
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-uyyaO4KhxoIKZztREqLPh+6/K3ZJx/rp72JKoUEL9/kC+vfQTThUfPnY/bUryUpcnawx8IY/tSoYNOi/8PCv7w=="],
- "@ai-sdk/azure": ["@ai-sdk/azure@2.0.73", "", { "dependencies": { "@ai-sdk/openai": "2.0.71", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LpAg3Ak/V3WOemBu35Qbx9jfQfApsHNXX9p3bXVsnRu3XXi1QQUt5gMOCIb4znPonz+XnHenIDZMBwdsb1TfRQ=="],
+ "@ai-sdk/azure": ["@ai-sdk/azure@2.0.82", "", { "dependencies": { "@ai-sdk/openai": "2.0.80", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Bpab51ETBB4adZC1xGMYsryL/CB8j1sA+t5aDqhRv3t3WRLTxhaBDcFKtQTIuxiEQTFosz9Q2xQqdfBvQm5jHw=="],
"@ai-sdk/cerebras": ["@ai-sdk/cerebras@1.0.33", "", { "dependencies": { "@ai-sdk/openai-compatible": "1.0.29", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2gSSS/7kunIwMdC4td5oWsUAzoLw84ccGpz6wQbxVnrb1iWnrEnKa5tRBduaP6IXpzLWsu8wME3+dQhZy+gT7w=="],
@@ -551,14 +550,12 @@
"@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.23", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg=="],
- "@ai-sdk/google": ["@ai-sdk/google@2.0.44", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw=="],
+ "@ai-sdk/google": ["@ai-sdk/google@2.0.49", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-efwKk4mOV0SpumUaQskeYABk37FJPmEYwoDJQEjyLRmGSjtHRe9P5Cwof5ffLvaFav2IaJpBGEz98pyTs7oNWA=="],
"@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.81", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.50", "@ai-sdk/google": "2.0.44", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18", "google-auth-library": "^9.15.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yrl5Ug0Mqwo9ya45oxczgy2RWgpEA/XQQCSFYP+3NZMQ4yA3Iim1vkOjVCsGaZZ8rjVk395abi1ZMZV0/6rqVA=="],
"@ai-sdk/groq": ["@ai-sdk/groq@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FWGl7xNr88NBveao3y9EcVWYUt9ABPrwLFY7pIutSNgaTf32vgvyhREobaMrLU4Scr5G/2tlNqOPZ5wkYMaZig=="],
- "@ai-sdk/mcp": ["@ai-sdk/mcp@0.0.8", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "pkce-challenge": "^5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9y9GuGcZ9/+pMIHfpOCJgZVp+AZMv6TkjX2NVT17SQZvTF2N8LXuCXyoUPyi1PxIxzxl0n463LxxaB2O6olC+Q=="],
-
"@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.26", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jxDB++4WI1wEx5ONNBI+VbkmYJOYIuS8UQY13/83UGRaiW7oB/WHiH4ETe6KzbKpQPB3XruwTJQjUMsMfKyTXA=="],
"@ai-sdk/openai": ["@ai-sdk/openai@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-D4zYz2uR90aooKQvX1XnS00Z7PkbrcY+snUvPfm5bCabTG7bzLrVtD56nJ5bSaZG8lmuOMfXpyiEEArYLyWPpw=="],
@@ -3901,22 +3898,16 @@
"@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
- "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
-
- "@ai-sdk/azure/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
+ "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.80", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tNHuraF11db+8xJEDBoU9E3vMcpnHFKRhnLQ3DQX2LnEzfPB9DksZ8rE+yVuDN1WRW9cm2OWAhgHFgVKs7ICuw=="],
"@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"@ai-sdk/deepinfra/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
- "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
-
"@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.50", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw=="],
"@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ=="],
- "@ai-sdk/mcp/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
-
"@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
"@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-BoQZtGcBxkeSH1zK+SRYNDtJPIPpacTeiMZqnG4Rv6xXjEwM0FH4MGs9c+PlhyEWmQCzjRM2HAotEydFhD4dYw=="],
@@ -4309,7 +4300,7 @@
"opencode/@ai-sdk/openai": ["@ai-sdk/openai@2.0.71", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tg+gj+R0z/On9P4V7hy7/7o04cQPjKGayMCL3gzWD/aNGjAKkhEnaocuNDidSnghizt8g2zJn16cAuAolnW+qQ=="],
- "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bpYruxVLhrTbVH6CCq48zMJNeHu6FmHtEedl9FXckEgcIEAi036idFhJlcRwC1jNCwlacbzb8dPD7OAH1EKJaQ=="],
+ "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.29", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cZUppWzxjfpNaH1oVZ6U8yDLKKsdGbC9X0Pex8cG9CXhKWSoVLLnW1rKr6tu9jDISK5okjBIW/O1ZzfnbUrtEw=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
@@ -4913,8 +4904,6 @@
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
- "opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
-
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
diff --git a/nix/hashes.json b/nix/hashes.json
index 52414e77a73..8c2a305e2a5 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -1,3 +1,3 @@
{
- "nodeModules": "sha256-fFIJ8CneAo0gHRK8ghsYnEQw/Dnmu00xN5MHgPam8hg="
+ "nodeModules": "sha256-kgJKAqccRJOQcJ8+/j+lGe7T6WZuQqYv6UGl3jvI9wQ="
}
diff --git a/packages/opencode/bunfig.toml b/packages/opencode/bunfig.toml
index 9afe227b326..c227328d5ae 100644
--- a/packages/opencode/bunfig.toml
+++ b/packages/opencode/bunfig.toml
@@ -2,5 +2,6 @@ preload = ["@opentui/solid/preload"]
[test]
preload = ["./test/preload.ts"]
+timeout = 10000 # 10 seconds (default is 5000ms)
# Enable code coverage
coverage = true
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index e9650145a3a..952f0bce2ce 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -50,19 +50,18 @@
"@actions/github": "6.0.1",
"@agentclientprotocol/sdk": "0.5.1",
"@ai-sdk/amazon-bedrock": "3.0.57",
+ "@ai-sdk/azure": "2.0.82",
"@ai-sdk/anthropic": "2.0.56",
- "@ai-sdk/azure": "2.0.73",
"@ai-sdk/cerebras": "1.0.33",
"@ai-sdk/cohere": "2.0.21",
"@ai-sdk/deepinfra": "1.0.30",
"@ai-sdk/gateway": "2.0.23",
- "@ai-sdk/google": "2.0.44",
+ "@ai-sdk/google": "2.0.49",
"@ai-sdk/google-vertex": "3.0.81",
"@ai-sdk/groq": "2.0.33",
- "@ai-sdk/mcp": "0.0.8",
"@ai-sdk/mistral": "2.0.26",
"@ai-sdk/openai": "2.0.71",
- "@ai-sdk/openai-compatible": "1.0.27",
+ "@ai-sdk/openai-compatible": "1.0.29",
"@ai-sdk/perplexity": "2.0.22",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/provider-utils": "3.0.19",
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index f819746d53c..6eec5d1d622 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
@@ -167,6 +167,13 @@ export function Prompt(props: PromptProps) {
if (!props.disabled) input.cursorColor = theme.text
})
+ const lastUserMessage = createMemo(() => {
+ if (!props.sessionID) return undefined
+ const messages = sync.data.message[props.sessionID]
+ if (!messages) return undefined
+ return messages.findLast((m) => m.role === "user")
+ })
+
const [store, setStore] = createStore<{
prompt: PromptInfo
mode: "normal" | "shell"
@@ -184,6 +191,26 @@ export function Prompt(props: PromptProps) {
interrupt: 0,
})
+ createEffect(() => {
+ const msg = lastUserMessage()
+ if (!msg) return
+
+ // Set agent from last message
+ if (msg.agent) {
+ local.agent.set(msg.agent)
+ }
+
+ // Set model from last message
+ if (msg.model) {
+ local.model.set(msg.model)
+ }
+
+ // Set variant from last message
+ if (msg.variant) {
+ local.model.variant.set(msg.variant)
+ }
+ })
+
command.register(() => {
return [
{
@@ -562,6 +589,7 @@ export function Prompt(props: PromptProps) {
// Capture mode before it gets reset
const currentMode = store.mode
+ const variant = local.model.variant.current()
if (store.mode === "shell") {
sdk.client.session.shell({
@@ -590,6 +618,7 @@ export function Prompt(props: PromptProps) {
agent: local.agent.current().name,
model: `${selectedModel.providerID}/${selectedModel.modelID}`,
messageID,
+ variant,
})
} else {
sdk.client.session.prompt({
@@ -598,6 +627,7 @@ export function Prompt(props: PromptProps) {
messageID,
agent: local.agent.current().name,
model: selectedModel,
+ variant,
parts: [
{
id: Identifier.ascending("part"),
@@ -718,6 +748,13 @@ export function Prompt(props: PromptProps) {
return local.agent.color(local.agent.current().name)
})
+ const showVariant = createMemo(() => {
+ const variants = local.model.variant.list()
+ if (variants.length === 0) return false
+ const current = local.model.variant.current()
+ return !!current
+ })
+
const spinnerDef = createMemo(() => {
const color = local.agent.color(local.agent.current().name)
return {
@@ -843,6 +880,12 @@ export function Prompt(props: PromptProps) {
return
}
}
+ if (keybind.match("variant_cycle", e)) {
+ e.preventDefault()
+ if (local.model.variant.list().length === 0) return
+ local.model.variant.cycle()
+ return
+ }
if (store.mode === "normal") autocomplete.onKeyDown(e)
if (!autocomplete.visible) {
if (
@@ -958,6 +1001,12 @@ export function Prompt(props: PromptProps) {
{local.model.parsed().model}
{local.model.parsed().provider}
+
+ ยท
+
+ {local.model.variant.current()}
+
+
diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx
index 55c04621ef3..cac542d54fc 100644
--- a/packages/opencode/src/cli/cmd/tui/context/local.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx
@@ -33,24 +33,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
}
- // Automatically update model when agent changes
- createEffect(() => {
- const value = agent.current()
- if (value.model) {
- if (isModelValid(value.model))
- model.set({
- providerID: value.model.providerID,
- modelID: value.model.modelID,
- })
- else
- toast.show({
- variant: "warning",
- message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
- duration: 3000,
- })
- }
- })
-
const agent = iife(() => {
const agents = createMemo(() => sync.data.agent.filter((x) => x.mode !== "subagent" && !x.hidden))
const [agentStore, setAgentStore] = createStore<{
@@ -120,11 +102,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
providerID: string
modelID: string
}[]
+ variant: Record
}>({
ready: false,
model: {},
recent: [],
favorite: [],
+ variant: {},
})
const file = Bun.file(path.join(Global.Path.state, "model.json"))
@@ -135,6 +119,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
JSON.stringify({
recent: modelStore.recent,
favorite: modelStore.favorite,
+ variant: modelStore.variant,
}),
)
}
@@ -144,6 +129,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
.then((x) => {
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
+ if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
})
.catch(() => {})
.finally(() => {
@@ -218,6 +204,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return {
provider: "Connect a provider",
model: "No provider selected",
+ reasoning: false,
}
}
const provider = sync.data.provider.find((x) => x.id === value.providerID)
@@ -225,6 +212,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return {
provider: provider?.name ?? value.providerID,
model: info?.name ?? value.modelID,
+ reasoning: info?.capabilities?.reasoning ?? false,
}
}),
cycle(direction: 1 | -1) {
@@ -309,6 +297,46 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
save()
})
},
+ variant: {
+ current() {
+ const m = currentModel()
+ if (!m) return undefined
+ const key = `${m.providerID}/${m.modelID}`
+ return modelStore.variant[key]
+ },
+ list() {
+ const m = currentModel()
+ if (!m) return []
+ const provider = sync.data.provider.find((x) => x.id === m.providerID)
+ const info = provider?.models[m.modelID]
+ if (!info?.variants) return []
+ return Object.entries(info.variants)
+ .filter(([_, v]) => !v.disabled)
+ .map(([name]) => name)
+ },
+ set(value: string | undefined) {
+ const m = currentModel()
+ if (!m) return
+ const key = `${m.providerID}/${m.modelID}`
+ setModelStore("variant", key, value)
+ save()
+ },
+ cycle() {
+ const variants = this.list()
+ if (variants.length === 0) return
+ const current = this.current()
+ if (!current) {
+ this.set(variants[0])
+ return
+ }
+ const index = variants.indexOf(current)
+ if (index === -1 || index === variants.length - 1) {
+ this.set(undefined)
+ return
+ }
+ this.set(variants[index + 1])
+ },
+ },
}
})
@@ -329,6 +357,24 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
},
}
+ // Automatically update model when agent changes
+ createEffect(() => {
+ const value = agent.current()
+ if (value.model) {
+ if (isModelValid(value.model))
+ model.set({
+ providerID: value.model.providerID,
+ modelID: value.model.modelID,
+ })
+ else
+ toast.show({
+ variant: "warning",
+ message: `Agent ${value.name}'s configured model ${value.model.providerID}/${value.model.modelID} is not valid`,
+ duration: 3000,
+ })
+ }
+ })
+
const result = {
model,
agent,
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 0132bb91daf..4f4ff118862 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -490,6 +490,7 @@ export namespace Config {
agent_list: z.string().optional().default("a").describe("List agents"),
agent_cycle: z.string().optional().default("tab").describe("Next agent"),
agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"),
+ variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"),
input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"),
input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"),
input_submit: z.string().optional().default("return").describe("Submit input"),
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index 62bc5beaa00..4150703d2e3 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -34,6 +34,7 @@ import { createCohere } from "@ai-sdk/cohere"
import { createGateway } from "@ai-sdk/gateway"
import { createTogetherAI } from "@ai-sdk/togetherai"
import { createPerplexity } from "@ai-sdk/perplexity"
+import { ProviderTransform } from "./transform"
export namespace Provider {
const log = Log.create({ service: "provider" })
@@ -404,6 +405,16 @@ export namespace Provider {
},
}
+ export const Variant = z
+ .object({
+ disabled: z.boolean(),
+ })
+ .catchall(z.any())
+ .meta({
+ ref: "Variant",
+ })
+ export type Variant = z.infer
+
export const Model = z
.object({
id: z.string(),
@@ -467,6 +478,7 @@ export namespace Provider {
options: z.record(z.string(), z.any()),
headers: z.record(z.string(), z.string()),
release_date: z.string(),
+ variants: z.record(z.string(), Variant).optional(),
})
.meta({
ref: "Model",
@@ -489,7 +501,7 @@ export namespace Provider {
export type Info = z.infer
function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model {
- return {
+ const m: Model = {
id: model.id,
providerID: provider.id,
name: model.name,
@@ -546,7 +558,12 @@ export namespace Provider {
interleaved: model.interleaved ?? false,
},
release_date: model.release_date,
+ variants: {},
}
+
+ m.variants = mapValues(ProviderTransform.variants(m), (v) => ({ disabled: false, ...v }))
+
+ return m
}
export function fromModelsDevProvider(provider: ModelsDev.Provider): Info {
diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts
index 6feceb5e6f8..9de2ad52c5d 100644
--- a/packages/opencode/src/provider/transform.ts
+++ b/packages/opencode/src/provider/transform.ts
@@ -124,7 +124,7 @@ export namespace ProviderTransform {
cacheControl: { type: "ephemeral" },
},
openrouter: {
- cache_control: { type: "ephemeral" },
+ cacheControl: { type: "ephemeral" },
},
bedrock: {
cachePoint: { type: "ephemeral" },
@@ -243,6 +243,162 @@ export namespace ProviderTransform {
return undefined
}
+ const WIDELY_SUPPORTED_EFFORTS = ["low", "medium", "high"]
+ const OPENAI_EFFORTS = ["none", "minimal", ...WIDELY_SUPPORTED_EFFORTS, "xhigh"]
+
+ export function variants(model: Provider.Model) {
+ if (!model.capabilities.reasoning) return {}
+
+ const id = model.id.toLowerCase()
+ if (id.includes("deepseek") || id.includes("minimax") || id.includes("glm") || id.includes("mistral")) return {}
+
+ switch (model.api.npm) {
+ case "@openrouter/ai-sdk-provider":
+ if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("grok-4")) return {}
+ return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }]))
+
+ // TODO: YOU CANNOT SET max_tokens if this is set!!!
+ case "@ai-sdk/gateway":
+ return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
+
+ case "@ai-sdk/cerebras":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras
+ case "@ai-sdk/togetherai":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/togetherai
+ case "@ai-sdk/xai":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/xai
+ case "@ai-sdk/deepinfra":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/deepinfra
+ case "@ai-sdk/openai-compatible":
+ return Object.fromEntries(WIDELY_SUPPORTED_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }]))
+
+ case "@ai-sdk/azure":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
+ if (id === "o1-mini") return {}
+ const azureEfforts = ["low", "medium", "high"]
+ if (id.includes("gpt-5")) {
+ azureEfforts.unshift("minimal")
+ }
+ return Object.fromEntries(
+ azureEfforts.map((effort) => [
+ effort,
+ {
+ reasoningEffort: effort,
+ reasoningSummary: "auto",
+ include: ["reasoning.encrypted_content"],
+ },
+ ]),
+ )
+ case "@ai-sdk/openai":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/openai
+ if (id === "gpt-5-pro") return {}
+ const openaiEfforts = ["minimal", ...WIDELY_SUPPORTED_EFFORTS]
+ if (model.release_date >= "2025-11-13") {
+ openaiEfforts.unshift("none")
+ }
+ if (model.release_date >= "2025-12-04") {
+ openaiEfforts.push("xhigh")
+ }
+ return Object.fromEntries(
+ openaiEfforts.map((effort) => [
+ effort,
+ {
+ reasoningEffort: effort,
+ reasoningSummary: "auto",
+ include: ["reasoning.encrypted_content"],
+ },
+ ]),
+ )
+
+ case "@ai-sdk/anthropic":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/anthropic
+ return {
+ high: {
+ thinking: {
+ type: "enabled",
+ budgetTokens: 16000,
+ },
+ },
+ max: {
+ thinking: {
+ type: "enabled",
+ budgetTokens: 31999,
+ },
+ },
+ }
+
+ case "@ai-sdk/amazon-bedrock":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
+ return Object.fromEntries(
+ WIDELY_SUPPORTED_EFFORTS.map((effort) => [
+ effort,
+ {
+ reasoningConfig: {
+ type: "enabled",
+ maxReasoningEffort: effort,
+ },
+ },
+ ]),
+ )
+
+ case "@ai-sdk/google-vertex":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-vertex
+ case "@ai-sdk/google":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
+ if (id.includes("2.5")) {
+ return {
+ high: {
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 16000,
+ },
+ },
+ max: {
+ thinkingConfig: {
+ includeThoughts: true,
+ thinkingBudget: 24576,
+ },
+ },
+ }
+ }
+ return Object.fromEntries(
+ ["low", "high"].map((effort) => [
+ effort,
+ {
+ includeThoughts: true,
+ thinkingLevel: effort,
+ },
+ ]),
+ )
+
+ case "@ai-sdk/mistral":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/mistral
+ return {}
+
+ case "@ai-sdk/cohere":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cohere
+ return {}
+
+ case "@ai-sdk/groq":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/groq
+ const groqEffort = ["none", ...WIDELY_SUPPORTED_EFFORTS]
+ return Object.fromEntries(
+ groqEffort.map((effort) => [
+ effort,
+ {
+ includeThoughts: true,
+ thinkingLevel: effort,
+ },
+ ]),
+ )
+
+ case "@ai-sdk/perplexity":
+ // https://v5.ai-sdk.dev/providers/ai-sdk-providers/perplexity
+ return {}
+ }
+ return {}
+ }
+
export function options(
model: Provider.Model,
sessionID: string,
@@ -322,6 +478,7 @@ export namespace ProviderTransform {
export function providerOptions(model: Provider.Model, options: { [x: string]: any }) {
switch (model.api.npm) {
+ case "@ai-sdk/github-copilot":
case "@ai-sdk/openai":
case "@ai-sdk/azure":
return {
@@ -335,6 +492,7 @@ export namespace ProviderTransform {
return {
["anthropic" as string]: options,
}
+ case "@ai-sdk/google-vertex":
case "@ai-sdk/google":
return {
["google" as string]: options,
diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts
index a81aa7db224..a1461c31557 100644
--- a/packages/opencode/src/session/llm.ts
+++ b/packages/opencode/src/session/llm.ts
@@ -74,6 +74,14 @@ export namespace LLM {
}
const provider = await Provider.getProvider(input.model.providerID)
+ const variant = input.model.variants && input.user.variant ? input.model.variants[input.user.variant] : undefined
+ const options = pipe(
+ ProviderTransform.options(input.model, input.sessionID, provider.options),
+ mergeDeep(input.small ? ProviderTransform.smallOptions(input.model) : {}),
+ mergeDeep(input.model.options),
+ mergeDeep(input.agent.options),
+ mergeDeep(variant && !variant.disabled ? variant : {}),
+ )
const params = await Plugin.trigger(
"chat.params",
@@ -90,13 +98,7 @@ export namespace LLM {
: undefined,
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
topK: ProviderTransform.topK(input.model),
- options: pipe(
- {},
- mergeDeep(ProviderTransform.options(input.model, input.sessionID, provider.options)),
- input.small ? mergeDeep(ProviderTransform.smallOptions(input.model)) : mergeDeep({}),
- mergeDeep(input.model.options),
- mergeDeep(input.agent.options),
- ),
+ options,
},
)
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index da89a1a0e04..bb78ae64ce6 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -1,8 +1,6 @@
import { BusEvent } from "@/bus/bus-event"
-import { Bus } from "@/bus"
import z from "zod"
import { NamedError } from "@opencode-ai/util/error"
-import { Message } from "./message"
import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai"
import { Identifier } from "../id/id"
import { LSP } from "../lsp"
@@ -308,6 +306,7 @@ export namespace MessageV2 {
}),
system: z.string().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
+ variant: z.string().optional(),
}).meta({
ref: "UserMessage",
})
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 19dc90b3bcb..595fc746e7f 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -90,6 +90,7 @@ export namespace SessionPrompt {
noReply: z.boolean().optional(),
tools: z.record(z.string(), z.boolean()).optional(),
system: z.string().optional(),
+ variant: z.string().optional(),
parts: z.array(
z.discriminatedUnion("type", [
MessageV2.TextPart.omit({
@@ -727,6 +728,7 @@ export namespace SessionPrompt {
agent: agent.name,
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
system: input.system,
+ variant: input.variant,
}
const parts = await Promise.all(
@@ -1267,6 +1269,7 @@ export namespace SessionPrompt {
model: z.string().optional(),
arguments: z.string(),
command: z.string(),
+ variant: z.string().optional(),
})
export type CommandInput = z.infer
const bashRegex = /!`([^`]+)`/g
@@ -1369,6 +1372,7 @@ export namespace SessionPrompt {
model,
agent: agentName,
parts,
+ variant: input.variant,
})) as MessageV2.WithParts
Bus.publish(Command.Event.Executed, {
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 797896ace9a..c01d351bafe 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -1228,6 +1228,7 @@ export class Session extends HeyApiClient {
[key: string]: boolean
}
system?: string
+ variant?: string
parts?: Array
},
options?: Options,
@@ -1245,6 +1246,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "noReply" },
{ in: "body", key: "tools" },
{ in: "body", key: "system" },
+ { in: "body", key: "variant" },
{ in: "body", key: "parts" },
],
},
@@ -1314,6 +1316,7 @@ export class Session extends HeyApiClient {
[key: string]: boolean
}
system?: string
+ variant?: string
parts?: Array
},
options?: Options,
@@ -1331,6 +1334,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "noReply" },
{ in: "body", key: "tools" },
{ in: "body", key: "system" },
+ { in: "body", key: "variant" },
{ in: "body", key: "parts" },
],
},
@@ -1362,6 +1366,7 @@ export class Session extends HeyApiClient {
model?: string
arguments?: string
command?: string
+ variant?: string
},
options?: Options,
) {
@@ -1377,6 +1382,7 @@ export class Session extends HeyApiClient {
{ in: "body", key: "model" },
{ in: "body", key: "arguments" },
{ in: "body", key: "command" },
+ { in: "body", key: "variant" },
],
},
],
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 5c4cc69423d..8f4daa1439d 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -90,6 +90,7 @@ export type UserMessage = {
tools?: {
[key: string]: boolean
}
+ variant?: string
}
export type ProviderAuthError = {
@@ -969,6 +970,10 @@ export type KeybindsConfig = {
* Previous agent
*/
agent_cycle_reverse?: string
+ /**
+ * Cycle model variants
+ */
+ variant_cycle?: string
/**
* Clear input field
*/
@@ -1712,6 +1717,11 @@ export type Command = {
subtask?: boolean
}
+export type Variant = {
+ disabled: boolean
+ [key: string]: unknown | boolean
+}
+
export type Model = {
id: string
providerID: string
@@ -1775,6 +1785,9 @@ export type Model = {
[key: string]: string
}
release_date: string
+ variants?: {
+ [key: string]: Variant
+ }
}
export type Provider = {
@@ -2944,6 +2957,7 @@ export type SessionPromptData = {
[key: string]: boolean
}
system?: string
+ variant?: string
parts: Array
}
path: {
@@ -3127,6 +3141,7 @@ export type SessionPromptAsyncData = {
[key: string]: boolean
}
system?: string
+ variant?: string
parts: Array
}
path: {
@@ -3170,6 +3185,7 @@ export type SessionCommandData = {
model?: string
arguments: string
command: string
+ variant?: string
}
path: {
/**