diff --git a/bun.lock b/bun.lock index 061e36e143..c29176596d 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -73,7 +73,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -107,7 +107,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -134,7 +134,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@ai-sdk/anthropic": "2.0.0", "@ai-sdk/openai": "2.0.2", @@ -158,7 +158,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -182,7 +182,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -215,7 +215,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@opencode-ai/ui": "workspace:*", "@opencode-ai/util": "workspace:*", @@ -244,7 +244,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -260,7 +260,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.2.1", + "version": "1.2.2", "bin": { "opencode": "./bin/opencode", }, @@ -276,7 +276,7 @@ "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.98", + "@ai-sdk/google-vertex": "3.0.103", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", @@ -369,7 +369,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@opencode-ai/sdk": "workspace:*", "zod": "catalog:", @@ -389,7 +389,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.2.1", + "version": "1.2.2", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -400,7 +400,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -413,7 +413,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", @@ -455,7 +455,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "zod": "catalog:", }, @@ -466,7 +466,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -504,6 +504,7 @@ "tree-sitter-bash", ], "patchedDependencies": { + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", }, "overrides": { @@ -594,7 +595,7 @@ "@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="], - "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.98", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.58", "@ai-sdk/google": "2.0.52", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-uuv0RHkdJ5vTzeH1+iuBlv7GAjRcOPd2jiqtGLz6IKOUDH+PRQoE3ExrvOysVnKuhhTBMqvawkktDhMDQE6sVQ=="], + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.103", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.63", "@ai-sdk/google": "2.0.53", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MPZRSVOJFxYGHE4s6XjSWaiUPru7u2i/LUUA1Ih2nzNYZaei8c46Z56imOCD/KQjQX3afRA2iZh6P5McsmwhqA=="], "@ai-sdk/groq": ["@ai-sdk/groq@2.0.34", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-wfCYkVgmVjxNA32T57KbLabVnv9aFUflJ4urJ7eWgTwbnmGQHElCTu+rJ3ydxkXSqxOkXPwMOttDm7XNrvPjmg=="], @@ -4208,7 +4209,11 @@ "@ai-sdk/fireworks/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], - "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.58", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CkNW5L1Arv8gPtPlEmKd+yf/SG9ucJf0XQdpMG8OiYEtEMc2smuCA+tyCp8zI7IBVg/FE7nUfFHntQFaOjRwJQ=="], + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.63", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zXlUPCkumnvp8lWS9VFcen/MLF6CL/t1zAKDhpobYj9y/nmylQrKtRvn3RwH871Wd3dF3KYEUXd6M2c6dfCKOA=="], + + "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.53", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ccCxr5mrd3AC2CjLq4e1ST7+UiN5T2Pdmgi0XdWM3QohmNBwUQ/RBG7BvL+cB/ex/j6y64tkMmpYz9zBw/SEFQ=="], + + "@ai-sdk/google-vertex/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], diff --git a/flake.lock b/flake.lock index 10fa973cfe..9efa1883b1 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1770073757, - "narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=", + "lastModified": 1770812194, + "narHash": "sha256-OH+lkaIKAvPXR3nITO7iYZwew2nW9Y7Xxq0yfM/UcUU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "47472570b1e607482890801aeaf29bfb749884f6", + "rev": "8482c7ded03bae7550f3d69884f1e611e3bd19e8", "type": "github" }, "original": { diff --git a/nix/hashes.json b/nix/hashes.json index ef0b63897d..fde268ba83 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-hVf8rBEqy3q4xexOqyKDtKmlMydl1hFoDV0JiEvmfgs=", - "aarch64-linux": "sha256-4m3UZllEmfJXB70cOgIoyWRIYMXxGzzenyOfF3kEQKk=", - "aarch64-darwin": "sha256-27xGR9+FVnC0rsUIyepk2tCP1eEUmGvqWUGAZ+rk7IQ=", - "x86_64-darwin": "sha256-+At7bHSeg6QJu6yGawyvzt53Tu/fddDg6Ms+xhaMLhY=" + "x86_64-linux": "sha256-5pgd2xuvIIkTbIOGIdK5MIXo6O9qRpvk1RKQZ1e1R+8=", + "aarch64-linux": "sha256-FZiHwihM4b82ipQ9XfW08X+sd5CvZhx/+pU/8X1zsns=", + "aarch64-darwin": "sha256-iZv0w1NthV53pY5uvuf3JlI14GeKmCu7WHwGSRdEQeM=", + "x86_64-darwin": "sha256-c3Zm3P1goFPgg3vNAZPMFOhHX/gyTmsCN/PKbGO/v0E=" } } diff --git a/package.json b/package.json index c4408e264b..5d93205056 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@types/node": "catalog:" }, "patchedDependencies": { - "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch" + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch" } } diff --git a/packages/app/package.json b/packages/app/package.json index e8c24c08ba..b2f2f23246 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.2.1", + "version": "1.2.2", "description": "", "type": "module", "exports": { diff --git a/packages/console/app/package.json b/packages/console/app/package.json index a6267c73ef..ea16083326 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.2.1", + "version": "1.2.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 85f9c200a7..8c71ca77b2 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.2.1", + "version": "1.2.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 02aa6f76ef..debba52bab 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.2.1", + "version": "1.2.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 115d365f7e..e361cc0e73 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.2.1", + "version": "1.2.2", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 858dbf5c4d..f9ab28cfca 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.2.1", + "version": "1.2.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 7aa3bb90d1..173688eb1d 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.2.1", + "version": "1.2.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index fdb66d69bf..9498ce1269 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "opencode" name = "OpenCode" description = "The open source coding agent." -version = "1.2.1" +version = "1.2.2" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/anomalyco/opencode" @@ -11,26 +11,26 @@ name = "OpenCode" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.1/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.1/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.1/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.1/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.1/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.2/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 242ce353f3..1718b791a6 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.2.1", + "version": "1.2.2", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index bf372379bd..728ec38385 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.2.1", + "version": "1.2.2", "name": "opencode", "type": "module", "license": "MIT", @@ -62,7 +62,7 @@ "@ai-sdk/deepinfra": "1.0.36", "@ai-sdk/gateway": "2.0.30", "@ai-sdk/google": "2.0.52", - "@ai-sdk/google-vertex": "3.0.98", + "@ai-sdk/google-vertex": "3.0.103", "@ai-sdk/groq": "2.0.34", "@ai-sdk/mistral": "2.0.27", "@ai-sdk/openai": "2.0.89", diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 8091f731f0..853d03c1d8 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -171,7 +171,7 @@ export namespace ProviderTransform { return msgs } - function applyCaching(msgs: ModelMessage[], providerID: string): ModelMessage[] { + function applyCaching(msgs: ModelMessage[], model: Provider.Model): ModelMessage[] { const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) const final = msgs.filter((msg) => msg.role !== "system").slice(-2) @@ -194,7 +194,7 @@ export namespace ProviderTransform { } for (const msg of unique([...system, ...final])) { - const useMessageLevelOptions = providerID === "anthropic" || providerID.includes("bedrock") + const useMessageLevelOptions = model.providerID === "anthropic" || model.providerID.includes("bedrock") const shouldUseContentOptions = !useMessageLevelOptions && Array.isArray(msg.content) && msg.content.length > 0 if (shouldUseContentOptions) { @@ -253,14 +253,15 @@ export namespace ProviderTransform { msgs = unsupportedParts(msgs, model) msgs = normalizeMessages(msgs, model, options) if ( - model.providerID === "anthropic" || - model.api.id.includes("anthropic") || - model.api.id.includes("claude") || - model.id.includes("anthropic") || - model.id.includes("claude") || - model.api.npm === "@ai-sdk/anthropic" + (model.providerID === "anthropic" || + model.api.id.includes("anthropic") || + model.api.id.includes("claude") || + model.id.includes("anthropic") || + model.id.includes("claude") || + model.api.npm === "@ai-sdk/anthropic") && + model.api.npm !== "@ai-sdk/gateway" ) { - msgs = applyCaching(msgs, model.providerID) + msgs = applyCaching(msgs, model) } // Remap providerOptions keys from stored providerID to expected SDK key @@ -360,11 +361,53 @@ export namespace ProviderTransform { switch (model.api.npm) { case "@openrouter/ai-sdk-provider": - if (!model.id.includes("gpt") && !model.id.includes("gemini-3")) return {} + if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("claude")) 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": + if (model.id.includes("anthropic")) { + return { + high: { + thinking: { + type: "enabled", + budgetTokens: 16000, + }, + }, + max: { + thinking: { + type: "enabled", + budgetTokens: 31999, + }, + }, + } + } + if (model.id.includes("google")) { + 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, + }, + ]), + ) + } return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) case "@ai-sdk/github-copilot": @@ -720,6 +763,15 @@ export namespace ProviderTransform { result["promptCacheKey"] = input.sessionID } + if (input.model.providerID === "openrouter") { + result["prompt_cache_key"] = input.sessionID + } + if (input.model.api.npm === "@ai-sdk/gateway") { + result["gateway"] = { + caching: "auto", + } + } + return result } @@ -753,7 +805,43 @@ export namespace ProviderTransform { return {} } + // Maps model ID prefix to provider slug used in providerOptions. + // Example: "amazon/nova-2-lite" → "bedrock" + const SLUG_OVERRIDES: Record = { + amazon: "bedrock", + } + export function providerOptions(model: Provider.Model, options: { [x: string]: any }) { + if (model.api.npm === "@ai-sdk/gateway") { + // Gateway providerOptions are split across two namespaces: + // - `gateway`: gateway-native routing/caching controls (order, only, byok, etc.) + // - ``: provider-specific model options (anthropic/openai/...) + // We keep `gateway` as-is and route every other top-level option under the + // model-derived upstream slug. + const i = model.api.id.indexOf("/") + const rawSlug = i > 0 ? model.api.id.slice(0, i) : undefined + const slug = rawSlug ? (SLUG_OVERRIDES[rawSlug] ?? rawSlug) : undefined + const gateway = options.gateway + const rest = Object.fromEntries(Object.entries(options).filter(([k]) => k !== "gateway")) + const has = Object.keys(rest).length > 0 + + const result: Record = {} + if (gateway !== undefined) result.gateway = gateway + + if (has) { + if (slug) { + // Route model-specific options under the provider slug + result[slug] = rest + } else if (gateway && typeof gateway === "object" && !Array.isArray(gateway)) { + result.gateway = { ...gateway, ...rest } + } else { + result.gateway = rest + } + } + + return result + } + const key = sdkKey(model.api.npm) ?? model.providerID return { [key]: options } } diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 2cf5473f22..1195529e06 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -53,15 +53,15 @@ export const SessionRoutes = lazy(() => ), async (c) => { const query = c.req.valid("query") - const term = query.search?.toLowerCase() const sessions: Session.Info[] = [] - for await (const session of Session.list()) { - if (query.directory !== undefined && session.directory !== query.directory) continue - if (query.roots && session.parentID) continue - if (query.start !== undefined && session.time.updated < query.start) continue - if (term !== undefined && !session.title.toLowerCase().includes(term)) continue + for await (const session of Session.list({ + directory: query.directory, + roots: query.roots, + start: query.start, + search: query.search, + limit: query.limit, + })) { sessions.push(session) - if (query.limit !== undefined && sessions.length >= query.limit) break } return c.json(sessions) }, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 38007a0a7f..255f4dd460 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -10,7 +10,7 @@ import { Flag } from "../flag/flag" import { Identifier } from "../id/id" import { Installation } from "../installation" -import { Database, NotFoundError, eq, and, or, like } from "../storage/db" +import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like } from "../storage/db" import { SessionTable, MessageTable, PartTable } from "./session.sql" import { Storage } from "@/storage/storage" import { Log } from "../util/log" @@ -505,20 +505,38 @@ export namespace Session { }, ) - export function* list() { + export function* list(input?: { + directory?: string + roots?: boolean + start?: number + search?: string + limit?: number + }) { const project = Instance.project - // const rel = path.relative(Instance.worktree, Instance.directory) - // const suffix = path.sep + rel + const conditions = [eq(SessionTable.project_id, project.id)] + + if (input?.directory) { + conditions.push(eq(SessionTable.directory, input.directory)) + } + if (input?.roots) { + conditions.push(isNull(SessionTable.parent_id)) + } + if (input?.start) { + conditions.push(gte(SessionTable.time_updated, input.start)) + } + if (input?.search) { + conditions.push(like(SessionTable.title, `%${input.search}%`)) + } + + const limit = input?.limit ?? 100 + const rows = Database.use((db) => db .select() .from(SessionTable) - .where( - and( - eq(SessionTable.project_id, project.id), - // or(eq(SessionTable.directory, Instance.directory), like(SessionTable.directory, `%${suffix}`)), - ), - ) + .where(and(...conditions)) + .orderBy(desc(SessionTable.time_updated)) + .limit(limit) .all(), ) for (const row of rows) { diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 50aa76384e..387e93b376 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -74,6 +74,7 @@ export namespace Database { sqlite.run("PRAGMA busy_timeout = 5000") sqlite.run("PRAGMA cache_size = -64000") sqlite.run("PRAGMA foreign_keys = ON") + sqlite.run("PRAGMA wal_checkpoint(PASSIVE)") const db = drizzle({ client: sqlite, schema }) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 02bb5278fc..3494cb56fd 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -175,6 +175,204 @@ describe("ProviderTransform.options - gpt-5 textVerbosity", () => { }) }) +describe("ProviderTransform.options - gateway", () => { + const sessionID = "test-session-123" + + const createModel = (id: string) => + ({ + id, + providerID: "vercel", + api: { + id, + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + name: id, + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 200_000, + output: 8192, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2024-01-01", + }) as any + + test("puts gateway defaults under gateway key", () => { + const model = createModel("anthropic/claude-sonnet-4") + const result = ProviderTransform.options({ model, sessionID, providerOptions: {} }) + expect(result).toEqual({ + gateway: { + caching: "auto", + }, + }) + }) +}) + +describe("ProviderTransform.providerOptions", () => { + const createModel = (overrides: Partial = {}) => + ({ + id: "test/test-model", + providerID: "test", + api: { + id: "test-model", + url: "https://api.test.com", + npm: "@ai-sdk/openai", + }, + name: "Test Model", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { + input: 0.001, + output: 0.002, + cache: { read: 0.0001, write: 0.0002 }, + }, + limit: { + context: 200_000, + output: 64_000, + }, + status: "active", + options: {}, + headers: {}, + release_date: "2024-01-01", + ...overrides, + }) as any + + test("uses sdk key for non-gateway models", () => { + const model = createModel({ + providerID: "my-bedrock", + api: { + id: "anthropic.claude-sonnet-4", + url: "https://bedrock.aws", + npm: "@ai-sdk/amazon-bedrock", + }, + }) + + expect(ProviderTransform.providerOptions(model, { cachePoint: { type: "default" } })).toEqual({ + bedrock: { cachePoint: { type: "default" } }, + }) + }) + + test("uses gateway model provider slug for gateway models", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({ + anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } }, + }) + }) + + test("falls back to gateway key when gateway api id is unscoped", () => { + const model = createModel({ + id: "anthropic/claude-sonnet-4", + providerID: "vercel", + api: { + id: "claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { thinking: { type: "enabled", budgetTokens: 12_000 } })).toEqual({ + gateway: { thinking: { type: "enabled", budgetTokens: 12_000 } }, + }) + }) + + test("splits gateway routing options from provider-specific options", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect( + ProviderTransform.providerOptions(model, { + gateway: { order: ["vertex", "anthropic"] }, + thinking: { type: "enabled", budgetTokens: 12_000 }, + }), + ).toEqual({ + gateway: { order: ["vertex", "anthropic"] }, + anthropic: { thinking: { type: "enabled", budgetTokens: 12_000 } }, + } as any) + }) + + test("falls back to gateway key when model id has no provider slug", () => { + const model = createModel({ + id: "claude-sonnet-4", + providerID: "vercel", + api: { + id: "claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningEffort: "high" })).toEqual({ + gateway: { reasoningEffort: "high" }, + }) + }) + + test("maps amazon slug to bedrock for provider options", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "amazon/nova-2-lite", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningConfig: { type: "enabled" } })).toEqual({ + bedrock: { reasoningConfig: { type: "enabled" } }, + }) + }) + + test("uses groq slug for groq models", () => { + const model = createModel({ + providerID: "vercel", + api: { + id: "groq/llama-3.3-70b-versatile", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + }) + + expect(ProviderTransform.providerOptions(model, { reasoningFormat: "parsed" })).toEqual({ + groq: { reasoningFormat: "parsed" }, + }) + }) +}) + describe("ProviderTransform.schema - gemini array items", () => { test("adds missing items for array properties", () => { const geminiModel = { @@ -1232,6 +1430,105 @@ describe("ProviderTransform.message - claude w/bedrock custom inference profile" }) }) +describe("ProviderTransform.message - cache control on gateway", () => { + const createModel = (overrides: Partial = {}) => + ({ + id: "anthropic/claude-sonnet-4", + providerID: "vercel", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://ai-gateway.vercel.sh/v3/ai", + npm: "@ai-sdk/gateway", + }, + name: "Claude Sonnet 4", + capabilities: { + temperature: true, + reasoning: true, + attachment: true, + toolcall: true, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0.001, output: 0.002, cache: { read: 0.0001, write: 0.0002 } }, + limit: { context: 200_000, output: 8192 }, + status: "active", + options: {}, + headers: {}, + ...overrides, + }) as any + + test("gateway does not set cache control for anthropic models", () => { + const model = createModel() + const msgs = [ + { + role: "system", + content: [{ type: "text", text: "You are a helpful assistant" }], + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].content[0].providerOptions).toBeUndefined() + expect(result[0].providerOptions).toBeUndefined() + }) + + test("non-gateway anthropic keeps existing cache control behavior", () => { + const model = createModel({ + providerID: "anthropic", + api: { + id: "claude-sonnet-4", + url: "https://api.anthropic.com", + npm: "@ai-sdk/anthropic", + }, + }) + const msgs = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ] as any[] + + const result = ProviderTransform.message(msgs, model, {}) as any[] + + expect(result[0].providerOptions).toEqual({ + anthropic: { + cacheControl: { + type: "ephemeral", + }, + }, + openrouter: { + cacheControl: { + type: "ephemeral", + }, + }, + bedrock: { + cachePoint: { + type: "default", + }, + }, + openaiCompatible: { + cache_control: { + type: "ephemeral", + }, + }, + copilot: { + copilot_cache_control: { + type: "ephemeral", + }, + }, + }) + }) +}) + describe("ProviderTransform.variants", () => { const createMockModel = (overrides: Partial = {}): any => ({ id: "test/test-model", @@ -1408,6 +1705,32 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/gateway", () => { + test("anthropic models return anthropic thinking options", () => { + const model = createMockModel({ + id: "anthropic/claude-sonnet-4", + providerID: "gateway", + api: { + id: "anthropic/claude-sonnet-4", + url: "https://gateway.ai", + npm: "@ai-sdk/gateway", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["high", "max"]) + expect(result.high).toEqual({ + thinking: { + type: "enabled", + budgetTokens: 16000, + }, + }) + expect(result.max).toEqual({ + thinking: { + type: "enabled", + budgetTokens: 31999, + }, + }) + }) + test("returns OPENAI_EFFORTS with reasoningEffort", () => { const model = createMockModel({ id: "gateway/gateway-model", diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts index 623c16a811..675a89011f 100644 --- a/packages/opencode/test/server/session-list.test.ts +++ b/packages/opencode/test/server/session-list.test.ts @@ -1,20 +1,17 @@ import { describe, expect, test } from "bun:test" import path from "path" import { Instance } from "../../src/project/instance" -import { Server } from "../../src/server/server" import { Session } from "../../src/session" import { Log } from "../../src/util/log" const projectRoot = path.join(__dirname, "../..") Log.init({ print: false }) -describe("session.list", () => { +describe("Session.list", () => { test("filters by directory", async () => { await Instance.provide({ directory: projectRoot, fn: async () => { - const app = Server.App() - const first = await Session.create({}) const otherDir = path.join(projectRoot, "..", "__session_list_other") @@ -23,17 +20,71 @@ describe("session.list", () => { fn: async () => Session.create({}), }) - const response = await app.request(`/session?directory=${encodeURIComponent(projectRoot)}`) - expect(response.status).toBe(200) - - const body = (await response.json()) as unknown[] - const ids = body - .map((s) => (typeof s === "object" && s && "id" in s ? (s as { id: string }).id : undefined)) - .filter((x): x is string => typeof x === "string") + const sessions = [...Session.list({ directory: projectRoot })] + const ids = sessions.map((s) => s.id) expect(ids).toContain(first.id) expect(ids).not.toContain(second.id) }, }) }) + + test("filters root sessions", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const root = await Session.create({ title: "root-session" }) + const child = await Session.create({ title: "child-session", parentID: root.id }) + + const sessions = [...Session.list({ roots: true })] + const ids = sessions.map((s) => s.id) + + expect(ids).toContain(root.id) + expect(ids).not.toContain(child.id) + }, + }) + }) + + test("filters by start time", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const session = await Session.create({ title: "new-session" }) + const futureStart = Date.now() + 86400000 + + const sessions = [...Session.list({ start: futureStart })] + expect(sessions.length).toBe(0) + }, + }) + }) + + test("filters by search term", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + await Session.create({ title: "unique-search-term-abc" }) + await Session.create({ title: "other-session-xyz" }) + + const sessions = [...Session.list({ search: "unique-search" })] + const titles = sessions.map((s) => s.title) + + expect(titles).toContain("unique-search-term-abc") + expect(titles).not.toContain("other-session-xyz") + }, + }) + }) + + test("respects limit parameter", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + await Session.create({ title: "session-1" }) + await Session.create({ title: "session-2" }) + await Session.create({ title: "session-3" }) + + const sessions = [...Session.list({ limit: 2 })] + expect(sessions.length).toBe(2) + }, + }) + }) }) diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 481404969c..d078c0028e 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.2.1", + "version": "1.2.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index e141774821..a03d8382db 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.2.1", + "version": "1.2.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/slack/package.json b/packages/slack/package.json index d9234899d9..8a6d289ec0 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.2.1", + "version": "1.2.2", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index a0e5c2af25..8771b397ce 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.2.1", + "version": "1.2.2", "type": "module", "license": "MIT", "exports": { diff --git a/packages/util/package.json b/packages/util/package.json index bf0cb074be..c610140398 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/util", - "version": "1.2.1", + "version": "1.2.2", "private": true, "type": "module", "license": "MIT", diff --git a/packages/web/package.json b/packages/web/package.json index 6bdecea178..79858de586 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -2,7 +2,7 @@ "name": "@opencode-ai/web", "type": "module", "license": "MIT", - "version": "1.2.1", + "version": "1.2.2", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch b/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch new file mode 100644 index 0000000000..6226bf790c --- /dev/null +++ b/patches/@openrouter%2Fai-sdk-provider@1.5.4.patch @@ -0,0 +1,128 @@ +diff --git a/dist/index.js b/dist/index.js +index f33510a50d11a2cb92a90ea70cc0ac84c89f29b9..e887a60352c0c08ab794b1e6821854dfeefd20cc 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -2110,7 +2110,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2307,7 +2312,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { +diff --git a/dist/index.mjs b/dist/index.mjs +index 8a688331b88b4af738ee4ca8062b5f24124d3d81..6310cb8b7c8d0a728d86e1eed09906c6b4c91ae2 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -2075,7 +2075,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2272,7 +2277,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { +diff --git a/dist/internal/index.js b/dist/internal/index.js +index d40fa66125941155ac13a4619503caba24d89f8a..8dd86d1b473f2fa31c1acd9881d72945b294a197 100644 +--- a/dist/internal/index.js ++++ b/dist/internal/index.js +@@ -2064,7 +2064,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2261,7 +2266,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { +diff --git a/dist/internal/index.mjs b/dist/internal/index.mjs +index b0ed9d113549c5c55ea3b1e08abb3db6f92ae5a7..5695930a8e038facc071d58a4179a369a29be9c7 100644 +--- a/dist/internal/index.mjs ++++ b/dist/internal/index.mjs +@@ -2030,7 +2030,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted && !textStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + reasoningStarted = false; + } +@@ -2227,7 +2232,12 @@ var OpenRouterChatLanguageModel = class { + if (reasoningStarted) { + controller.enqueue({ + type: "reasoning-end", +- id: reasoningId || generateId() ++ id: reasoningId || generateId(), ++ providerMetadata: accumulatedReasoningDetails.length > 0 ? { ++ openrouter: { ++ reasoning_details: accumulatedReasoningDetails ++ } ++ } : undefined + }); + } + if (textStarted) { diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index d19dcbf467..e91d1a324d 100644 --- a/sdks/vscode/package.json +++ b/sdks/vscode/package.json @@ -2,7 +2,7 @@ "name": "opencode", "displayName": "opencode", "description": "opencode for VS Code", - "version": "1.2.1", + "version": "1.2.2", "publisher": "sst-dev", "repository": { "type": "git",