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: { /**