diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 65bda9804a2e..589002d08411 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -33,3 +33,4 @@ simonklee -spider-yamet clawdbot/llm psychosis, spam pinging the team thdxr -toastythebot +-davidbernat looks to be a clawdbot that spams team and sends super weird emails, doesnt appear to be a real person diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml index 58e73fac8fbc..2bd1f0c4a002 100644 --- a/.github/workflows/review.yml +++ b/.github/workflows/review.yml @@ -45,13 +45,13 @@ jobs: - name: Check PR guidelines compliance env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENCODE_PERMISSION: '{ "bash": { "*": "deny", "gh*": "allow", "gh pr review*": "deny" } }' PR_TITLE: ${{ steps.pr-details.outputs.title }} run: | PR_BODY=$(jq -r .body pr_data.json) - opencode run -m anthropic/claude-opus-4-5 "A new pull request has been created: '${PR_TITLE}' + opencode run -m opencode/gpt-5.5 --variant medium "A new pull request has been created: '${PR_TITLE}' ${{ steps.pr-number.outputs.number }} diff --git a/bun.lock b/bun.lock index 06a5cfdb4c5a..347cdc8d330e 100644 --- a/bun.lock +++ b/bun.lock @@ -29,11 +29,11 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/shared": "workspace:*", "@opencode-ai/ui": "workspace:*", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", @@ -83,7 +83,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -117,7 +117,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -144,7 +144,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/openai": "3.0.48", @@ -168,7 +168,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -190,9 +190,43 @@ "cloudflare": "5.2.0", }, }, + "packages/core": { + "name": "@opencode-ai/core", + "version": "1.14.25", + "bin": { + "opencode": "./bin/opencode", + }, + "dependencies": { + "@effect/opentelemetry": "catalog:", + "@effect/platform-node": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1", + "cross-spawn": "catalog:", + "effect": "catalog:", + "glob": "13.0.5", + "mime-types": "3.0.2", + "minimatch": "10.2.5", + "npm-package-arg": "13.0.2", + "semver": "^7.6.3", + "xdg-basedir": "5.1.0", + "zod": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:", + }, + }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -225,7 +259,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -269,9 +303,9 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", "@pierre/diffs": "catalog:", "@solidjs/meta": "catalog:", @@ -298,7 +332,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -314,7 +348,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.14.22", + "version": "1.14.25", "bin": { "opencode": "./bin/opencode", }, @@ -353,8 +387,6 @@ "@hono/zod-validator": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", - "@npmcli/arborist": "9.4.0", - "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -367,8 +399,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -403,7 +435,7 @@ "open": "10.1.2", "opencode-gitlab-auth": "2.0.1", "opencode-poe-auth": "0.0.1", - "opentui-spinner": "0.0.6", + "opentui-spinner": "catalog:", "partial-json": "0.1.7", "remeda": "catalog:", "semver": "^7.6.3", @@ -426,8 +458,8 @@ "@babel/core": "7.28.4", "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", + "@opencode-ai/core": "workspace:*", "@opencode-ai/script": "workspace:*", - "@opencode-ai/shared": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", @@ -443,7 +475,6 @@ "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3", "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", "@types/which": "3.0.4", @@ -451,6 +482,7 @@ "@typescript/native-preview": "catalog:", "drizzle-kit": "catalog:", "drizzle-orm": "catalog:", + "prettier": "3.6.2", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -459,23 +491,23 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@opencode-ai/sdk": "workspace:*", "effect": "catalog:", "zod": "catalog:", }, "devDependencies": { - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "@typescript/native-preview": "catalog:", "typescript": "catalog:", }, "peerDependencies": { - "@opentui/core": ">=0.1.99", - "@opentui/solid": ">=0.1.99", + "@opentui/core": ">=0.1.103", + "@opentui/solid": ">=0.1.103", }, "optionalPeers": [ "@opentui/core", @@ -494,7 +526,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "cross-spawn": "catalog:", }, @@ -507,33 +539,9 @@ "typescript": "catalog:", }, }, - "packages/shared": { - "name": "@opencode-ai/shared", - "version": "1.14.22", - "bin": { - "opencode": "./bin/opencode", - }, - "dependencies": { - "@effect/platform-node": "catalog:", - "@npmcli/arborist": "catalog:", - "effect": "catalog:", - "glob": "13.0.5", - "mime-types": "3.0.2", - "minimatch": "10.2.5", - "semver": "catalog:", - "xdg-basedir": "5.1.0", - "zod": "catalog:", - }, - "devDependencies": { - "@tsconfig/bun": "catalog:", - "@types/bun": "catalog:", - "@types/npmcli__arborist": "6.3.3", - "@types/semver": "catalog:", - }, - }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -568,11 +576,11 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@kobalte/core": "catalog:", + "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/shared": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", @@ -617,7 +625,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -677,8 +685,8 @@ "@npmcli/arborist": "9.4.0", "@octokit/rest": "22.0.0", "@openauthjs/openauth": "0.0.0-20250322224806", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.103", + "@opentui/solid": "0.1.103", "@pierre/diffs": "1.1.0-beta.18", "@playwright/test": "1.59.1", "@solid-primitives/storage": "4.3.3", @@ -707,6 +715,7 @@ "luxon": "3.6.1", "marked": "17.0.1", "marked-shiki": "1.2.1", + "opentui-spinner": "0.0.6", "remeda": "2.26.0", "remend": "1.3.0", "semver": "7.7.4", @@ -1552,6 +1561,8 @@ "@opencode-ai/console-resource": ["@opencode-ai/console-resource@workspace:packages/console/resource"], + "@opencode-ai/core": ["@opencode-ai/core@workspace:packages/core"], + "@opencode-ai/desktop": ["@opencode-ai/desktop@workspace:packages/desktop"], "@opencode-ai/desktop-electron": ["@opencode-ai/desktop-electron@workspace:packages/desktop-electron"], @@ -1566,8 +1577,6 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], - "@opencode-ai/shared": ["@opencode-ai/shared@workspace:packages/shared"], - "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/storybook": ["@opencode-ai/storybook@workspace:packages/storybook"], @@ -1604,21 +1613,21 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], - "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], + "@opentui/core": ["@opentui/core@0.1.103", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.103", "@opentui/core-darwin-x64": "0.1.103", "@opentui/core-linux-arm64": "0.1.103", "@opentui/core-linux-x64": "0.1.103", "@opentui/core-win32-arm64": "0.1.103", "@opentui/core-win32-x64": "0.1.103", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-PWVv/bDmlk1i6X1f0zXs+jSaTrQ/ByX8wFbP2WinOObTGf//UbcRP4dbWxPXvOyka9QlmRBG/7GbloQSIStyVw=="], - "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.103", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lxCyedDkcen12IgBtXjkJ7iY66xa7VC4nxRNKCUeLY2ZP9hUE1AsDtbyQzqY+BQadsI/ZME9STzaHDCUFg0TpA=="], - "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.103", "", { "os": "darwin", "cpu": "x64" }, "sha512-QMYD+zUDGQliJ6m5nuNvA72jtluFeyVMoHkuA5m/Xmed/u8eLfahAKmDj3kY66ntUroPHWevcpbpvd7NCFEoFQ=="], - "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.103", "", { "os": "linux", "cpu": "arm64" }, "sha512-GzOvNr9dN6JaQ9qs7m8E75wLAHwT5CyxqkE6rEr1BO23/d2Ix7e3GYw/JRY5VnTge+eXrfDVbqNtPcQamUNiEA=="], - "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.103", "", { "os": "linux", "cpu": "x64" }, "sha512-odywllco5zUKNc60uD3JKaCybK64u6BfmpScs4a8Qn89yH/yk23bzWXDRWaGgQdY65L2/VCbcGs1ezA1S/2YTw=="], - "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.103", "", { "os": "win32", "cpu": "arm64" }, "sha512-tZL5w3Y0JnO7RkIvfuNDAzJn0j6+JIYl6M8DgPM5p8AQt+162S8LmbumzmqQLZl4cEev2eN7/tw72WIk6b+/CQ=="], - "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.103", "", { "os": "win32", "cpu": "x64" }, "sha512-wqnibt/OE5ldSzVPxEbriA0TjI2B11CJl4uJoLxTZ47KDx7tAFIMdhBf9IRAGNCSbcDuZ8ZGEFhV+SLaftkrlw=="], - "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], + "@opentui/solid": ["@opentui/solid@0.1.103", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.103", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-L28WFBs17Z5JXkJhPagLy8tUamkttDaaQDdb2KEO01IQ9r81yLBRpYqD/lHp6UoSISXgwQVDQ9yNtxwR1BQZvQ=="], "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], @@ -2140,7 +2149,7 @@ "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="], - "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }, "sha512-7JjjA49VGNOsMRI8QRUhVudZmv0CnJ18SliSgK1ojszs/c3ijftgVkzvXdkSLN4miDTzbkXewf65D6ZBo6W+GQ=="], + "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }], "@speed-highlight/core": ["@speed-highlight/core@1.2.15", "", {}, "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw=="], diff --git a/nix/hashes.json b/nix/hashes.json index c09604610638..84fb57a1ca75 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-AgHhYsiygxbsBo3JN4HqHXKAwh8n1qeuSCe2qqxlxW4=", - "aarch64-linux": "sha256-h2lpWRQ5EDYnjpqZXtUAp1mxKLQxJ4m8MspgSY8Ev78=", - "aarch64-darwin": "sha256-xnd91+WyeAqn06run2ajsekxJvTMiLsnqNPe/rR8VTM=", - "x86_64-darwin": "sha256-rXpz45IOjGEk73xhP9VY86eOj2CZBg2l1vzwzTIOOOQ=" + "x86_64-linux": "sha256-LpzWEZzURUEj7fcHGvh33gM7D9GNPE+XIvU0/hmdcQM=", + "aarch64-linux": "sha256-0zdO3zuj6g9cMZFEOsvQJcKKcPjGVZJ2DkJdDcb2VCM=", + "aarch64-darwin": "sha256-dmT8R9Pmzh5tjO8NCCCtENiQpJQeifQpVdhaty1MXOs=", + "x86_64-darwin": "sha256-Q6rAQRoC6WaMAQl++YHAZmbNuO303cWgGaYzXaRlzy4=" } } diff --git a/package.json b/package.json index f918bcd025f5..b2c8a2d7a8ae 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "@types/cross-spawn": "6.0.6", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "0.1.103", + "@opentui/solid": "0.1.103", "ulid": "3.0.1", "@kobalte/core": "0.13.11", "@types/luxon": "3.7.1", @@ -46,6 +46,7 @@ "@cloudflare/workers-types": "4.20251008.0", "@openauthjs/openauth": "0.0.0-20250322224806", "@pierre/diffs": "1.1.0-beta.18", + "opentui-spinner": "0.0.6", "@solid-primitives/storage": "4.3.3", "@tailwindcss/vite": "4.1.11", "diff": "8.0.2", diff --git a/packages/app/package.json b/packages/app/package.json index 0ab5e304a2b8..f9d8150ba215 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.14.22", + "version": "1.14.25", "description": "", "type": "module", "exports": { @@ -42,7 +42,7 @@ "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@shikijs/transformers": "3.9.2", "@solid-primitives/active-element": "2.1.3", "@solid-primitives/audio": "1.4.2", diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index 8eb12daf52e5..b4b69246cbdd 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -9,7 +9,7 @@ import { createStore } from "solid-js/store" import { useGlobalSDK } from "@/context/global-sdk" import { useGlobalSync } from "@/context/global-sync" import { type LocalProject, getAvatarColors } from "@/context/layout" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { Avatar } from "@opencode-ai/ui/avatar" import { useLanguage } from "@/context/language" import { getProjectAvatarSource } from "@/pages/layout/sidebar-items" diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 710618c30160..3618a0581e02 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -9,7 +9,7 @@ import { List } from "@opencode-ai/ui/list" import { showToast } from "@opencode-ai/ui/toast" import { extractPromptFromParts } from "@/utils/prompt" import type { TextPart as SDKTextPart } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { useLanguage } from "@/context/language" interface ForkableMessage { diff --git a/packages/app/src/components/dialog-select-directory.tsx b/packages/app/src/components/dialog-select-directory.tsx index 903cb1915da7..005d28709161 100644 --- a/packages/app/src/components/dialog-select-directory.tsx +++ b/packages/app/src/components/dialog-select-directory.tsx @@ -3,7 +3,7 @@ import { Dialog } from "@opencode-ai/ui/dialog" import { FileIcon } from "@opencode-ai/ui/file-icon" import { List } from "@opencode-ai/ui/list" import type { ListRef } from "@opencode-ai/ui/list" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import fuzzysort from "fuzzysort" import { createMemo, createResource, createSignal } from "solid-js" import { useGlobalSDK } from "@/context/global-sdk" diff --git a/packages/app/src/components/dialog-select-file.tsx b/packages/app/src/components/dialog-select-file.tsx index 186906f92049..63a321e46a4f 100644 --- a/packages/app/src/components/dialog-select-file.tsx +++ b/packages/app/src/components/dialog-select-file.tsx @@ -4,8 +4,8 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" import { Keybind } from "@opencode-ai/ui/keybind" import { List } from "@opencode-ai/ui/list" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { useNavigate } from "@solidjs/router" import { createMemo, createSignal, Match, onCleanup, Show, Switch } from "solid-js" import { formatKeybind, useCommand, type CommandOption } from "@/context/command" diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts index c268af35eefb..98771aedd199 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -1,4 +1,4 @@ -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client" import type { FileSelection } from "@/context/file" import { encodeFilePath } from "@/context/file/path" diff --git a/packages/app/src/components/prompt-input/context-items.tsx b/packages/app/src/components/prompt-input/context-items.tsx index 9f20f1c04b0a..95289f9894ba 100644 --- a/packages/app/src/components/prompt-input/context-items.tsx +++ b/packages/app/src/components/prompt-input/context-items.tsx @@ -2,7 +2,7 @@ import { Component, For, Show } from "solid-js" import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename, getFilenameTruncated } from "@opencode-ai/core/util/path" import type { ContextItem } from "@/context/prompt" type PromptContextItem = ContextItem & { key: string } diff --git a/packages/app/src/components/prompt-input/placeholder.test.ts b/packages/app/src/components/prompt-input/placeholder.test.ts index 5f6aa59e9a41..d4caead0d2e4 100644 --- a/packages/app/src/components/prompt-input/placeholder.test.ts +++ b/packages/app/src/components/prompt-input/placeholder.test.ts @@ -12,7 +12,7 @@ describe("promptPlaceholder", () => { suggest: true, t, }) - expect(value).toBe("prompt.placeholder.shell") + expect(value).toBe("prompt.placeholder.shell:example") }) test("returns summarize placeholders for comment context", () => { diff --git a/packages/app/src/components/prompt-input/slash-popover.tsx b/packages/app/src/components/prompt-input/slash-popover.tsx index 0c8c95923493..d8c4bd035c75 100644 --- a/packages/app/src/components/prompt-input/slash-popover.tsx +++ b/packages/app/src/components/prompt-input/slash-popover.tsx @@ -1,7 +1,7 @@ import { Component, For, Match, Show, Switch } from "solid-js" import { FileIcon } from "@opencode-ai/ui/file-icon" import { Icon } from "@opencode-ai/ui/icon" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" export type AtOption = | { type: "agent"; name: string; display: string } diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index cf99497232d4..83b6212dcc56 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -74,7 +74,7 @@ beforeAll(async () => { showToast: () => 0, })) - mock.module("@opencode-ai/shared/util/encode", () => ({ + mock.module("@opencode-ai/core/util/encode", () => ({ base64Encode: (value: string) => value, })) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 6805f619c194..05f0a3ed2cb3 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -1,7 +1,7 @@ import type { Message, Session } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { Binary } from "@opencode-ai/shared/util/binary" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { Binary } from "@opencode-ai/core/util/binary" import { useNavigate, useParams } from "@solidjs/router" import { batch, type Accessor } from "solid-js" import type { FileSelection } from "@/context/file" diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index abf4c933462d..43741bd3fc0d 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -1,8 +1,8 @@ import { createMemo, createEffect, on, onCleanup, For, Show } from "solid-js" import type { JSX } from "solid-js" import { useSync } from "@/context/sync" -import { checksum } from "@opencode-ai/shared/util/encode" -import { findLast } from "@opencode-ai/shared/util/array" +import { checksum } from "@opencode-ai/core/util/encode" +import { findLast } from "@opencode-ai/core/util/array" import { same } from "@/utils/same" import { Icon } from "@opencode-ai/ui/icon" import { Accordion } from "@opencode-ai/ui/accordion" diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 021e5be67e35..3d4f58deec44 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -7,7 +7,7 @@ import { Keybind } from "@opencode-ai/ui/keybind" import { Spinner } from "@opencode-ai/ui/spinner" import { showToast } from "@opencode-ai/ui/toast" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js" import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" diff --git a/packages/app/src/components/session/session-new-view.tsx b/packages/app/src/components/session/session-new-view.tsx index d2cac28fc430..36c1eb42c316 100644 --- a/packages/app/src/components/session/session-new-view.tsx +++ b/packages/app/src/components/session/session-new-view.tsx @@ -5,7 +5,7 @@ import { useSDK } from "@/context/sdk" import { useLanguage } from "@/context/language" import { Icon } from "@opencode-ai/ui/icon" import { Mark } from "@opencode-ai/ui/logo" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" const MAIN_WORKTREE = "main" const CREATE_WORKTREE = "create" diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx index fb2275c445a7..f04228ca66c7 100644 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ b/packages/app/src/components/session/session-sortable-tab.tsx @@ -5,7 +5,7 @@ import { FileIcon } from "@opencode-ai/ui/file-icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { TooltipKeybind } from "@opencode-ai/ui/tooltip" import { Tabs } from "@opencode-ai/ui/tabs" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { useFile } from "@/context/file" import { useLanguage } from "@/context/language" import { useCommand } from "@/context/command" diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index 8998731a6cb4..0298e3416afd 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -3,7 +3,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { createSimpleContext } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" import { useParams } from "@solidjs/router" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { useSDK } from "./sdk" import { useSync } from "./sync" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index b742667d72c1..86496bad50c2 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -8,7 +8,7 @@ import type { Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index be789a5e53a6..66f4a3b156b0 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -11,8 +11,8 @@ import type { Todo, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" -import { getFilename } from "@opencode-ai/shared/util/path" -import { retry } from "@opencode-ai/shared/util/retry" +import { getFilename } from "@opencode-ai/core/util/path" +import { retry } from "@opencode-ai/core/util/retry" import { batch } from "solid-js" import { reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 82408fdfe9e5..5f43c341bc0b 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -1,4 +1,4 @@ -import { Binary } from "@opencode-ai/shared/util/binary" +import { Binary } from "@opencode-ai/core/util/binary" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { Message, diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 0b0972ee6703..2db0f9b04f91 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,5 +1,5 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { useParams } from "@solidjs/router" import { batch, createEffect, createMemo } from "solid-js" import { createStore } from "solid-js/store" diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 251b67b06ce9..c926dc1d99ae 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -7,8 +7,8 @@ import { useGlobalSync } from "./global-sync" import { usePlatform } from "@/context/platform" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" -import { Binary } from "@opencode-ai/shared/util/binary" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { Binary } from "@opencode-ai/core/util/binary" +import { base64Encode } from "@opencode-ai/core/util/encode" import { decode64 } from "@/utils/base64" import { EventSessionError } from "@opencode-ai/sdk/v2" import { Persist, persisted } from "@/utils/persist" diff --git a/packages/app/src/context/permission-auto-respond.test.ts b/packages/app/src/context/permission-auto-respond.test.ts index 2f8ca6265e6c..002ae94e5b92 100644 --- a/packages/app/src/context/permission-auto-respond.test.ts +++ b/packages/app/src/context/permission-auto-respond.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import type { PermissionRequest, Session } from "@opencode-ai/sdk/v2/client" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { autoRespondsPermission, isDirectoryAutoAccepting } from "./permission-auto-respond" const session = (input: { id: string; parentID?: string }) => diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts index 2ebca34347d5..58ab75c57d91 100644 --- a/packages/app/src/context/permission-auto-respond.ts +++ b/packages/app/src/context/permission-auto-respond.ts @@ -1,4 +1,4 @@ -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" export function acceptKey(sessionID: string, directory?: string) { if (!directory) return sessionID diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index 15af57b355ee..dffb7983104b 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -1,5 +1,5 @@ import { createSimpleContext } from "@opencode-ai/ui/context" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { useParams } from "@solidjs/router" import { batch, createMemo, createRoot, getOwner, onCleanup } from "solid-js" import { createStore, type SetStoreFunction } from "solid-js/store" diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index 29b7fe68c511..34b597b6bb52 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -1,7 +1,7 @@ import { batch, createMemo } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" -import { Binary } from "@opencode-ai/shared/util/binary" -import { retry } from "@opencode-ai/shared/util/retry" +import { Binary } from "@opencode-ai/core/util/binary" +import { retry } from "@opencode-ai/core/util/retry" import { createSimpleContext } from "@opencode-ai/ui/context" import { clearSessionPrefetch, diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index 36514f56c63a..90ce3c1a5209 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -1,6 +1,6 @@ import { DataProvider } from "@opencode-ai/ui/context" import { showToast } from "@opencode-ai/ui/toast" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { useLocation, useNavigate, useParams } from "@solidjs/router" import { createEffect, createMemo, createResource, type ParentProps, Show } from "solid-js" import { useLanguage } from "@/context/language" diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 46cacdf627ab..2df69ee92251 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -3,7 +3,7 @@ import { Button } from "@opencode-ai/ui/button" import { Logo } from "@opencode-ai/ui/logo" import { useLayout } from "@/context/layout" import { useNavigate } from "@solidjs/router" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { Icon } from "@opencode-ai/ui/icon" import { usePlatform } from "@/context/platform" import { DateTime } from "luxon" diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index ac5cf104aa7e..d9ce87a02e90 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -17,7 +17,7 @@ import { useLocation, useNavigate, useParams } from "@solidjs/router" import { useLayout, LocalProject } from "@/context/layout" import { useGlobalSync } from "@/context/global-sync" import { Persist, persisted } from "@/utils/persist" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { decode64 } from "@/utils/base64" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Button } from "@opencode-ai/ui/button" @@ -25,7 +25,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Tooltip } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { Session, type Message } from "@opencode-ai/sdk/v2/client" import { usePlatform } from "@/context/platform" import { useSettings } from "@/context/settings" @@ -48,8 +48,8 @@ import { } from "@/context/global-sync/session-prefetch" import { useNotification } from "@/context/notification" import { usePermission } from "@/context/permission" -import { Binary } from "@opencode-ai/shared/util/binary" -import { retry } from "@opencode-ai/shared/util/retry" +import { Binary } from "@opencode-ai/core/util/binary" +import { retry } from "@opencode-ai/core/util/retry" import { playSoundById } from "@/utils/sound" import { createAim } from "@/utils/aim" import { setNavigate } from "@/utils/notification-click" diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts index 32b94c9cb760..4bc5254d959d 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -1,4 +1,4 @@ -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { type Session } from "@opencode-ai/sdk/v2/client" type SessionStore = { diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx index 9a9a1d7fca6a..d9fd4d6a8398 100644 --- a/packages/app/src/pages/layout/sidebar-items.tsx +++ b/packages/app/src/pages/layout/sidebar-items.tsx @@ -4,7 +4,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { IconButton } from "@opencode-ai/ui/icon-button" import { Spinner } from "@opencode-ai/ui/spinner" import { Tooltip } from "@opencode-ai/ui/tooltip" -import { getFilename } from "@opencode-ai/shared/util/path" +import { getFilename } from "@opencode-ai/core/util/path" import { A, useParams } from "@solidjs/router" import { type Accessor, createMemo, For, type JSX, Match, Show, Switch } from "solid-js" import { useGlobalSync } from "@/context/global-sync" diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx index 076e1ef88b54..2ba20092c585 100644 --- a/packages/app/src/pages/layout/sidebar-project.tsx +++ b/packages/app/src/pages/layout/sidebar-project.tsx @@ -1,6 +1,6 @@ import { createMemo, For, Show, type Accessor, type JSX } from "solid-js" import { createStore } from "solid-js/store" -import { base64Encode } from "@opencode-ai/shared/util/encode" +import { base64Encode } from "@opencode-ai/core/util/encode" import { Button } from "@opencode-ai/ui/button" import { ContextMenu } from "@opencode-ai/ui/context-menu" import { HoverCard } from "@opencode-ai/ui/hover-card" diff --git a/packages/app/src/pages/layout/sidebar-workspace.tsx b/packages/app/src/pages/layout/sidebar-workspace.tsx index cbb570106530..0a3fc7f41d50 100644 --- a/packages/app/src/pages/layout/sidebar-workspace.tsx +++ b/packages/app/src/pages/layout/sidebar-workspace.tsx @@ -3,8 +3,8 @@ import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "so import { createStore } from "solid-js/store" import { createSortable } from "@thisbeyond/solid-dnd" import { createMediaQuery } from "@solid-primitives/media" -import { base64Encode } from "@opencode-ai/shared/util/encode" -import { getFilename } from "@opencode-ai/shared/util/path" +import { base64Encode } from "@opencode-ai/core/util/encode" +import { getFilename } from "@opencode-ai/core/util/path" import { Button } from "@opencode-ai/ui/button" import { Collapsible } from "@opencode-ai/ui/collapsible" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 4ae973b85815..1345e355eb25 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -28,7 +28,7 @@ import { createAutoScroll } from "@opencode-ai/ui/hooks" import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { Button } from "@opencode-ai/ui/button" import { showToast } from "@opencode-ai/ui/toast" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { useSearchParams } from "@solidjs/router" import { NewSessionView, SessionHeader } from "@/components/session" import { useComments } from "@/context/comments" diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 37bffcd2fa56..65b076d7c630 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -6,7 +6,7 @@ import type { FileSearchHandle } from "@opencode-ai/ui/file" import { useFileComponent } from "@opencode-ai/ui/context/file" import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge" import { createLineCommentController } from "@opencode-ai/ui/line-comment-annotations" -import { sampledChecksum } from "@opencode-ai/shared/util/encode" +import { sampledChecksum } from "@opencode-ai/core/util/encode" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { IconButton } from "@opencode-ai/ui/icon-button" import { Tabs } from "@opencode-ai/ui/tabs" diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index 592ca774e624..8bbaafb4e433 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -15,8 +15,8 @@ import { ScrollView } from "@opencode-ai/ui/scroll-view" import { TextField } from "@opencode-ai/ui/text-field" import type { AssistantMessage, Message as MessageType, Part, TextPart, UserMessage } from "@opencode-ai/sdk/v2" import { showToast } from "@opencode-ai/ui/toast" -import { Binary } from "@opencode-ai/shared/util/binary" -import { getFilename } from "@opencode-ai/shared/util/path" +import { Binary } from "@opencode-ai/core/util/binary" +import { getFilename } from "@opencode-ai/core/util/path" import { Popover as KobaltePopover } from "@kobalte/core/popover" import { shouldMarkBoundaryGesture, normalizeWheelDelta } from "@/pages/session/message-gesture" import { SessionContextUsage } from "@/components/session-context-usage" diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index d649aeb0cb9a..922299bec198 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -14,7 +14,7 @@ import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { showToast } from "@opencode-ai/ui/toast" -import { findLast } from "@opencode-ai/shared/util/array" +import { findLast } from "@opencode-ai/core/util/array" import { createSessionTabs } from "@/pages/session/helpers" import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" diff --git a/packages/app/src/utils/base64.ts b/packages/app/src/utils/base64.ts index f60dff2b6d81..34b904051caa 100644 --- a/packages/app/src/utils/base64.ts +++ b/packages/app/src/utils/base64.ts @@ -1,4 +1,4 @@ -import { base64Decode } from "@opencode-ai/shared/util/encode" +import { base64Decode } from "@opencode-ai/core/util/encode" export function decode64(value: string | undefined) { if (value === undefined) return diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 0cac30cb1e21..024552727439 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -1,6 +1,6 @@ import { Platform, usePlatform } from "@/context/platform" import { makePersisted, type AsyncStorage, type SyncStorage } from "@solid-primitives/storage" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { createResource, type Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 3d375275081f..5abd19256801 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.14.22", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index a7883cfe4c6d..5c0919e8e26c 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -249,7 +249,7 @@ export const dict = { "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", "go.meta.description": - "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.", + "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", "go.problem.item4": - "يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7", + "يتضمن GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -324,7 +324,7 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7.", + "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -347,7 +347,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج GLM-5.1 وGLM-5 وKimi K2.5 وKimi K2.6 وMiMo-V2-Pro وMiMo-V2-Omni وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.5 Plus وQwen3.6 Plus وMiniMax M2.5 وMiniMax M2.7 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index cf7b68d259c3..76e6987d3e1b 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.hero.title": "Modelos de codificação de baixo custo para todos", "go.hero.body": "O Go traz a codificação com agentes para programadores em todo o mundo. Oferecendo limites generosos e acesso confiável aos modelos de código aberto mais capazes, para que você possa construir com agentes poderosos sem se preocupar com custos ou disponibilidade.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Limites generosos e acesso confiável", "go.problem.item3": "Feito para o maior número possível de programadores", "go.problem.item4": - "Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7", + "Inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash", "go.how.title": "Como o Go funciona", "go.how.body": "O Go começa em $5 no primeiro mês, depois $10/mês. Você pode usá-lo com o OpenCode ou qualquer agente.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "O Go inclui os modelos listados abaixo, com limites generosos e acesso confiável.", "go.faq.q3": "O Go é o mesmo que o Zen?", "go.faq.a3": - "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "Não. Zen é pay-as-you-go, enquanto o Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos e acesso confiável aos modelos open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.faq.q4": "Quanto custa o Go?", "go.faq.a4.p1.beforePricing": "O Go custa", "go.faq.a4.p1.pricingLink": "$5 no primeiro mês", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "Qual a diferença entre os modelos gratuitos e o Go?", "go.faq.a9": - "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", + "Os modelos gratuitos incluem Big Pickle e modelos promocionais disponíveis no momento, com uma cota de 200 requisições/dia. O Go inclui GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash com cotas de requisição mais altas aplicadas em janelas móveis (5 horas, semanal e mensal), aproximadamente equivalentes a $12 por 5 horas, $30 por semana e $60 por mês (as contagens reais de requisições variam de acordo com o modelo e o uso).", "zen.api.error.rateLimitExceeded": "Limite de taxa excedido. Por favor, tente novamente mais tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} não suportado", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 90eff469a226..b97ee2cc0a42 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.hero.title": "Kodningsmodeller til lav pris for alle", "go.hero.body": "Go bringer agentisk kodning til programmører over hele verden. Med generøse grænser og pålidelig adgang til de mest kapable open source-modeller, så du kan bygge med kraftfulde agenter uden at bekymre dig om omkostninger eller tilgængelighed.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "Generøse grænser og pålidelig adgang", "go.problem.item3": "Bygget til så mange programmører som muligt", "go.problem.item4": - "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7", + "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash", "go.how.title": "Hvordan Go virker", "go.how.body": "Go starter ved $5 for den første måned, derefter $10/måned. Du kan bruge det med OpenCode eller enhver agent.", @@ -328,7 +328,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellerne nedenfor med generøse grænser og pålidelig adgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Nej. Zen er pay-as-you-go, mens Go starter ved $5 for den første måned, derefter $10/måned, med generøse grænser og pålidelig adgang til open source-modellerne GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.faq.q4": "Hvad koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", + "Gratis modeller inkluderer Big Pickle plus salgsfremmende modeller tilgængelige på det tidspunkt, med en kvote på 200 forespørgsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med højere anmodningskvoter håndhævet over rullende vinduer (5-timers, ugentlig og månedlig), nogenlunde svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (faktiske anmodningstal varierer efter model og brug).", "zen.api.error.rateLimitExceeded": "Hastighedsgrænse overskredet. Prøv venligst igen senere.", "zen.api.error.modelNotSupported": "Model {{model}} understøttes ikke", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index af339802fac2..33b6e1b3de0c 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", "go.meta.description": - "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.", + "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.", "go.hero.title": "Kostengünstige Coding-Modelle für alle", "go.hero.body": "Go bringt Agentic Coding zu Programmierern auf der ganzen Welt. Mit großzügigen Limits und zuverlässigem Zugang zu den leistungsfähigsten Open-Source-Modellen, damit du mit leistungsstarken Agenten entwickeln kannst, ohne dir Gedanken über Kosten oder Verfügbarkeit zu machen.", @@ -304,7 +304,7 @@ export const dict = { "go.problem.item2": "Großzügige Limits und zuverlässiger Zugang", "go.problem.item3": "Für so viele Programmierer wie möglich gebaut", "go.problem.item4": - "Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7", + "Beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash", "go.how.title": "Wie Go funktioniert", "go.how.body": "Go beginnt bei $5 für den ersten Monat, danach $10/Monat. Du kannst es mit OpenCode oder jedem Agenten nutzen.", @@ -330,7 +330,7 @@ export const dict = { "go.faq.a2": "Go umfasst die unten aufgeführten Modelle mit großzügigen Limits und zuverlässigem Zugriff.", "go.faq.q3": "Ist Go dasselbe wie Zen?", "go.faq.a3": - "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7.", + "Nein. Zen ist Pay-as-you-go, während Go bei $5 für den ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu den Open-Source-Modellen GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash.", "go.faq.q4": "Wie viel kostet Go?", "go.faq.a4.p1.beforePricing": "Go kostet", "go.faq.a4.p1.pricingLink": "$5 im ersten Monat", @@ -354,7 +354,7 @@ export const dict = { "go.faq.q9": "Was ist der Unterschied zwischen kostenlosen Modellen und Go?", "go.faq.a9": - "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 und MiniMax M2.7 mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", + "Kostenlose Modelle beinhalten Big Pickle sowie Werbemodelle, die zum jeweiligen Zeitpunkt verfügbar sind, mit einem Kontingent von 200 Anfragen/Tag. Go beinhaltet GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro und DeepSeek V4 Flash mit höheren Anfragekontingenten, die über rollierende Zeitfenster (5 Stunden, wöchentlich und monatlich) durchgesetzt werden, grob äquivalent zu $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).", "zen.api.error.rateLimitExceeded": "Ratenlimit überschritten. Bitte versuche es später erneut.", "zen.api.error.modelNotSupported": "Modell {{model}} wird nicht unterstützt", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index f5cc954e5ef6..b6934b94de55 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -248,7 +248,7 @@ export const dict = { "go.title": "OpenCode Go | Low cost coding models for everyone", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.", + "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.", "go.banner.badge": "3x", "go.banner.text": "Kimi K2.6 gets 3× usage limits through April 27", "go.hero.title": "Low cost coding models for everyone", @@ -298,7 +298,7 @@ export const dict = { "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", "go.problem.item4": - "Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7", + "Includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash", "go.how.title": "How Go works", "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", "go.how.step1.title": "Create an account", @@ -323,7 +323,7 @@ export const dict = { "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", "go.faq.q3": "Is Go the same as Zen?", "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7.", + "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to open-source models GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash.", "go.faq.q4": "How much does Go cost?", "go.faq.a4.p1.beforePricing": "Go costs", "go.faq.a4.p1.pricingLink": "$5 first month", @@ -347,7 +347,7 @@ export const dict = { "go.faq.q9": "What is the difference between free models and Go?", "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, and MiniMax M2.7 with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", + "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go includes GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, and DeepSeek V4 Flash with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", "zen.api.error.modelNotSupported": "Model {{model}} not supported", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index fb718e0541e8..c5cc71ae1e8d 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -254,7 +254,7 @@ export const dict = { "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.", "go.hero.title": "Modelos de programación de bajo coste para todos", "go.hero.body": "Go lleva la programación agéntica a programadores de todo el mundo. Ofrece límites generosos y acceso fiable a los modelos de código abierto más capaces, para que puedas crear con agentes potentes sin preocuparte por el coste o la disponibilidad.", @@ -306,7 +306,7 @@ export const dict = { "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", "go.problem.item4": - "Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7", + "Incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash", "go.how.title": "Cómo funciona Go", "go.how.body": "Go comienza en $5 el primer mes, luego 10 $/mes. Puedes usarlo con OpenCode o cualquier agente.", "go.how.step1.title": "Crear una cuenta", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go incluye los modelos que se indican abajo, con límites generosos y acceso confiable.", "go.faq.q3": "¿Es Go lo mismo que Zen?", "go.faq.a3": - "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7.", + "No. Zen es pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a los modelos de código abierto GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash.", "go.faq.q4": "¿Cuánto cuesta Go?", "go.faq.a4.p1.beforePricing": "Go cuesta", "go.faq.a4.p1.pricingLink": "$5 el primer mes", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "¿Cuál es la diferencia entre los modelos gratuitos y Go?", "go.faq.a9": - "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 y MiniMax M2.7 con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", + "Los modelos gratuitos incluyen Big Pickle más modelos promocionales disponibles en el momento, con una cuota de 200 solicitudes/día. Go incluye GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro y DeepSeek V4 Flash con cuotas de solicitud más altas aplicadas a través de ventanas móviles (5 horas, semanal y mensual), aproximadamente equivalente a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (los recuentos reales de solicitudes varían según el modelo y el uso).", "zen.api.error.rateLimitExceeded": "Límite de tasa excedido. Por favor, inténtalo de nuevo más tarde.", "zen.api.error.modelNotSupported": "Modelo {{model}} no soportado", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 976d93a29a37..04e6e3bc62b9 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.", "go.hero.title": "Modèles de code à faible coût pour tous", "go.hero.body": "Go apporte le codage agentique aux programmeurs du monde entier. Offrant des limites généreuses et un accès fiable aux modèles open source les plus capables, pour que vous puissiez construire avec des agents puissants sans vous soucier du coût ou de la disponibilité.", @@ -306,7 +306,7 @@ export const dict = { "go.problem.item2": "Limites généreuses et accès fiable", "go.problem.item3": "Conçu pour autant de programmeurs que possible", "go.problem.item4": - "Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7", + "Inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash", "go.how.title": "Comment fonctionne Go", "go.how.body": "Go commence à $5 pour le premier mois, puis 10 $/mois. Vous pouvez l'utiliser avec OpenCode ou n'importe quel agent.", @@ -332,7 +332,7 @@ export const dict = { "go.faq.a2": "Go inclut les modèles ci-dessous, avec des limites généreuses et un accès fiable.", "go.faq.q3": "Est-ce que Go est la même chose que Zen ?", "go.faq.a3": - "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7.", + "Non. Zen est un paiement à l'utilisation, tandis que Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites généreuses et un accès fiable aux modèles open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash.", "go.faq.q4": "Combien coûte Go ?", "go.faq.a4.p1.beforePricing": "Go coûte", "go.faq.a4.p1.pricingLink": "$5 le premier mois", @@ -355,7 +355,7 @@ export const dict = { "Oui, vous pouvez utiliser Go avec n'importe quel agent. Suivez les instructions de configuration dans votre agent de code préféré.", "go.faq.q9": "Quelle est la différence entre les modèles gratuits et Go ?", "go.faq.a9": - "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 et MiniMax M2.7 avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", + "Les modèles gratuits incluent Big Pickle ainsi que des modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go inclut GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro et DeepSeek V4 Flash avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalent à 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois (le nombre réel de requêtes varie selon le modèle et l'utilisation).", "zen.api.error.rateLimitExceeded": "Limite de débit dépassée. Veuillez réessayer plus tard.", "zen.api.error.modelNotSupported": "Modèle {{model}} non pris en charge", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 6069ad73ce36..13f33bfc39f0 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.hero.title": "Modelli di coding a basso costo per tutti", "go.hero.body": "Go porta il coding agentico ai programmatori di tutto il mondo. Offrendo limiti generosi e un accesso affidabile ai modelli open source più capaci, in modo da poter costruire con agenti potenti senza preoccuparsi dei costi o della disponibilità.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", "go.problem.item4": - "Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7", + "Include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash", "go.how.title": "Come funziona Go", "go.how.body": "Go inizia a $5 per il primo mese, poi $10/mese. Puoi usarlo con OpenCode o qualsiasi agente.", "go.how.step1.title": "Crea un account", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go include i modelli elencati di seguito, con limiti generosi e accesso affidabile.", "go.faq.q3": "Go è lo stesso di Zen?", "go.faq.a3": - "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7.", + "No. Zen è a consumo, mentre Go inizia a $5 per il primo mese, poi $10/mese, con limiti generosi e accesso affidabile ai modelli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash.", "go.faq.q4": "Quanto costa Go?", "go.faq.a4.p1.beforePricing": "Go costa", "go.faq.a4.p1.pricingLink": "$5 il primo mese", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "Qual è la differenza tra i modelli gratuiti e Go?", "go.faq.a9": - "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 e MiniMax M2.7 con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", + "I modelli gratuiti includono Big Pickle più modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go include GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro e DeepSeek V4 Flash con quote di richiesta più elevate applicate su finestre mobili (5 ore, settimanale e mensile), approssimativamente equivalenti a $12 ogni 5 ore, $30 a settimana e $60 al mese (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).", "zen.api.error.rateLimitExceeded": "Limite di richieste superato. Riprova più tardi.", "zen.api.error.modelNotSupported": "Modello {{model}} non supportato", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index dcf2f9b52f7a..845faebf618a 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", "go.meta.description": - "Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7に対して5時間のゆとりあるリクエスト上限があります。", + "Goは最初の月$5、その後$10/月で、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", "go.problem.item4": - "GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7を含む", + "GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashを含む", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -327,7 +327,7 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7のオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -351,7 +351,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7が含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index f2a67fbbae63..7efe563a079a 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -247,7 +247,7 @@ export const dict = { "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7에 대해 넉넉한 5시간 요청 한도를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -299,7 +299,7 @@ export const dict = { "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", "go.problem.item4": - "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 포함", + "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 포함", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -323,7 +323,7 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -346,7 +346,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 0207a57760ce..8948e158b0be 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -251,7 +251,7 @@ export const dict = { "go.title": "OpenCode Go | Rimelige kodemodeller for alle", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.hero.title": "Rimelige kodemodeller for alle", "go.hero.body": "Go bringer agent-koding til programmerere over hele verden. Med rause grenser og pålitelig tilgang til de mest kapable åpen kildekode-modellene, kan du bygge med kraftige agenter uten å bekymre deg for kostnader eller tilgjengelighet.", @@ -302,7 +302,7 @@ export const dict = { "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", "go.problem.item4": - "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7", + "Inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash", "go.how.title": "Hvordan Go fungerer", "go.how.body": "Go starter på $5 for den første måneden, deretter $10/måned. Du kan bruke det med OpenCode eller hvilken som helst agent.", @@ -328,7 +328,7 @@ export const dict = { "go.faq.a2": "Go inkluderer modellene nedenfor, med høye grenser og pålitelig tilgang.", "go.faq.q3": "Er Go det samme som Zen?", "go.faq.a3": - "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7.", + "Nei. Zen er betaling etter bruk, mens Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse grenser og pålitelig tilgang til åpen kildekode-modellene GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash.", "go.faq.q4": "Hva koster Go?", "go.faq.a4.p1.beforePricing": "Go koster", "go.faq.a4.p1.pricingLink": "$5 første måned", @@ -352,7 +352,7 @@ export const dict = { "go.faq.q9": "Hva er forskjellen mellom gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 og MiniMax M2.7 med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", + "Gratis modeller inkluderer Big Pickle pluss kampanjemodeller tilgjengelig på det tidspunktet, med en kvote på 200 forespørsler/dag. Go inkluderer GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro og DeepSeek V4 Flash med høyere kvoter håndhevet over rullerende vinduer (5 timer, ukentlig og månedlig), omtrent tilsvarende $12 per 5 timer, $30 per uke og $60 per måned (faktiske forespørselsantall varierer etter modell og bruk).", "zen.api.error.rateLimitExceeded": "Rate limit overskredet. Vennligst prøv igjen senere.", "zen.api.error.modelNotSupported": "Modell {{model}} støttes ikke", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 554a2d0aabd4..f879ed705799 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -252,7 +252,7 @@ export const dict = { "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", "go.meta.description": - "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.", + "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.", "go.hero.title": "Niskokosztowe modele do kodowania dla każdego", "go.hero.body": "Go udostępnia programowanie z agentami programistom na całym świecie. Oferuje hojne limity i niezawodny dostęp do najzdolniejszych modeli open source, dzięki czemu możesz budować za pomocą potężnych agentów, nie martwiąc się o koszty czy dostępność.", @@ -303,7 +303,7 @@ export const dict = { "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", "go.problem.item4": - "Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7", + "Zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash", "go.how.title": "Jak działa Go", "go.how.body": "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc. Możesz go używać z OpenCode lub dowolnym agentem.", @@ -329,7 +329,7 @@ export const dict = { "go.faq.a2": "Go obejmuje poniższe modele z wysokimi limitami i niezawodnym dostępem.", "go.faq.q3": "Czy Go to to samo co Zen?", "go.faq.a3": - "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7.", + "Nie. Zen to model płatności za użycie, podczas gdy Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi limitami i niezawodnym dostępem do modeli open source GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash.", "go.faq.q4": "Ile kosztuje Go?", "go.faq.a4.p1.beforePricing": "Go kosztuje", "go.faq.a4.p1.pricingLink": "$5 za pierwszy miesiąc", @@ -353,7 +353,7 @@ export const dict = { "go.faq.q9": "Jaka jest różnica między darmowymi modelami a Go?", "go.faq.a9": - "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 i MiniMax M2.7 z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", + "Darmowe modele obejmują Big Pickle oraz modele promocyjne dostępne w danym momencie, z limitem 200 zapytań/dzień. Go zawiera GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro i DeepSeek V4 Flash z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), w przybliżeniu równoważnymi $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie (rzeczywista liczba zapytań zależy od modelu i użycia).", "zen.api.error.rateLimitExceeded": "Przekroczono limit zapytań. Spróbuj ponownie później.", "zen.api.error.modelNotSupported": "Model {{model}} nie jest obsługiwany", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 1e50134199b1..9ba36d22081a 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -255,7 +255,7 @@ export const dict = { "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", "go.meta.description": - "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.", + "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -307,7 +307,7 @@ export const dict = { "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", "go.problem.item4": - "Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7", + "Включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -333,7 +333,7 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7.", + "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -357,7 +357,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 и MiniMax M2.7 с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index 3a2dc4ba4c38..01b2b19c39ad 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -250,7 +250,7 @@ export const dict = { "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -300,7 +300,7 @@ export const dict = { "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", "go.problem.item4": - "รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7", + "รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -325,7 +325,7 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 อย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -348,7 +348,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 และ MiniMax M2.7 ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index e2370f83c25d..0345277b8744 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -253,7 +253,7 @@ export const dict = { "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 için cömert 5 saatlik istek limitleri sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash için cömert 5 saatlik istek limitleri sunar.", "go.hero.title": "Herkes için düşük maliyetli kodlama modelleri", "go.hero.body": "Go, dünya çapındaki programcılara ajan tabanlı kodlama getiriyor. En yetenekli açık kaynaklı modellere cömert limitler ve güvenilir erişim sunarak, maliyet veya erişilebilirlik konusunda endişelenmeden güçlü ajanlarla geliştirme yapmanızı sağlar.", @@ -305,7 +305,7 @@ export const dict = { "go.problem.item2": "Cömert limitler ve güvenilir erişim", "go.problem.item3": "Mümkün olduğunca çok programcı için geliştirildi", "go.problem.item4": - "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 içerir", + "GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash içerir", "go.how.title": "Go nasıl çalışır?", "go.how.body": "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar. OpenCode veya herhangi bir ajanla kullanabilirsiniz.", @@ -331,7 +331,7 @@ export const dict = { "go.faq.a2": "Go, aşağıda listelenen modelleri cömert limitler ve güvenilir erişimle sunar.", "go.faq.q3": "Go, Zen ile aynı mı?", "go.faq.a3": - "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", + "Hayır. Zen kullandıkça öde modelidir, Go ise ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash açık kaynak modellerine cömert limitler ve güvenilir erişim sunar.", "go.faq.q4": "Go ne kadar?", "go.faq.a4.p1.beforePricing": "Go'nun maliyeti", "go.faq.a4.p1.pricingLink": "İlk ay $5", @@ -355,7 +355,7 @@ export const dict = { "go.faq.q9": "Ücretsiz modeller ve Go arasındaki fark nedir?", "go.faq.a9": - "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 ve MiniMax M2.7 modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", + "Ücretsiz modeller, günlük 200 istek kotası ile Big Pickle ve o sırada mevcut olan promosyonel modelleri içerir. Go ise GLM-5.1, GLM-5, Kimi K2.5, Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5, MiniMax M2.7, DeepSeek V4 Pro ve DeepSeek V4 Flash modellerini; yuvarlanan pencereler (5 saatlik, haftalık ve aylık) üzerinden uygulanan daha yüksek istek kotalarıyla içerir. Bu kotalar kabaca her 5 saatte 12$, haftada 30$ ve ayda 60$ değerine eşdeğerdir (gerçek istek sayıları modele ve kullanıma göre değişir).", "zen.api.error.rateLimitExceeded": "İstek limiti aşıldı. Lütfen daha sonra tekrar deneyin.", "zen.api.error.modelNotSupported": "{{model}} modeli desteklenmiyor", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 649384c23bb3..b9300cc87ea1 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -241,7 +241,7 @@ export const dict = { "go.title": "OpenCode Go | 人人可用的低成本编程模型", "go.meta.description": - "Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小时充裕请求额度。", + "Go 首月 $5,之后 $10/月,提供对 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -291,7 +291,7 @@ export const dict = { "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", "go.problem.item4": - "包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7", + "包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -313,7 +313,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", "go.faq.a3": - "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等开源模型。", + "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -335,7 +335,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5 和 MiniMax M2.7,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 GLM-5.1, GLM-5, Kimi K2.5、Kimi K2.6, MiMo-V2-Pro, MiMo-V2-Omni, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.5 Plus, Qwen3.6 Plus, MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 8e93f989e82c..f129a99d025b 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -241,7 +241,7 @@ export const dict = { "go.title": "OpenCode Go | 低成本全民編碼模型", "go.meta.description": - "Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 的 5 小時充裕請求額度。", + "Go 首月 $5,之後 $10/月,提供對 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -291,7 +291,7 @@ export const dict = { "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", "go.problem.item4": - "包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7", + "包含 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -313,7 +313,7 @@ export const dict = { "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", "go.faq.a3": - "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 和 MiniMax M2.7 等開源模型。", + "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -335,7 +335,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5 與 MiniMax M2.7,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 GLM-5.1、GLM-5、Kimi K2.5、Kimi K2.6、MiMo-V2-Pro、MiMo-V2-Omni、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.5 Plus、Qwen3.6 Plus、MiniMax M2.5、MiniMax M2.7、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index bfaf2969bc9e..2b169fd50058 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -23,8 +23,8 @@ const checkLoggedIn = query(async () => { }, "checkLoggedIn.get") const models = [ - { name: "GLM-5.1", provider: "DeepInfra, Z.ai" }, - { name: "GLM-5", provider: "DeepInfra, Z.ai" }, + { name: "GLM-5.1", provider: "DeepInfra, Fireworks AI, Z.ai" }, + { name: "GLM-5", provider: "DeepInfra, Fireworks AI, Z.ai" }, { name: "Kimi K2.5", provider: "Moonshot AI" }, { name: "Kimi K2.6", provider: "Moonshot AI" }, { name: "MiMo-V2-Pro", provider: "Xiaomi MiMo" }, @@ -35,6 +35,8 @@ const models = [ { name: "Qwen3.6 Plus", provider: "Alibaba Cloud Model Studio" }, { name: "MiniMax M2.7", provider: "MiniMax" }, { name: "MiniMax M2.5", provider: "MiniMax" }, + { name: "DeepSeek V4 Pro", provider: "DeepSeek" }, + { name: "DeepSeek V4 Flash", provider: "DeepSeek" }, ] function LimitsGraph(props: { href: string }) { @@ -63,13 +65,15 @@ function LimitsGraph(props: { href: string }) { { id: "glm-5.1", name: "GLM-5.1", req: 880, d: "100ms" }, { id: "kimi-k2.6", name: "Kimi K2.6 (3x usage)", req: 3450, baseReq: 1150, d: "150ms" }, { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 1290, d: "150ms" }, + { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 1300, d: "200ms" }, { id: "qwen3.6-plus", name: "Qwen3.6 Plus", req: 3300, d: "280ms" }, { id: "minimax-m2.7", name: "MiniMax M2.7", req: 3400, d: "300ms" }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7450, d: "340ms" }, { id: "qwen3.5-plus", name: "Qwen3.5 Plus", req: 10200, d: "360ms" }, ] const w = 720 - const h = 270 + const h = 330 const left = 40 const right = 60 const top = 18 @@ -103,7 +107,7 @@ function LimitsGraph(props: { href: string }) { })() const shown = ticks.filter((t) => labels.has(t)) const bh = 8 - const gap = 16 + const gap = 20 const step = bh + gap const sep = bh + 40 const fy = top + 22 diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index abd417e06b7a..0df181ae16d4 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -297,6 +297,8 @@ export function LiteSection() {
  • MiniMax M2.7
  • Qwen3.5 Plus
  • Qwen3.6 Plus
  • +
  • DeepSeek V4 Pro
  • +
  • DeepSeek V4 Flash
  • {i18n.t("workspace.lite.promo.footer")}

    diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 70d44b3820a4..31f4f9a0ae0d 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.14.22", + "version": "1.14.25", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 55791a3072cd..4f30ea99b455 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.14.22", + "version": "1.14.25", "$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 4966bf3e634c..39556c7d3941 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.14.22", + "version": "1.14.25", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/shared/package.json b/packages/core/package.json similarity index 53% rename from packages/shared/package.json rename to packages/core/package.json index 48ae0fb78df8..62d56908cbcf 100644 --- a/packages/shared/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.22", - "name": "@opencode-ai/shared", + "version": "1.14.25", + "name": "@opencode-ai/core", "type": "module", "license": "MIT", "private": true, @@ -18,18 +18,28 @@ "imports": {}, "devDependencies": { "@tsconfig/bun": "catalog:", - "@types/semver": "catalog:", "@types/bun": "catalog:", - "@types/npmcli__arborist": "6.3.3" + "@types/cross-spawn": "catalog:", + "@types/npm-package-arg": "6.1.4", + "@types/npmcli__arborist": "6.3.3", + "@types/semver": "catalog:" }, "dependencies": { + "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", - "@npmcli/arborist": "catalog:", + "@npmcli/arborist": "9.4.0", + "@npmcli/config": "10.8.1", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/context-async-hooks": "2.6.1", + "@opentelemetry/exporter-trace-otlp-http": "0.214.0", + "@opentelemetry/sdk-trace-base": "2.6.1", "effect": "catalog:", + "cross-spawn": "catalog:", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", - "semver": "catalog:", + "npm-package-arg": "13.0.2", + "semver": "^7.6.3", "xdg-basedir": "5.1.0", "zod": "catalog:" }, diff --git a/packages/opencode/src/effect/cross-spawn-spawner.ts b/packages/core/src/cross-spawn-spawner.ts similarity index 97% rename from packages/opencode/src/effect/cross-spawn-spawner.ts rename to packages/core/src/cross-spawn-spawner.ts index 5e25263a08bb..ad8d4126d454 100644 --- a/packages/opencode/src/effect/cross-spawn-spawner.ts +++ b/packages/core/src/cross-spawn-spawner.ts @@ -502,13 +502,4 @@ export const layer: Layer.Layer { - // Dynamic import to avoid circular dep: cross-spawn-spawner → run-service → Instance → project → cross-spawn-spawner - const { makeRuntime } = await import("@/effect/run-service") - return makeRuntime(ChildProcessSpawner, defaultLayer) -}) - -type RT = Awaited> -export const runPromiseExit: RT["runPromiseExit"] = async (...args) => (await rt()).runPromiseExit(...(args as [any])) +export * as CrossSpawnSpawner from "./cross-spawn-spawner" diff --git a/packages/opencode/src/effect/logger.ts b/packages/core/src/effect/logger.ts similarity index 98% rename from packages/opencode/src/effect/logger.ts rename to packages/core/src/effect/logger.ts index 0e58b8acb4b5..69f9631e06bf 100644 --- a/packages/opencode/src/effect/logger.ts +++ b/packages/core/src/effect/logger.ts @@ -1,5 +1,5 @@ import { Cause, Effect, Logger, References } from "effect" -import { Log } from "@/util" +import * as Log from "../util/log" type Fields = Record diff --git a/packages/opencode/src/effect/memo-map.ts b/packages/core/src/effect/memo-map.ts similarity index 100% rename from packages/opencode/src/effect/memo-map.ts rename to packages/core/src/effect/memo-map.ts diff --git a/packages/opencode/src/effect/observability.ts b/packages/core/src/effect/observability.ts similarity index 92% rename from packages/opencode/src/effect/observability.ts rename to packages/core/src/effect/observability.ts index fb81d5f5b58e..0203079abe1e 100644 --- a/packages/opencode/src/effect/observability.ts +++ b/packages/core/src/effect/observability.ts @@ -2,9 +2,9 @@ import { Effect, Layer, Logger } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { OtlpLogger, OtlpSerialization } from "effect/unstable/observability" import * as EffectLogger from "./logger" -import { Flag } from "@/flag/flag" -import { InstallationChannel, InstallationVersion } from "@/installation/version" -import { ensureProcessMetadata } from "@/util/opencode-process" +import { Flag } from "../flag/flag" +import { InstallationChannel, InstallationVersion } from "../installation/version" +import { ensureProcessMetadata } from "../util/opencode-process" const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT export const enabled = !!base @@ -76,7 +76,7 @@ const traces = async () => { // register(), so the global @opentelemetry/api context manager stays // as the no-op default. Non-Effect code (like the AI SDK) that calls // tracer.startActiveSpan() relies on context.active() to find the - // parent span — without a real context manager every span starts a + // parent span - without a real context manager every span starts a // new trace. Registering AsyncLocalStorageContextManager fixes this. const { AsyncLocalStorageContextManager } = await import("@opentelemetry/context-async-hooks") const { context } = await import("@opentelemetry/api") diff --git a/packages/opencode/src/effect/runtime.ts b/packages/core/src/effect/runtime.ts similarity index 94% rename from packages/opencode/src/effect/runtime.ts rename to packages/core/src/effect/runtime.ts index ad7872f0b56e..e4f682709897 100644 --- a/packages/opencode/src/effect/runtime.ts +++ b/packages/core/src/effect/runtime.ts @@ -1,11 +1,13 @@ -import { Observability } from "./observability" import { Layer, type Context, ManagedRuntime, type Effect } from "effect" import { memoMap } from "./memo-map" +import { Observability } from "./observability" export function makeRuntime(service: Context.Service, layer: Layer.Layer) { let rt: ManagedRuntime.ManagedRuntime | undefined const getRuntime = () => - (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { memoMap })) + (rt ??= ManagedRuntime.make(Layer.provideMerge(layer, Observability.layer) as Layer.Layer, { + memoMap, + })) return { runSync: (fn: (svc: S) => Effect.Effect) => getRuntime().runSync(service.use(fn)), diff --git a/packages/shared/src/filesystem.ts b/packages/core/src/filesystem.ts similarity index 100% rename from packages/shared/src/filesystem.ts rename to packages/core/src/filesystem.ts diff --git a/packages/opencode/src/flag/flag.ts b/packages/core/src/flag/flag.ts similarity index 100% rename from packages/opencode/src/flag/flag.ts rename to packages/core/src/flag/flag.ts diff --git a/packages/core/src/global.ts b/packages/core/src/global.ts new file mode 100644 index 000000000000..0c83e3a1fa93 --- /dev/null +++ b/packages/core/src/global.ts @@ -0,0 +1,65 @@ +import path from "path" +import fs from "fs/promises" +import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" +import os from "os" +import { Context, Effect, Layer } from "effect" +import { Flock } from "./util/flock" + +const app = "opencode" +const data = path.join(xdgData!, app) +const cache = path.join(xdgCache!, app) +const config = path.join(xdgConfig!, app) +const state = path.join(xdgState!, app) + +const paths = { + get home() { + return process.env.OPENCODE_TEST_HOME ?? os.homedir() + }, + data, + bin: path.join(cache, "bin"), + log: path.join(data, "log"), + cache, + config, + state, +} + +export const Path = paths + +Flock.setGlobal({ state }) + +await Promise.all([ + fs.mkdir(Path.data, { recursive: true }), + fs.mkdir(Path.config, { recursive: true }), + fs.mkdir(Path.state, { recursive: true }), + fs.mkdir(Path.log, { recursive: true }), + fs.mkdir(Path.bin, { recursive: true }), +]) + +export class Service extends Context.Service()("@opencode/Global") {} + +export interface Interface { + readonly home: string + readonly data: string + readonly cache: string + readonly config: string + readonly state: string + readonly bin: string + readonly log: string +} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + return Service.of({ + home: Path.home, + data: Path.data, + cache: Path.cache, + config: Path.config, + state: Path.state, + bin: Path.bin, + log: Path.log, + }) + }), +) + +export * as Global from "./global" diff --git a/packages/opencode/src/installation/version.ts b/packages/core/src/installation/version.ts similarity index 100% rename from packages/opencode/src/installation/version.ts rename to packages/core/src/installation/version.ts diff --git a/packages/opencode/src/npm/index.ts b/packages/core/src/npm.ts similarity index 95% rename from packages/opencode/src/npm/index.ts rename to packages/core/src/npm.ts index 4b1f80707091..a52e0a9a51ff 100644 --- a/packages/opencode/src/npm/index.ts +++ b/packages/core/src/npm.ts @@ -1,20 +1,22 @@ -export * as Npm from "." +export * as Npm from "./npm" import path from "path" import { fileURLToPath } from "url" import npa from "npm-package-arg" import semver from "semver" +// @ts-expect-error npm does not publish types for this internal config API. import Config from "@npmcli/config" +// @ts-expect-error npm does not publish types for this internal config API. import { definitions, flatten, nerfDarts, shorthands } from "@npmcli/config/lib/definitions/index.js" import { Effect, Schema, Context, Layer, Option, FileSystem, Stream } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Global } from "@opencode-ai/shared/global" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { AppFileSystem } from "./filesystem" +import { Global } from "./global" +import { EffectFlock } from "./util/effect-flock" +import { makeRuntime } from "./effect/runtime" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "../effect/cross-spawn-spawner" -import { makeRuntime } from "../effect/runtime" +import { CrossSpawnSpawner } from "./cross-spawn-spawner" export class InstallFailedError extends Schema.TaggedErrorClass()("NpmInstallFailedError", { add: Schema.Array(Schema.String).pipe(Schema.optional), @@ -45,7 +47,7 @@ export interface Interface { export class Service extends Context.Service()("@opencode/Npm") {} const illegal = process.platform === "win32" ? new Set(["<", ">", ":", '"', "|", "?", "*"]) : undefined -const npmPath = fileURLToPath(new URL("../..", import.meta.url)) +const npmPath = fileURLToPath(new URL("..", import.meta.url)) export function sanitize(pkg: string) { if (!illegal) return pkg diff --git a/packages/shared/src/util/array.ts b/packages/core/src/util/array.ts similarity index 100% rename from packages/shared/src/util/array.ts rename to packages/core/src/util/array.ts diff --git a/packages/shared/src/util/binary.ts b/packages/core/src/util/binary.ts similarity index 100% rename from packages/shared/src/util/binary.ts rename to packages/core/src/util/binary.ts diff --git a/packages/shared/src/util/effect-flock.ts b/packages/core/src/util/effect-flock.ts similarity index 100% rename from packages/shared/src/util/effect-flock.ts rename to packages/core/src/util/effect-flock.ts diff --git a/packages/shared/src/util/encode.ts b/packages/core/src/util/encode.ts similarity index 100% rename from packages/shared/src/util/encode.ts rename to packages/core/src/util/encode.ts diff --git a/packages/shared/src/util/error.ts b/packages/core/src/util/error.ts similarity index 100% rename from packages/shared/src/util/error.ts rename to packages/core/src/util/error.ts diff --git a/packages/shared/src/util/flock.ts b/packages/core/src/util/flock.ts similarity index 100% rename from packages/shared/src/util/flock.ts rename to packages/core/src/util/flock.ts diff --git a/packages/shared/src/util/fn.ts b/packages/core/src/util/fn.ts similarity index 100% rename from packages/shared/src/util/fn.ts rename to packages/core/src/util/fn.ts diff --git a/packages/shared/src/util/glob.ts b/packages/core/src/util/glob.ts similarity index 100% rename from packages/shared/src/util/glob.ts rename to packages/core/src/util/glob.ts diff --git a/packages/shared/src/util/hash.ts b/packages/core/src/util/hash.ts similarity index 100% rename from packages/shared/src/util/hash.ts rename to packages/core/src/util/hash.ts diff --git a/packages/shared/src/util/identifier.ts b/packages/core/src/util/identifier.ts similarity index 100% rename from packages/shared/src/util/identifier.ts rename to packages/core/src/util/identifier.ts diff --git a/packages/shared/src/util/iife.ts b/packages/core/src/util/iife.ts similarity index 100% rename from packages/shared/src/util/iife.ts rename to packages/core/src/util/iife.ts diff --git a/packages/shared/src/util/lazy.ts b/packages/core/src/util/lazy.ts similarity index 100% rename from packages/shared/src/util/lazy.ts rename to packages/core/src/util/lazy.ts diff --git a/packages/opencode/src/util/log.ts b/packages/core/src/util/log.ts similarity index 98% rename from packages/opencode/src/util/log.ts rename to packages/core/src/util/log.ts index 7c1581bfc0a2..a61c15f7a7a4 100644 --- a/packages/opencode/src/util/log.ts +++ b/packages/core/src/util/log.ts @@ -1,9 +1,9 @@ import path from "path" import fs from "fs/promises" import { createWriteStream } from "fs" -import { Global } from "../global" +import * as Global from "../global" import z from "zod" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "./glob" export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" }) export type Level = z.infer diff --git a/packages/shared/src/util/module.ts b/packages/core/src/util/module.ts similarity index 100% rename from packages/shared/src/util/module.ts rename to packages/core/src/util/module.ts diff --git a/packages/opencode/src/util/opencode-process.ts b/packages/core/src/util/opencode-process.ts similarity index 100% rename from packages/opencode/src/util/opencode-process.ts rename to packages/core/src/util/opencode-process.ts diff --git a/packages/shared/src/util/path.ts b/packages/core/src/util/path.ts similarity index 100% rename from packages/shared/src/util/path.ts rename to packages/core/src/util/path.ts diff --git a/packages/shared/src/util/retry.ts b/packages/core/src/util/retry.ts similarity index 100% rename from packages/shared/src/util/retry.ts rename to packages/core/src/util/retry.ts diff --git a/packages/shared/src/util/slug.ts b/packages/core/src/util/slug.ts similarity index 100% rename from packages/shared/src/util/slug.ts rename to packages/core/src/util/slug.ts diff --git a/packages/shared/sst-env.d.ts b/packages/core/sst-env.d.ts similarity index 100% rename from packages/shared/sst-env.d.ts rename to packages/core/sst-env.d.ts diff --git a/packages/opencode/test/effect/cross-spawn-spawner.test.ts b/packages/core/test/effect/cross-spawn-spawner.test.ts similarity index 96% rename from packages/opencode/test/effect/cross-spawn-spawner.test.ts rename to packages/core/test/effect/cross-spawn-spawner.test.ts index b4e52529c1de..2612b75e464c 100644 --- a/packages/opencode/test/effect/cross-spawn-spawner.test.ts +++ b/packages/core/test/effect/cross-spawn-spawner.test.ts @@ -1,11 +1,11 @@ import { describe, expect } from "bun:test" import fs from "node:fs/promises" +import os from "node:os" import path from "node:path" import { Effect, Exit, Stream } from "effect" import type * as PlatformError from "effect/PlatformError" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { tmpdir } from "../fixture/fixture" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" const live = CrossSpawnSpawner.defaultLayer @@ -39,11 +39,21 @@ function alive(pid: number) { } } +async function tmpdir() { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-test-")) + return { + path: dir, + async [Symbol.asyncDispose]() { + await fs.rm(dir, { recursive: true, force: true }) + }, + } +} + async function gone(pid: number, timeout = 5_000) { const end = Date.now() + timeout while (Date.now() < end) { if (!alive(pid)) return true - await Bun.sleep(50) + await new Promise((resolve) => setTimeout(resolve, 50)) } return !alive(pid) } @@ -395,7 +405,7 @@ describe("cross-spawn spawner", () => { const file = path.join(dir, "echo cmd.cmd") yield* Effect.promise(() => fs.mkdir(dir, { recursive: true })) - yield* Effect.promise(() => Bun.write(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")) + yield* Effect.promise(() => fs.writeFile(file, "@echo off\r\nif %~1==--stdio exit /b 0\r\nexit /b 7\r\n")) const code = yield* ChildProcessSpawner.ChildProcessSpawner.use((svc) => svc.exitCode( diff --git a/packages/opencode/test/effect/observability.test.ts b/packages/core/test/effect/observability.test.ts similarity index 96% rename from packages/opencode/test/effect/observability.test.ts rename to packages/core/test/effect/observability.test.ts index d06220282742..50ea23f89463 100644 --- a/packages/opencode/test/effect/observability.test.ts +++ b/packages/core/test/effect/observability.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test" -import { resource } from "../../src/effect/observability" +import { resource } from "@opencode-ai/core/effect/observability" const otelResourceAttributes = process.env.OTEL_RESOURCE_ATTRIBUTES const opencodeClient = process.env.OPENCODE_CLIENT diff --git a/packages/shared/test/filesystem/filesystem.test.ts b/packages/core/test/filesystem/filesystem.test.ts similarity index 99% rename from packages/shared/test/filesystem/filesystem.test.ts rename to packages/core/test/filesystem/filesystem.test.ts index b49026bcba9d..b77f4e356fa0 100644 --- a/packages/shared/test/filesystem/filesystem.test.ts +++ b/packages/core/test/filesystem/filesystem.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { Effect, Layer, FileSystem } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/shared/test/fixture/effect-flock-worker.ts b/packages/core/test/fixture/effect-flock-worker.ts similarity index 88% rename from packages/shared/test/fixture/effect-flock-worker.ts rename to packages/core/test/fixture/effect-flock-worker.ts index c9116c2d5c1f..3dc3ee2c8b6d 100644 --- a/packages/shared/test/fixture/effect-flock-worker.ts +++ b/packages/core/test/fixture/effect-flock-worker.ts @@ -1,9 +1,9 @@ import fs from "fs/promises" import os from "os" import { Effect, Layer } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" -import { Global } from "@opencode-ai/shared/global" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { Global } from "@opencode-ai/core/global" type Msg = { key: string diff --git a/packages/shared/test/fixture/flock-worker.ts b/packages/core/test/fixture/flock-worker.ts similarity index 96% rename from packages/shared/test/fixture/flock-worker.ts rename to packages/core/test/fixture/flock-worker.ts index 9954d290cce9..0b9c314c0877 100644 --- a/packages/shared/test/fixture/flock-worker.ts +++ b/packages/core/test/fixture/flock-worker.ts @@ -1,5 +1,5 @@ import fs from "fs/promises" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" type Msg = { key: string diff --git a/packages/shared/test/lib/effect.ts b/packages/core/test/lib/effect.ts similarity index 100% rename from packages/shared/test/lib/effect.ts rename to packages/core/test/lib/effect.ts diff --git a/packages/shared/test/util/effect-flock.test.ts b/packages/core/test/util/effect-flock.test.ts similarity index 98% rename from packages/shared/test/util/effect-flock.test.ts rename to packages/core/test/util/effect-flock.test.ts index bd71e4f02294..9e8bc24ace2a 100644 --- a/packages/shared/test/util/effect-flock.test.ts +++ b/packages/core/test/util/effect-flock.test.ts @@ -5,10 +5,10 @@ import path from "path" import os from "os" import { Cause, Effect, Exit, Layer } from "effect" import { testEffect } from "../lib/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" -import { Global } from "@opencode-ai/shared/global" -import { Hash } from "@opencode-ai/shared/util/hash" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" +import { Global } from "@opencode-ai/core/global" +import { Hash } from "@opencode-ai/core/util/hash" function lock(dir: string, key: string) { return path.join(dir, Hash.fast(key) + ".lock") diff --git a/packages/shared/test/util/flock.test.ts b/packages/core/test/util/flock.test.ts similarity index 98% rename from packages/shared/test/util/flock.test.ts rename to packages/core/test/util/flock.test.ts index f1053dfd2b0d..e1b647b64801 100644 --- a/packages/shared/test/util/flock.test.ts +++ b/packages/core/test/util/flock.test.ts @@ -3,8 +3,8 @@ import fs from "fs/promises" import { spawn } from "child_process" import path from "path" import os from "os" -import { Flock } from "@opencode-ai/shared/util/flock" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Flock } from "@opencode-ai/core/util/flock" +import { Hash } from "@opencode-ai/core/util/hash" type Msg = { key: string diff --git a/packages/shared/tsconfig.json b/packages/core/tsconfig.json similarity index 100% rename from packages/shared/tsconfig.json rename to packages/core/tsconfig.json diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index a528a9a3ac36..f382ad16dddf 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "1.14.22", + "version": "1.14.25", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 32b63fff13ac..242bd197185a 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.14.22", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index e4428f0cb269..a5a2997b9558 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.14.22", + "version": "1.14.25", "private": true, "type": "module", "license": "MIT", @@ -13,7 +13,7 @@ "shell-prod": "sst shell --target Teams --stage production" }, "dependencies": { - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@opencode-ai/ui": "workspace:*", "aws4fetch": "^1.0.20", "@pierre/diffs": "catalog:", diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index 1a343272f74d..fb8cd302951d 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -1,6 +1,6 @@ import { Message, Model, Part, Session, SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import { fn } from "@opencode-ai/shared/util/fn" -import { iife } from "@opencode-ai/shared/util/iife" +import { fn } from "@opencode-ai/core/util/fn" +import { iife } from "@opencode-ai/core/util/iife" import z from "zod" import { Storage } from "./storage" diff --git a/packages/enterprise/src/core/storage.ts b/packages/enterprise/src/core/storage.ts index a6222e41545f..58d61aca7893 100644 --- a/packages/enterprise/src/core/storage.ts +++ b/packages/enterprise/src/core/storage.ts @@ -1,5 +1,5 @@ import { AwsClient } from "aws4fetch" -import { lazy } from "@opencode-ai/shared/util/lazy" +import { lazy } from "@opencode-ai/core/util/lazy" export namespace Storage { export interface Adapter { diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index f3be14e3934e..b12afce27aef 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -10,9 +10,9 @@ import { Share } from "~/core/share" import { Logo, Mark } from "@opencode-ai/ui/logo" import { IconButton } from "@opencode-ai/ui/icon-button" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { iife } from "@opencode-ai/shared/util/iife" -import { Binary } from "@opencode-ai/shared/util/binary" -import { NamedError } from "@opencode-ai/shared/util/error" +import { iife } from "@opencode-ai/core/util/iife" +import { Binary } from "@opencode-ai/core/util/binary" +import { NamedError } from "@opencode-ai/core/util/error" import { DateTime } from "luxon" import { createStore } from "solid-js/store" import z from "zod" diff --git a/packages/enterprise/test/core/share.test.ts b/packages/enterprise/test/core/share.test.ts index 2877f8e0e0fc..15c5f9205ab7 100644 --- a/packages/enterprise/test/core/share.test.ts +++ b/packages/enterprise/test/core/share.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import { Share } from "../../src/core/share" import { Storage } from "../../src/core/storage" -import { Identifier } from "@opencode-ai/shared/util/identifier" +import { Identifier } from "@opencode-ai/core/util/identifier" describe.concurrent("core.share", () => { test("should create a share", async () => { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 6449b8df65da..6deaee220151 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.14.22" +version = "1.14.25" 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.14.22/opencode-darwin-arm64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-darwin-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.22/opencode-linux-arm64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/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.14.22/opencode-linux-x64.tar.gz" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/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.14.22/opencode-windows-x64.zip" +archive = "https://github.com/anomalyco/opencode/releases/download/v1.14.25/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/function/package.json b/packages/function/package.json index 5b06d03685eb..6c1428ad5af2 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.14.22", + "version": "1.14.25", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index ea8724ceb82e..98a707f4b9aa 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.14.22", + "version": "1.14.25", "name": "opencode", "type": "module", "license": "MIT", @@ -45,7 +45,7 @@ "@effect/language-service": "0.84.2", "@octokit/webhooks-types": "7.6.1", "@opencode-ai/script": "workspace:*", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", @@ -61,7 +61,6 @@ "@types/cross-spawn": "catalog:", "@types/mime-types": "3.0.1", "@types/npm-package-arg": "6.1.4", - "@types/npmcli__arborist": "6.3.3", "@types/semver": "^7.5.8", "@types/turndown": "5.0.5", "@types/which": "3.0.4", @@ -69,6 +68,7 @@ "@typescript/native-preview": "catalog:", "drizzle-kit": "catalog:", "drizzle-orm": "catalog:", + "prettier": "3.6.2", "typescript": "catalog:", "vscode-languageserver-types": "3.17.5", "why-is-node-running": "3.2.2", @@ -109,8 +109,6 @@ "@hono/zod-validator": "catalog:", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.27.1", - "@npmcli/arborist": "9.4.0", - "@npmcli/config": "10.8.1", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", "@openauthjs/openauth": "catalog:", @@ -123,8 +121,8 @@ "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", @@ -159,7 +157,7 @@ "open": "10.1.2", "opencode-gitlab-auth": "2.0.1", "opencode-poe-auth": "0.0.1", - "opentui-spinner": "0.0.6", + "opentui-spinner": "catalog:", "partial-json": "0.1.7", "remeda": "catalog:", "semver": "^7.6.3", diff --git a/packages/opencode/specs/effect/http-api.md b/packages/opencode/specs/effect/http-api.md index 6c80dc65a250..1187fef74240 100644 --- a/packages/opencode/specs/effect/http-api.md +++ b/packages/opencode/specs/effect/http-api.md @@ -1,459 +1,211 @@ # HttpApi migration -Practical notes for an eventual migration of `packages/opencode` server routes from the current Hono handlers to Effect `HttpApi`, either as a full replacement or as a parallel surface. +Plan for replacing instance Hono route implementations with Effect `HttpApi` while preserving behavior, OpenAPI, and SDK output during the transition. -## Goal +## End State -Use Effect `HttpApi` where it gives us a better typed contract for: +- JSON route contracts and handlers live in `src/server/routes/instance/httpapi/*`. +- Route modules own their `HttpApiGroup`, schemas, handlers, and route-level middleware. +- `httpapi/server.ts` only composes groups, instance lookup, observability, and the web handler bridge. +- Hono route implementations are deleted once their `HttpApi` replacements are default, tested, and represented in the SDK/OpenAPI pipeline. +- Streaming, SSE, and websocket routes move later through Effect HTTP primitives or another explicit replacement plan; they do not need to fit `HttpApi` if `HttpApi` is the wrong abstraction. -- route definition -- request decoding and validation -- typed success and error responses -- OpenAPI generation -- handler composition inside Effect +## Current State -This should be treated as a later-stage HTTP boundary migration, not a prerequisite for ongoing service, route-handler, or schema work. +- `OPENCODE_EXPERIMENTAL_HTTPAPI` gates the bridge. Default behavior still uses Hono. +- The bridge mounts selected paths in `server/routes/instance/index.ts` before legacy Hono routes. +- Legacy Hono routes remain for default behavior and for `hono-openapi` SDK generation. +- `HttpApi` auth is independent of Hono auth. +- `Authorization` is attached in each route module, not centrally wrapped in `server.ts`. +- Auth supports Basic auth and the legacy `auth_token` query parameter through `HttpApiSecurity.apiKey`. +- Instance context is provided by `httpapi/server.ts` using `directory`, `workspace`, and `x-opencode-directory`. +- `Observability.layer` is provided in the Effect route layer and deduplicated through the shared `memoMap`. -## Core model +## Migration Rules -`HttpApi` is definition-first. +- Preserve runtime behavior first. Semantic changes, new error behavior, or route shape changes need separate PRs. +- Migrate one route group, or one coherent subset of a route group, at a time. +- Reuse existing services. Do not re-architect service logic during HTTP boundary migration. +- Effect Schema owns route DTOs. Keep `.zod` only as compatibility for remaining Hono/OpenAPI surfaces. +- Regenerate the SDK after schema or OpenAPI-affecting changes and verify the diff is expected. +- Do not delete a Hono route until the SDK/OpenAPI pipeline no longer depends on its Hono `describeRoute` entry. -- `HttpApi` is the root API -- `HttpApiGroup` groups related endpoints -- `HttpApiEndpoint` defines a single route and its request / response schemas -- handlers are implemented separately from the contract +## Route Slice Checklist -This is a better fit once route inputs and outputs are already moving toward Effect Schema-first models. +Use this checklist for each small HttpApi migration PR: -## Why it is relevant here +1. Read the legacy Hono route and copy behavior exactly, including default values, headers, operation IDs, response schemas, and status codes. +2. Put the new `HttpApiGroup`, route paths, DTO schemas, and handlers in `src/server/routes/instance/httpapi/*`. +3. Mount the new paths in `src/server/routes/instance/index.ts` only inside the `OPENCODE_EXPERIMENTAL_HTTPAPI` block. +4. Use `InstanceState.context` / `InstanceState.directory` inside HttpApi handlers instead of `Instance.directory`, `Instance.worktree`, or `Instance.project` ALS globals. +5. Reuse existing services directly. If a service returns plain objects, use `Schema.Struct`; use `Schema.Class` only when handlers return actual class instances. +6. Keep legacy Hono routes and `.zod` compatibility in place for SDK/OpenAPI generation. +7. Add tests that hit the Hono-mounted bridge via `InstanceRoutes`, not only the raw `HttpApi` web handler, when the route depends on auth or instance context. +8. Run `bun typecheck` from `packages/opencode`, relevant `bun run test:ci ...` tests from `packages/opencode`, and `./packages/sdk/js/script/build.ts` from the repo root. -The current route-effectification work is already pushing handlers toward: +## Hono Deletion Checklist -- one `AppRuntime.runPromise(Effect.gen(...))` body -- yielding services from context -- using typed Effect errors instead of Promise wrappers +Use this checklist before deleting any Hono route implementation. A route being `bridged` is not enough. -That work is a good prerequisite for `HttpApi`. Once the handler body is already a composed Effect, the remaining migration is mostly about replacing the Hono route declaration and validator layer. +1. `HttpApi` parity is complete for the route path, method, auth behavior, query parameters, request body, response status, response headers, and error status. +2. The route is mounted by default, not only behind `OPENCODE_EXPERIMENTAL_HTTPAPI`. +3. If a fallback flag exists, tests cover both the default `HttpApi` path and the fallback Hono path until the fallback is removed. +4. OpenAPI generation uses the Effect `HttpApi` route as the source for that path. +5. Generated SDK output is unchanged from the Hono-generated contract, or the SDK diff is intentionally reviewed and accepted. +6. The legacy Hono `describeRoute`, validator, and handler for that path are removed. +7. Any duplicate Zod-only DTOs are deleted or kept only as `.zod` compatibility on the canonical Effect Schema. +8. Bridge tests exist for auth, instance selection, success response, and route-specific side effects. +9. Mutation routes prove persisted side effects and cleanup behavior in tests. If the mutation disposes/reloads the active instance, disposal happens through an explicit post-response lifecycle hook rather than inline handler teardown. +10. Streaming, SSE, websocket, and UI bridge routes have a specific non-Hono replacement plan. Do not force them through `HttpApi` if raw Effect HTTP is a better fit. -## What HttpApi gives us +Hono can be removed from the instance server only after all mounted Hono route groups meet this checklist and `server/routes/instance/index.ts` no longer depends on Hono routing for default behavior. -### Contracts +## Experimental Read Slice Guidance -Request params, query, payload, success payloads, and typed error payloads are declared in one place using Effect Schema. +For the experimental route group, port read-only JSON routes before mutations: -### Validation and decoding +- Good first batch: `GET /console`, `GET /console/orgs`, `GET /tool/ids`, `GET /resource`. +- Consider `GET /worktree` only if the handler uses `InstanceState.context` instead of `Instance.project`. +- Defer `POST /console/switch`, worktree create/remove/reset, and `GET /session` to separate PRs because they mutate state or have broader pagination/session behavior. +- Preserve response headers such as pagination cursors if a route is ported. +- If SDK generation changes, explain whether it is a semantic contract change or a generator-equivalent type normalization. -Incoming data is decoded through Effect Schema instead of hand-maintained Zod validators per route. +## Schema Notes -### OpenAPI +- Use `Schema.Struct(...).annotate({ identifier })` for named OpenAPI refs when handlers return plain objects. +- Use `Schema.Class` only when the handler returns real class instances or the constructor requirement is intentional. +- Keep nested anonymous shapes as `Schema.Struct` unless a named SDK type is useful. +- Avoid parallel hand-written Zod and Effect definitions for the same route boundary. -`HttpApi` can derive OpenAPI from the API definition, which overlaps with the current `describeRoute(...)` and `resolver(...)` pattern. +## Phases -### Typed errors +### 1. Stabilize The Bridge -`Schema.TaggedErrorClass` maps naturally to endpoint error contracts. +Before porting more routes, cover the bridge behavior that every route depends on. -## Likely fit for opencode +- Add tests that hit the Hono-mounted `HttpApi` bridge, not just `HttpApiBuilder.layer` directly. +- Cover auth disabled, Basic auth success, `auth_token` success, missing credentials, and bad credentials. +- Cover `directory` and `x-opencode-directory` instance selection. +- Verify generated SDK output remains unchanged for non-SDK work. +- Fix or remove any implemented-but-unmounted `HttpApi` groups. -Best fit first: +### 2. Complete The Inventory -- JSON request / response endpoints -- route groups that already mostly delegate into services -- endpoints whose request and response models can be defined with Effect Schema +Create a route inventory from the actual Hono registrations and classify each route. -Harder / later fit: +Statuses: -- SSE endpoints -- websocket endpoints -- streaming handlers -- routes with heavy Hono-specific middleware assumptions +- `bridged`: served through the `HttpApi` bridge when the flag is on. +- `implemented`: `HttpApi` group exists but is not mounted through Hono. +- `next`: good JSON candidate for near-term porting. +- `later`: portable, but needs schema/service cleanup first. +- `special`: SSE, websocket, streaming, or UI bridge behavior that likely needs raw Effect HTTP rather than `HttpApi`. -## Current blockers and gaps +### 3. Finish JSON Route Parity -### Schema split +Port remaining JSON routes in small batches. -Many route boundaries still use Zod-first validators. That does not block all experimentation, but full `HttpApi` adoption is easier after the domain and boundary types are more consistently Schema-first with `.zod` compatibility only where needed. +Good near-term candidates: -### Mixed handler styles +- top-level reads: `GET /path`, `GET /vcs`, `GET /vcs/diff`, `GET /command`, `GET /agent`, `GET /skill`, `GET /lsp`, `GET /formatter` +- simple mutations: `POST /instance/dispose` +- experimental JSON reads: console, tool, worktree list, resource list +- deferred JSON mutations: `PATCH /config`, project git init, workspace/worktree create/remove/reset, file search, MCP auth flows -Many current `server/routes/instance/*.ts` handlers still mix composed Effect code with smaller Promise- or ALS-backed seams. Migrating those to consistent `Effect.gen(...)` handlers is the low-risk step to do first. +Keep large or stateful groups for later: -### Non-JSON routes +- `session` +- `sync` +- process-level experimental routes -The server currently includes SSE, websocket, and streaming-style endpoints. Those should not be the first `HttpApi` targets. +### 4. Move OpenAPI And SDK Generation -### Existing Hono integration +Hono routes cannot be deleted while `hono-openapi` is the source of SDK generation. -The current server composition, middleware, and docs flow are Hono-centered today. That suggests a parallel or incremental adoption plan is safer than a flag day rewrite. +Required before route deletion: -## Recommended strategy +- Generate the public OpenAPI surface from Effect `HttpApi` for ported routes. +- Keep operation IDs, schemas, status codes, and SDK type names stable unless the change is intentional. +- Compare generated SDK output against `dev` for every route group deletion. +- Remove Hono OpenAPI stubs only after Effect OpenAPI is the SDK source for those paths. -### 1. Finish the prerequisites first +### 5. Make HttpApi Default For JSON Routes -- continue route-handler effectification in `server/routes/instance/*.ts` -- continue schema migration toward Effect Schema-first DTOs and errors -- keep removing service facades +After JSON parity and SDK generation are covered: -### 2. Start with one parallel group +- Flip the bridge default for ported JSON routes. +- Keep a short-lived fallback flag for the old Hono implementation. +- Run the same tests against both the default and fallback path during rollout. +- Stop adding new Hono handlers for JSON routes once the default flips. -Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in: +### 6. Delete Hono Route Implementations -- `server/routes/instance/question.ts` -- `server/routes/instance/provider.ts` -- `server/routes/instance/permission.ts` +Delete Hono routes group-by-group after each group meets the deletion criteria. -Avoid `session.ts`, SSE, websocket, and TUI-facing routes first. +Deletion criteria: -Recommended first slice: +- `HttpApi` route is mounted by default. +- Behavior is covered by bridge-level tests. +- OpenAPI/SDK generation comes from Effect for that path. +- SDK diff is zero or explicitly accepted. +- Legacy Hono route is no longer needed as a fallback. -- start with `question` -- start with `GET /question` -- start with `POST /question/:requestID/reply` +After deleting a group: -Why `question` first: +- Remove its Hono route file or dead endpoints. +- Remove its `.route(...)` registration from `instance/index.ts`. +- Remove duplicate Zod-only route DTOs if Effect Schema now owns the type. +- Regenerate SDK and verify output. -- already JSON-only -- already delegates into an Effect service -- proves list + mutation + params + payload + OpenAPI in one small slice -- avoids the harder streaming and middleware cases +### 7. Replace Special Routes -### 3. Reuse existing services +Special routes need explicit designs before Hono can disappear completely. -Do not re-architect business logic during the HTTP migration. `HttpApi` handlers should call the same Effect services already used by the Hono handlers. +- `event`: SSE +- `pty`: websocket +- `tui`: UI/control bridge behavior +- streaming `session` endpoints -### 4. Bridge into Hono behind a feature flag +Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Hono implementations, not forcing every transport shape through `HttpApi`. -The `HttpApi` routes are bridged into the Hono server via `HttpRouter.toWebHandler` with a shared `memoMap`. This means: +## Current Route Status -- one process, one port — no separate server -- the Effect handler shares layer instances with `AppRuntime` (same `Question.Service`, etc.) -- Effect middleware handles auth and instance lookup independently from Hono middleware -- Hono's `.all()` catch-all intercepts matching paths before the Hono route handlers +| Area | Status | Notes | +| ------------------------- | ----------------- | -------------------------------------------------------------------------------------------------- | +| `question` | `bridged` | `GET /question`, reply, reject | +| `permission` | `bridged` | list and reply | +| `provider` | `bridged` | list, auth, OAuth authorize/callback | +| `config` | `bridged` | read, providers, update | +| `project` | `bridged` partial | reads only; git-init remains Hono | +| `file` | `bridged` partial | find text/file/symbol, list/content/status | +| `mcp` | `bridged` partial | status only | +| `workspace` | `bridged` | list, get, enter | +| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | +| experimental JSON routes | `bridged` partial | console reads, tool ids, worktree list/mutations, resource list; global session list remains later | +| `session` | `later/special` | large stateful surface plus streaming | +| `sync` | `later` | process/control side effects | +| `event` | `special` | SSE | +| `pty` | `special` | websocket | +| `tui` | `special` | UI bridge | -The bridge is gated behind `OPENCODE_EXPERIMENTAL_HTTPAPI` (or `OPENCODE_EXPERIMENTAL`). When the flag is off (default), all requests go through the original Hono handlers unchanged. +## Next PRs -```ts -// in instance/index.ts -if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { - const handler = ExperimentalHttpApiServer.webHandler().handler - app.all("/question", (c) => handler(c.req.raw)).all("/question/*", (c) => handler(c.req.raw)) -} -``` - -The Hono route handlers are always registered (after the bridge) so `hono-openapi` generates the OpenAPI spec entries that feed SDK codegen. When the flag is on, these handlers are dead code — the `.all()` bridge matches first. - -### 5. Observability - -The `webHandler` provides `Observability.layer` via `Layer.provideMerge`. Since the `memoMap` is shared with `AppRuntime`, the tracing provider is deduplicated — no extra initialization cost. - -This gives: - -- **spans**: `Effect.fn("QuestionHttpApi.list")` etc. appear in traces alongside service-layer spans -- **HTTP logs**: `HttpMiddleware.logger` emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` annotations, flowing to motel via `OtlpLogger` - -### 6. Migrate JSON route groups gradually - -As each route group is ported to `HttpApi`: - -1. add `.get(...)` / `.post(...)` bridge entries to the flag block in `server/routes/instance/index.ts` -2. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path -3. keep the legacy Hono route registered behind it for OpenAPI / SDK generation until the spec pipeline changes -4. verify SDK output is unchanged - -Leave streaming-style endpoints on Hono until there is a clear reason to move them. - -## Schema rule for HttpApi work - -Every `HttpApi` slice should follow `specs/effect/schema.md` and the Schema -> Zod interop rule in `specs/effect/migration.md`. - -Default rule: - -- Effect Schema owns the type -- `.zod` exists only as a compatibility surface -- do not introduce a new hand-written Zod schema for a type that is already migrating to Effect Schema - -Practical implication for `HttpApi` migration: - -- if a route boundary already depends on a shared DTO, ID, input, output, or tagged error, migrate that model to Effect Schema first or in the same change -- if an existing Hono route or tool still needs Zod, derive it with `@/util/effect-zod` -- avoid maintaining parallel Zod and Effect definitions for the same request or response type - -Ordering for a route-group migration: - -1. move implicated shared `schema.ts` leaf types to Effect Schema first -2. move exported `Info` / `Input` / `Output` route DTOs to Effect Schema -3. move tagged route-facing errors to `Schema.TaggedErrorClass` where needed -4. switch existing Zod boundary validators to derived `.zod` -5. define the `HttpApi` contract from the canonical Effect schemas -6. regenerate the SDK (`./packages/sdk/js/script/build.ts`) and verify zero diff against `dev` - -SDK shape rule: - -- every schema migration must preserve the generated SDK output byte-for-byte **unless the new ref is intentional** (see Schema.Class vs Schema.Struct below) -- if an unintended diff appears in `packages/sdk/js/src/v2/gen/types.gen.ts`, the migration introduced an unintended API surface change — fix it before merging - -### Schema.Class vs Schema.Struct - -The pattern choice determines whether a schema becomes a **named** export in the SDK or stays **anonymous inline**. - -**Schema.Class** emits a named `$ref` in OpenAPI via its identifier → produces a named `export type Foo = ...` in `types.gen.ts`: - -```ts -export class Info extends Schema.Class("FooConfig")({ ... }) { - static readonly zod = zod(this) -} -``` - -**Schema.Struct** stays anonymous and is inlined everywhere it is referenced: - -```ts -export const Info = Schema.Struct({ ... }).pipe( - withStatics((s) => ({ zod: zod(s) })), -) -export type Info = Schema.Schema.Type -``` - -When to use each: - -- Use **Schema.Class** when: - - the original Zod had `.meta({ ref: ... })` (preserve the existing named SDK type byte-for-byte) - - the schema is a top-level endpoint request or response (SDK consumers benefit from a stable importable name) -- Use **Schema.Struct** when: - - the type is only used as a nested field inside another named schema - - the original Zod was anonymous and promoting it would bloat SDK types with no import value - -Promoting a previously-anonymous schema to Schema.Class is acceptable when it is top-level or endpoint-facing, but call it out in the PR — it is an additive SDK change (`export type Foo = ...` newly appears) even if it preserves the JSON shape. - -Schemas that are **not** pure objects (enums, unions, records, tuples) cannot use Schema.Class. For those — and for pure-object schemas where handlers populate plain objects rather than class instances — add `.annotate({ identifier: "FooName" })` to get the same named-ref behavior without the `instanceof` requirement: - -```ts -export const Action = Schema.Literals(["ask", "allow", "deny"]).annotate({ identifier: "PermissionActionConfig" }) -``` - -Temporary exception: - -- it is acceptable to keep a route-local Zod schema for the first spike only when the type is boundary-local and migrating it would create unrelated churn -- if that happens, leave a short note so the type does not become a permanent second source of truth - -## First vertical slice - -The first `HttpApi` spike should be intentionally small and repeatable. - -Chosen slice: - -- group: `question` -- endpoints: `GET /question` and `POST /question/:requestID/reply` - -Non-goals: - -- no `session` routes -- no SSE or websocket routes -- no auth redesign -- no broad service refactor - -Behavior rule: - -- preserve current runtime behavior first -- treat semantic changes such as introducing new `404` behavior as a separate follow-up unless they are required to make the contract honest - -Add `POST /question/:requestID/reject` only after the first two endpoints work cleanly. - -## Repeatable slice template - -Use the same sequence for each route group. - -1. Pick one JSON-only route group that already mostly delegates into services. -2. Identify the shared DTOs, IDs, and errors implicated by that slice. -3. Apply the schema migration ordering above so those types are Effect Schema-first. -4. Define the `HttpApi` contract separately from the handlers. -5. Implement handlers by yielding the existing service from context. -6. Mount the new surface in parallel behind the `OPENCODE_EXPERIMENTAL_HTTPAPI` bridge. -7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above). -8. Add one end-to-end test and one OpenAPI-focused test. -9. Compare ergonomics before migrating the next endpoint. - -Rule of thumb: - -- migrate one route group at a time -- migrate one or two endpoints first, not the whole file -- keep business logic in the existing service -- keep the first spike easy to delete if the experiment is not worth continuing - -## Example structure - -Placement rule: - -- keep `HttpApi` code under `src/server`, not `src/effect` -- `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing -- place each `HttpApi` slice next to the HTTP boundary it serves -- for instance-scoped routes, prefer `src/server/routes/instance/httpapi/*` -- if control-plane routes ever migrate, prefer `src/server/routes/control/httpapi/*` - -Suggested file layout for a repeatable spike: - -- `src/server/routes/instance/httpapi/question.ts` — contract and handler layer for one route group -- `src/server/routes/instance/httpapi/server.ts` — bridged Effect HTTP layer that composes all groups -- route or OpenAPI verification should live alongside the existing server tests; there is no dedicated `question-httpapi` test file on this branch - -Suggested responsibilities: - -- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers -- `server.ts` composes all route groups into one `HttpRouter.toWebHandler(...)` bridge with shared middleware (auth, instance lookup) -- tests should verify the bridged routes through the normal server surface - -## Example migration shape - -Each route-group spike should follow the same shape. - -### 1. Contract - -- define an experimental `HttpApi` -- define one `HttpApiGroup` -- define endpoint params, payload, success, and error schemas from canonical Effect schemas -- annotate summary, description, and operation ids explicitly so generated docs are stable - -### 2. Handler layer - -- implement with `HttpApiBuilder.group(api, groupName, ...)` -- yield the existing Effect service from context -- keep handler bodies thin -- keep transport mapping at the HTTP boundary only - -### 3. Bridged server - -- the Effect HTTP layer is composed in `httpapi/server.ts` -- it is mounted into the Hono app via `HttpRouter.toWebHandler(...)` -- routes keep their normal instance paths and are gated by the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag -- the legacy Hono handlers stay registered after the bridge so current OpenAPI / SDK generation still works - -### 4. Verification - -- seed real state through the existing service -- call the bridged endpoints with the flag enabled -- assert that the service behavior is unchanged -- assert that the generated OpenAPI contains the migrated paths and schemas - -## Boundary composition - -The Effect `HttpApi` layer owns its own auth and instance middleware, but it is currently mounted inside the existing Hono server. - -### Auth - -- the bridged `HttpApi` layer implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic` -- each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served -- this is independent of the Hono auth layer; the current bridge keeps the responsibility local to the `HttpApi` slice - -### Instance and workspace lookup - -- the bridged `HttpApi` layer resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params -- this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware` -- `HttpApi` handlers yield services from context and assume the correct instance has already been provided - -### Error mapping - -- keep domain and service errors typed in the service layer -- declare typed transport errors on the endpoint only when the route can actually return them intentionally -- request decoding failures are transport-level `400`s handled by Effect `HttpApi` automatically -- storage or lookup failures that are part of the route contract should be declared as typed endpoint errors - -## Exit criteria for the spike - -The first slice is successful if: - -- the bridged endpoints serve correctly through the existing Hono host when the flag is enabled -- the handlers reuse the existing Effect service -- request decoding and response shapes are schema-defined from canonical Effect schemas -- any remaining Zod boundary usage is derived from `.zod` or clearly temporary -- OpenAPI is generated from the `HttpApi` contract -- the tests are straightforward enough that the next slice feels mechanical - -## Learnings - -### Schema - -- `Schema.Class` works well for route DTOs such as `Question.Request`, `Question.Info`, and `Question.Reply`. -- scalar or collection schemas such as `Question.Answer` should stay as schemas and use helpers like `withStatics(...)` instead of being forced into classes. -- if an `HttpApi` success schema uses `Schema.Class`, the handler or underlying service needs to return real schema instances rather than plain objects. `Schema.Class`'s Declaration AST enforces `input instanceof self || input.[ClassTypeId]` during encode (see effect-smol `Schema.ts:10479-10484`). Plain objects from zod parse fail with `Expected Foo, got {...}`. This surfaced on `GET /config` where the service returns zod-parsed plain objects and `Config.InfoSchema` referenced `ConfigProvider.Info` (class). The fix was to convert pure-object classes to `Schema.Struct(...).annotate({ identifier: "..." })` — same named SDK `$ref`, no instance requirement. Verified byte-identical `types.gen.ts` vs `dev`. -- internal event payloads can stay anonymous when we want to avoid adding extra named OpenAPI component churn for non-route shapes. -- `Schema.Class` emits named `$ref` in OpenAPI — only use it for types that already had `.meta({ ref })` in the old Zod schema **and** when the handler/service returns real instances. For schemas that need a named `$ref` but are populated from plain objects, use `Schema.Struct(...).annotate({ identifier: "..." })` instead. Inner/nested types should stay as `Schema.Struct` to avoid SDK shape changes. - -### Integration - -- `HttpRouter.toWebHandler` with the shared `memoMap` from `run-service.ts` cleanly bridges Effect routes into Hono — one process, one port, shared layer instances. -- `Observability.layer` must be explicitly provided via `Layer.provideMerge` in the routes layer for OTEL spans and HTTP logs to flow. The `memoMap` deduplicates it with `AppRuntime` — no extra cost. -- `HttpMiddleware.logger` (enabled by default when `disableLogger` is not set) emits structured `Effect.log` entries with `http.method`, `http.url`, `http.status` — these flow through `OtlpLogger` to motel. -- Hono OpenAPI stubs must remain registered for SDK codegen until the SDK pipeline reads from the Effect OpenAPI spec instead. -- the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag gates the bridge at the Hono router level — default off, no behavior change unless opted in. - -## Route inventory - -Status legend: - -- `bridged` - Effect HttpApi slice exists and is bridged into Hono behind the flag -- `done` - Effect HttpApi slice exists but not yet bridged -- `next` - good near-term candidate -- `later` - possible, but not first wave -- `defer` - not a good early `HttpApi` target - -Current instance route inventory: - -- `question` - `bridged` - endpoints: `GET /question`, `POST /question/:requestID/reply`, `POST /question/:requestID/reject` -- `permission` - `bridged` - endpoints: `GET /permission`, `POST /permission/:requestID/reply` -- `provider` - `bridged` - endpoints: `GET /provider`, `GET /provider/auth`, `POST /provider/:providerID/oauth/authorize`, `POST /provider/:providerID/oauth/callback` -- `config` - `bridged` (partial) - bridged endpoints: `GET /config`, `GET /config/providers` - defer `PATCH /config` for now -- `project` - `bridged` (partial) - bridged endpoints: `GET /project`, `GET /project/current` - defer git-init mutation first -- `workspace` - `bridged` - best small reads: `GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status` - defer create/remove mutations first -- `file` - `later` - good JSON-only candidate set, but larger than the current first-wave slices -- `mcp` - `later` - has JSON-only endpoints, but interactive OAuth/auth flows make it a worse early fit -- `session` - `defer` - large, stateful, mixes CRUD with prompt/shell/command/share/revert flows and a streaming route -- `event` - `defer` - SSE only -- `global` - `defer` - mixed bag with SSE and process-level side effects -- `pty` - `defer` - websocket-heavy route surface -- `tui` - `defer` - queue-style UI bridge, weak early `HttpApi` fit - -Recommended near-term sequence: - -1. `workspace` read endpoints (`GET /experimental/workspace/adaptor`, `GET /experimental/workspace`, `GET /experimental/workspace/status`) -2. `file` JSON read endpoints -3. `mcp` JSON read endpoints +1. Produce a generated route inventory from Hono registrations and update `Current Route Status` with exact paths. +2. Start the Effect OpenAPI/SDK generation path for already-bridged routes. ## Checklist -- [x] add one small spike that defines an `HttpApi` group for a simple JSON route set -- [x] use Effect Schema request / response types for that slice -- [x] keep the underlying service calls identical to the current handlers -- [x] compare generated OpenAPI against the current Hono/OpenAPI setup -- [x] document how auth, instance lookup, and error mapping would compose in the new stack -- [x] bridge Effect routes into Hono via `toWebHandler` with shared `memoMap` -- [x] gate behind `OPENCODE_EXPERIMENTAL_HTTPAPI` flag -- [x] verify OTEL spans and HTTP logs flow to motel -- [x] bridge question, permission, and provider auth routes -- [x] port remaining provider endpoints (`GET /provider`, OAuth mutations) -- [x] port `config` providers read endpoint -- [x] port `project` read endpoints (`GET /project`, `GET /project/current`) -- [x] port `GET /config` full read endpoint -- [x] port `workspace` read endpoints -- [ ] port `file` JSON read endpoints -- [ ] decide when to remove the flag and make Effect routes the default - -## Rule of thumb - -Do not start with the hardest route file. - -If `HttpApi` is adopted here, it should arrive after the handler body is already Effect-native and after the relevant request / response models have moved to Effect Schema. +- [x] Add first `HttpApi` JSON route slices. +- [x] Bridge selected `HttpApi` routes into Hono behind `OPENCODE_EXPERIMENTAL_HTTPAPI`. +- [x] Reuse existing Effect services in handlers. +- [x] Provide auth, instance lookup, and observability in the Effect route layer. +- [x] Attach auth middleware in route modules. +- [x] Support `auth_token` as a query security scheme. +- [x] Add bridge-level auth and instance tests. +- [ ] Complete exact Hono route inventory. +- [x] Resolve implemented-but-unmounted route groups. +- [x] Port remaining top-level JSON reads. +- [ ] Generate SDK/OpenAPI from Effect routes. +- [ ] Flip ported JSON routes to default-on with fallback. +- [ ] Delete replaced Hono route implementations. +- [ ] Replace SSE/websocket/streaming Hono routes with non-Hono implementations. diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts index 672b93f6ceb7..aff523a7e9d3 100644 --- a/packages/opencode/src/acp/agent.ts +++ b/packages/opencode/src/acp/agent.ts @@ -34,7 +34,7 @@ import { import { Log } from "../util" import { pathToFileURL } from "url" import { Filesystem } from "../util" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Hash } from "@opencode-ai/core/util/hash" import { ACPSessionManager } from "./session" import type { ACPConfig } from "./types" import { Provider } from "../provider" @@ -46,14 +46,15 @@ import { MessageV2 } from "@/session/message-v2" import { Config } from "@/config" import { ConfigMCP } from "@/config/mcp" import { Todo } from "@/session/todo" -import { z } from "zod" +import { Result, Schema } from "effect" import { LoadAPIKeyError } from "ai" import type { AssistantMessage, Event, OpencodeClient, SessionMessageResponse, ToolPart } from "@opencode-ai/sdk/v2" import { applyPatch } from "diff" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" type ModeOption = { id: string; name: string; description?: string } type ModelOption = { modelId: string; name: string } +const decodeTodos = Schema.decodeUnknownResult(Schema.fromJsonString(Schema.Array(Todo.Info))) const DEFAULT_VARIANT_VALUE = "default" @@ -372,14 +373,14 @@ export class Agent implements ACPAgent { } if (part.tool === "todowrite") { - const parsedTodos = z.array(Todo.Info.zod).safeParse(JSON.parse(part.state.output)) - if (parsedTodos.success) { + const parsedTodos = decodeTodos(part.state.output) + if (Result.isSuccess(parsedTodos)) { await this.connection .sessionUpdate({ sessionId, update: { sessionUpdate: "plan", - entries: parsedTodos.data.map((todo) => { + entries: parsedTodos.success.map((todo) => { const status: PlanEntry["status"] = todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) return { @@ -394,7 +395,7 @@ export class Agent implements ACPAgent { log.error("failed to send session update for todo", { error }) }) } else { - log.error("failed to parse todo output", { error: parsedTodos.error }) + log.error("failed to parse todo output", { error: parsedTodos.failure }) } } @@ -901,14 +902,14 @@ export class Agent implements ACPAgent { } if (part.tool === "todowrite") { - const parsedTodos = z.array(Todo.Info.zod).safeParse(JSON.parse(part.state.output)) - if (parsedTodos.success) { + const parsedTodos = decodeTodos(part.state.output) + if (Result.isSuccess(parsedTodos)) { await this.connection .sessionUpdate({ sessionId, update: { sessionUpdate: "plan", - entries: parsedTodos.data.map((todo) => { + entries: parsedTodos.success.map((todo) => { const status: PlanEntry["status"] = todo.status === "cancelled" ? "completed" : (todo.status as PlanEntry["status"]) return { @@ -923,7 +924,7 @@ export class Agent implements ACPAgent { log.error("failed to send session update for todo", { error: err }) }) } else { - log.error("failed to parse todo output", { error: parsedTodos.error }) + log.error("failed to parse todo output", { error: parsedTodos.failure }) } } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 355718b6bf39..231e17467156 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -3,7 +3,6 @@ import z from "zod" import { Provider } from "../provider" import { ModelID, ProviderID } from "../provider/schema" import { generateObject, streamObject, type ModelMessage } from "ai" -import { Instance } from "../project/instance" import { Truncate } from "../tool" import { Auth } from "../auth" import { ProviderTransform } from "../provider" @@ -15,41 +14,41 @@ import PROMPT_SUMMARY from "./prompt/summary.txt" import PROMPT_TITLE from "./prompt/title.txt" import { Permission } from "@/permission" import { mergeDeep, pipe, sortBy, values } from "remeda" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import path from "path" import { Plugin } from "@/plugin" import { Skill } from "../skill" -import { Effect, Context, Layer } from "effect" +import { Effect, Context, Layer, Schema } from "effect" import { InstanceState } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" +import { zod } from "@/util/effect-zod" +import { withStatics, type DeepMutable } from "@/util/schema" -export const Info = z - .object({ - name: z.string(), - description: z.string().optional(), - mode: z.enum(["subagent", "primary", "all"]), - native: z.boolean().optional(), - hidden: z.boolean().optional(), - topP: z.number().optional(), - temperature: z.number().optional(), - color: z.string().optional(), - permission: Permission.Ruleset.zod, - model: z - .object({ - modelID: ModelID.zod, - providerID: ProviderID.zod, - }) - .optional(), - variant: z.string().optional(), - prompt: z.string().optional(), - options: z.record(z.string(), z.any()), - steps: z.number().int().positive().optional(), - }) - .meta({ - ref: "Agent", - }) -export type Info = z.infer +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.optional(Schema.String), + mode: Schema.Literals(["subagent", "primary", "all"]), + native: Schema.optional(Schema.Boolean), + hidden: Schema.optional(Schema.Boolean), + topP: Schema.optional(Schema.Number), + temperature: Schema.optional(Schema.Number), + color: Schema.optional(Schema.String), + permission: Permission.Ruleset, + model: Schema.optional( + Schema.Struct({ + modelID: ModelID, + providerID: ProviderID, + }), + ), + variant: Schema.optional(Schema.String), + prompt: Schema.optional(Schema.String), + options: Schema.Record(Schema.String, Schema.Unknown), + steps: Schema.optional(Schema.Number), +}) + .annotate({ identifier: "Agent" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = DeepMutable> export interface Interface { readonly get: (agent: string) => Effect.Effect @@ -79,7 +78,7 @@ export const layer = Layer.effect( const provider = yield* Provider.Service const state = yield* InstanceState.make( - Effect.fn("Agent.state")(function* (_ctx) { + Effect.fn("Agent.state")(function* (ctx) { const cfg = yield* config.get() const skillDirs = yield* skill.dirs() const whitelistedDirs = [Truncate.GLOB, ...skillDirs.map((dir) => path.join(dir, "*"))] @@ -136,7 +135,7 @@ export const layer = Layer.effect( edit: { "*": "deny", [path.join(".opencode", "plans", "*.md")]: "allow", - [path.relative(Instance.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow", + [path.relative(ctx.worktree, path.join(Global.Path.data, path.join("plans", "*.md")))]: "allow", }, }), user, diff --git a/packages/opencode/src/auth/index.ts b/packages/opencode/src/auth/index.ts index 5b4b5120f864..539c40c1ae63 100644 --- a/packages/opencode/src/auth/index.ts +++ b/packages/opencode/src/auth/index.ts @@ -1,8 +1,8 @@ import path from "path" import { Effect, Layer, Record, Result, Schema, Context } from "effect" import { zod } from "@/util/effect-zod" -import { Global } from "../global" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Global } from "@opencode-ai/core/global" +import { AppFileSystem } from "@opencode-ai/core/filesystem" export const OAUTH_DUMMY_KEY = "opencode-oauth-dummy-key" diff --git a/packages/opencode/src/cli/cmd/agent.ts b/packages/opencode/src/cli/cmd/agent.ts index fd559935fcd9..acad3866818f 100644 --- a/packages/opencode/src/cli/cmd/agent.ts +++ b/packages/opencode/src/cli/cmd/agent.ts @@ -2,7 +2,7 @@ import { cmd } from "./cmd" import * as prompts from "@clack/prompts" import { AppRuntime } from "@/effect/app-runtime" import { UI } from "../ui" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { Agent } from "../../agent/agent" import { Provider } from "../../provider" import path from "path" diff --git a/packages/opencode/src/cli/cmd/debug/index.ts b/packages/opencode/src/cli/cmd/debug/index.ts index 8da6ff559373..194e66b1f202 100644 --- a/packages/opencode/src/cli/cmd/debug/index.ts +++ b/packages/opencode/src/cli/cmd/debug/index.ts @@ -1,4 +1,4 @@ -import { Global } from "../../../global" +import { Global } from "@opencode-ai/core/global" import { bootstrap } from "../../bootstrap" import { cmd } from "../cmd" import { ConfigCommand } from "./config" @@ -9,6 +9,7 @@ import { ScrapCommand } from "./scrap" import { SkillCommand } from "./skill" import { SnapshotCommand } from "./snapshot" import { AgentCommand } from "./agent" +import { StartupCommand } from "./startup" export const DebugCommand = cmd({ command: "debug", @@ -22,6 +23,7 @@ export const DebugCommand = cmd({ .command(ScrapCommand) .command(SkillCommand) .command(SnapshotCommand) + .command(StartupCommand) .command(AgentCommand) .command(PathsCommand) .command({ diff --git a/packages/opencode/src/cli/cmd/debug/startup.ts b/packages/opencode/src/cli/cmd/debug/startup.ts new file mode 100644 index 000000000000..27fd5246919f --- /dev/null +++ b/packages/opencode/src/cli/cmd/debug/startup.ts @@ -0,0 +1,11 @@ +import { EOL } from "os" +import { cmd } from "../cmd" + +export const StartupCommand = cmd({ + command: "startup", + describe: "print startup timing", + builder: (yargs) => yargs, + handler() { + process.stdout.write(performance.now().toString() + EOL) + }, +}) diff --git a/packages/opencode/src/cli/cmd/import.ts b/packages/opencode/src/cli/cmd/import.ts index e52120f1af79..26256a770fd4 100644 --- a/packages/opencode/src/cli/cmd/import.ts +++ b/packages/opencode/src/cli/cmd/import.ts @@ -13,6 +13,9 @@ import { Filesystem } from "../../util" import { AppRuntime } from "@/effect/app-runtime" import { Schema } from "effect" +const decodeMessageInfo = Schema.decodeUnknownSync(MessageV2.Info) +const decodePart = Schema.decodeUnknownSync(MessageV2.Part) + /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */ export type ShareData = | { type: "session"; data: SDKSession } @@ -169,7 +172,7 @@ export const ImportCommand = cmd({ ) for (const msg of exportData.messages) { - const msgInfo = MessageV2.Info.zod.parse(msg.info) + const msgInfo = decodeMessageInfo(msg.info) as MessageV2.Info const { id, sessionID: _, ...msgData } = msgInfo Database.use((db) => db @@ -185,7 +188,7 @@ export const ImportCommand = cmd({ ) for (const part of msg.parts) { - const partInfo = MessageV2.Part.zod.parse(part) + const partInfo = decodePart(part) as MessageV2.Part const { id: partId, sessionID: _s, messageID, ...partData } = partInfo Database.use((db) => db diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts index a5751ce83667..ef22340fb2ec 100644 --- a/packages/opencode/src/cli/cmd/mcp.ts +++ b/packages/opencode/src/cli/cmd/mcp.ts @@ -11,9 +11,9 @@ import { Config } from "../../config" import { ConfigMCP } from "../../config/mcp" import { Instance } from "../../project/instance" import { Installation } from "../../installation" -import { InstallationVersion } from "../../installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import path from "path" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { modify, applyEdits } from "jsonc-parser" import { Filesystem } from "../../util" import { Bus } from "../../bus" diff --git a/packages/opencode/src/cli/cmd/plug.ts b/packages/opencode/src/cli/cmd/plug.ts index 9dfda16d6458..14c846f2c8f8 100644 --- a/packages/opencode/src/cli/cmd/plug.ts +++ b/packages/opencode/src/cli/cmd/plug.ts @@ -2,7 +2,7 @@ import { intro, log, outro, spinner } from "@clack/prompts" import type { Argv } from "yargs" import { ConfigPaths } from "../../config" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { installPlugin, patchPluginConfig, readPluginManifest } from "../../plugin/install" import { resolvePluginTarget } from "../../plugin/shared" import { Instance } from "../../project/instance" diff --git a/packages/opencode/src/cli/cmd/providers.ts b/packages/opencode/src/cli/cmd/providers.ts index e2eb0b65a343..158405e5f667 100644 --- a/packages/opencode/src/cli/cmd/providers.ts +++ b/packages/opencode/src/cli/cmd/providers.ts @@ -8,7 +8,7 @@ import { map, pipe, sortBy, values } from "remeda" import path from "path" import os from "os" import { Config } from "../../config" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import { Plugin } from "../../plugin" import { Instance } from "../../project/instance" import type { Hooks } from "@opencode-ai/plugin" diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index 0874beee16c8..a9e044f1871b 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -3,7 +3,7 @@ import path from "path" import { pathToFileURL } from "url" import { UI } from "../ui" import { cmd } from "./cmd" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { bootstrap } from "../bootstrap" import { EOL } from "os" import { Filesystem } from "../../util" diff --git a/packages/opencode/src/cli/cmd/serve.ts b/packages/opencode/src/cli/cmd/serve.ts index d5eee75dd18e..5f3211aa1c62 100644 --- a/packages/opencode/src/cli/cmd/serve.ts +++ b/packages/opencode/src/cli/cmd/serve.ts @@ -1,7 +1,7 @@ import { Server } from "../../server/server" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" export const ServeCommand = cmd({ command: "serve", diff --git a/packages/opencode/src/cli/cmd/session.ts b/packages/opencode/src/cli/cmd/session.ts index 8537a74d4510..0d4bd96b0a05 100644 --- a/packages/opencode/src/cli/cmd/session.ts +++ b/packages/opencode/src/cli/cmd/session.ts @@ -5,7 +5,7 @@ import { SessionID } from "../../session/schema" import { bootstrap } from "../bootstrap" import { UI } from "../ui" import { Locale } from "../../util" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Filesystem } from "../../util" import { Process } from "../../util" import { EOL } from "os" diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index eb5cb44e8daa..015b0ed8f46d 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -1,7 +1,6 @@ import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import * as Clipboard from "@tui/util/clipboard" import * as Selection from "@tui/util/selection" -import * as Terminal from "@tui/util/terminal" import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core" import { RouteProvider, useRoute } from "@tui/context/route" import { @@ -17,7 +16,7 @@ import { on, } from "solid-js" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import semver from "semver" import { DialogProvider, useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider" @@ -121,12 +120,6 @@ export function tui(input: { const unguard = win32InstallCtrlCGuard() win32DisableProcessedInput() - const mode = await Terminal.getTerminalBackgroundColor() - - // Re-clear after getTerminalBackgroundColor() because setRawMode(false) - // restores the original console mode, including processed input on Windows. - win32DisableProcessedInput() - const onExit = async () => { unguard?.() resolve() @@ -137,6 +130,7 @@ export function tui(input: { } const renderer = await createCliRenderer(rendererConfig(input.config)) + const mode = (await renderer.waitForThemeMode(1000)) ?? "dark" await render(() => { return ( diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 32342e77247d..7260a14f9c75 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -8,7 +8,7 @@ import { useProject } from "@tui/context/project" import { useKeybind } from "../context/keybind" import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { DialogSessionRename } from "./dialog-session-rename" import { Keybind } from "@/util" import { createDebouncedSignal } from "../util/signal" diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx index a16c98a9f49d..899ab42ee1ff 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx @@ -7,7 +7,7 @@ import { useProject } from "@tui/context/project" import { createMemo, createSignal, onMount } from "solid-js" import { setTimeout as sleep } from "node:timers/promises" import { errorData, errorMessage } from "@/util/error" -import * as Log from "@/util/log" +import * as Log from "@opencode-ai/core/util/log" import { useSDK } from "../context/sdk" import { useToast } from "../ui/toast" diff --git a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx index c74d3bbc63a0..fcbd27ca9bd0 100644 --- a/packages/opencode/src/cli/cmd/tui/component/error-component.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/error-component.tsx @@ -2,7 +2,7 @@ import { TextAttributes } from "@opentui/core" import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import * as Clipboard from "@tui/util/clipboard" import { createSignal } from "solid-js" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { win32FlushInputBuffer } from "../win32" import { getScrollAcceleration } from "../util/scroll" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx index 929f3a07daee..61d4c9e9993e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/frecency.tsx @@ -1,5 +1,5 @@ import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore } from "solid-js/store" diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index 03db74de9496..2d979ce99937 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -1,5 +1,5 @@ import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" 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 5288a819b3c9..1f8f99796615 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1302,7 +1302,9 @@ export function Prompt(props: PromptProps) { flexDirection="row" gap={1} flexGrow={1} - justifyContent={status().type === "retry" ? "space-between" : "flex-start"} + justifyContent={ + status().type === "retry" || status().type === "reconnecting" ? "space-between" : "flex-start" + } > @@ -1367,6 +1369,41 @@ export function Prompt(props: PromptProps) { ) })()} + {(() => { + const reconnecting = createMemo(() => { + const s = status() + if (s.type !== "reconnecting") return + return s + }) + const [visible, setVisible] = createSignal(false) + let timer: ReturnType | undefined + createEffect(() => { + const r = reconnecting() + if (r) { + timer = setTimeout(() => setVisible(true), 1000) + } else { + clearTimeout(timer) + setVisible(false) + } + }) + onCleanup(() => clearTimeout(timer)) + const msg = createMemo(() => { + const r = reconnecting() + if (!r) return + if (r.message.length > 80) return r.message.slice(0, 80) + "..." + return r.message + }) + + return ( + + + + {msg()} [reconnecting attempt #{reconnecting()?.attempt}] + + + + ) + })()} 0 ? theme.primary : theme.text}> @@ -1377,7 +1414,7 @@ export function Prompt(props: PromptProps) { - + {(file) => {file()}} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx index 84ba62338af0..a7dd28965cac 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/stash.tsx @@ -1,5 +1,5 @@ import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { onMount } from "solid-js" import { createStore, produce, unwrap } from "solid-js/store" diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts index a7f50ddf9dc6..b6a832dcb0e0 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-migrate.ts @@ -3,8 +3,8 @@ import { type ParseError as JsoncParseError, applyEdits, modify, parse as parseJ import { unique } from "remeda" import z from "zod" import { TuiInfo, TuiOptions } from "./tui-schema" -import { Flag } from "@/flag/flag" -import { Global } from "@/global" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Global } from "@opencode-ai/core/global" import { Filesystem, Log } from "@/util" import * as ConfigPaths from "@/config/paths" diff --git a/packages/opencode/src/cli/cmd/tui/config/tui.ts b/packages/opencode/src/cli/cmd/tui/config/tui.ts index 9d5cd65bfd89..3f99e6d2fcae 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui.ts @@ -7,18 +7,18 @@ import { ConfigParse } from "@/config/parse" import * as ConfigPaths from "@/config/paths" import { migrateTuiConfig } from "./tui-migrate" import { TuiInfo } from "./tui-schema" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { isRecord } from "@/util/record" -import { Global } from "@/global" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Global } from "@opencode-ai/core/global" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { CurrentWorkingDirectory } from "./cwd" import { ConfigPlugin } from "@/config/plugin" import { ConfigKeybinds } from "@/config/keybinds" -import { InstallationLocal, InstallationVersion } from "@/installation/version" -import { makeRuntime } from "@/effect/runtime" +import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" +import { makeRuntime } from "@opencode-ai/core/effect/runtime" import { Filesystem, Log } from "@/util" import { ConfigVariable } from "@/config/variable" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "tui.config" }) diff --git a/packages/opencode/src/cli/cmd/tui/context/directory.ts b/packages/opencode/src/cli/cmd/tui/context/directory.ts index 81f217398089..0c4e5feb9231 100644 --- a/packages/opencode/src/cli/cmd/tui/context/directory.ts +++ b/packages/opencode/src/cli/cmd/tui/context/directory.ts @@ -1,7 +1,7 @@ import { createMemo } from "solid-js" import { useProject } from "./project" import { useSync } from "./sync" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" export function useDirectory() { const project = useProject() diff --git a/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts b/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts new file mode 100644 index 000000000000..cbf995f8d027 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/editor-zed.ts @@ -0,0 +1,172 @@ +import { Database } from "bun:sqlite" +import os from "node:os" +import path from "node:path" +import z from "zod" +import { Filesystem } from "@/util" +import type { EditorSelection } from "./editor" + +const ZedEditorRowSchema = z.object({ + editor_id: z.number(), + workspace_id: z.number(), + workspace_paths: z.string().nullable(), + timestamp: z.string(), + buffer_path: z.string().nullable(), + selection_start: z.number().nullable(), + selection_end: z.number().nullable(), +}) + +const ZedEditorContentsSchema = z.object({ + contents: z.string().nullable(), +}) + +type ZedEditorRow = z.infer + +export async function resolveZedSelection(dbPath: string): Promise { + const row = queryZedActiveEditor(dbPath, process.cwd()) + if (!row?.buffer_path || row.selection_start == null || row.selection_end == null) return + + const text = + queryZedEditorContents(dbPath, row) ?? + (await Bun.file(row.buffer_path) + .text() + .catch(() => undefined)) + if (text == null) return + + const startOffset = Math.min(row.selection_start, row.selection_end) + const endOffset = Math.max(row.selection_start, row.selection_end) + + return { + text: text.slice(startOffset, endOffset), + filePath: row.buffer_path, + selection: offsetsToSelection(text, startOffset, endOffset), + } +} + +function queryZedActiveEditor(dbPath: string, cwd: string) { + let db: Database | undefined + try { + db = new Database(dbPath, { readonly: true }) + return db + .query( + `select + e.item_id as editor_id, + e.workspace_id as workspace_id, + w.paths as workspace_paths, + w.timestamp as timestamp, + e.buffer_path as buffer_path, + s.start as selection_start, + s.end as selection_end + from items i + join panes p on p.pane_id = i.pane_id and p.workspace_id = i.workspace_id + join workspaces w on w.workspace_id = i.workspace_id + join editors e on e.item_id = i.item_id and e.workspace_id = i.workspace_id + left join editor_selections s on s.editor_id = e.item_id and s.workspace_id = e.workspace_id + where i.active = 1 and p.active = 1 and i.kind = 'Editor' and e.buffer_path is not null + order by w.timestamp desc`, + ) + .all() + .flatMap((row) => { + const parsed = ZedEditorRowSchema.safeParse(row) + return parsed.success ? [parsed.data] : [] + }) + .map((row) => ({ row, score: scoreZedWorkspace(row.workspace_paths, cwd) })) + .filter((entry) => entry.score > 0) + .sort((left, right) => right.score - left.score || right.row.timestamp.localeCompare(left.row.timestamp))[0]?.row + } catch { + return + } finally { + db?.close() + } +} + +function queryZedEditorContents(dbPath: string, row: ZedEditorRow) { + let db: Database | undefined + try { + db = new Database(dbPath, { readonly: true }) + return ZedEditorContentsSchema.safeParse( + db + .query( + `select contents + from editors + where item_id = $editorID and workspace_id = $workspaceID`, + ) + .get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }), + ).data?.contents + } catch { + return + } finally { + db?.close() + } +} + +export function resolveZedDbPath() { + const candidates = [ + process.env.OPENCODE_ZED_DB, + path.join(os.homedir(), "Library", "Application Support", "Zed", "db", "0-stable", "db.sqlite"), + path.join(os.homedir(), ".local", "share", "zed", "db", "0-stable", "db.sqlite"), + ].filter((item): item is string => Boolean(item)) + + return candidates.find((item) => Filesystem.stat(item)?.isFile()) +} + +function scoreZedWorkspace(workspacePaths: string | null, cwd: string) { + return zedWorkspacePaths(workspacePaths).reduce((score, item) => { + if (pathContains(item, cwd)) return Math.max(score, 2) + if (pathContains(cwd, item)) return Math.max(score, 1) + return score + }, 0) +} + +function zedWorkspacePaths(value: string | null) { + if (!value) return [] + const parsed = parseJson(value) + if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string") + return value.split(/\r?\n/).filter(Boolean) +} + +export function offsetToPosition(text: string, offset: number) { + return offsetsToSelection(text, offset, offset).start +} + +function offsetsToSelection(text: string, startOffset: number, endOffset: number) { + const start = Math.max(0, Math.min(startOffset, text.length)) + const end = Math.max(0, Math.min(endOffset, text.length)) + let line = 1 + let lineStart = 0 + let startPosition = position(line, lineStart, start) + let endPosition = position(line, lineStart, end) + + for (let index = 0; index <= end; index++) { + if (index === start) startPosition = position(line, lineStart, index) + if (index === end) { + endPosition = position(line, lineStart, index) + break + } + if (text[index] === "\n") { + line += 1 + lineStart = index + 1 + } + } + + return { start: startPosition, end: endPosition } +} + +function position(line: number, lineStart: number, offset: number) { + return { + line, + character: offset - lineStart + 1, + } +} + +function pathContains(parent: string, child: string) { + const relative = path.relative(path.resolve(parent), path.resolve(child)) + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) +} + +function parseJson(value: string) { + try { + return JSON.parse(value) as unknown + } catch { + return + } +} diff --git a/packages/opencode/src/cli/cmd/tui/context/editor.ts b/packages/opencode/src/cli/cmd/tui/context/editor.ts index 4e6c97f6e54b..75c5440f5d94 100644 --- a/packages/opencode/src/cli/cmd/tui/context/editor.ts +++ b/packages/opencode/src/cli/cmd/tui/context/editor.ts @@ -4,7 +4,9 @@ import path from "node:path" import { onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import z from "zod" +import { isRecord } from "@/util/record" import { createSimpleContext } from "./helper" +import { resolveZedDbPath, resolveZedSelection } from "./editor-zed" const MCP_PROTOCOL_VERSION = "2025-11-25" @@ -90,6 +92,8 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create let reconnect: ReturnType | undefined let attempt = 0 let requestID = 0 + let zedSelection: Promise | undefined + let lastZedSelectionKey: string | undefined const pending = new Map() const send = (payload: JsonRpcMessage) => { @@ -114,7 +118,29 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create const connection = resolveEditorConnection() if (!connection) { - setStore("status", "disabled") + const dbPath = resolveZedDbPath() + if (!dbPath) { + setStore("status", "disabled") + scheduleReconnect(1000) + return + } + zedSelection ??= resolveZedSelection(dbPath) + .then((selection) => { + if (closed || socket) return + const key = editorSelectionKey(selection) + if (key !== lastZedSelectionKey) { + lastZedSelectionKey = key + setStore("selection", selection) + setStore("status", selection ? "connected" : "disabled") + } + }) + .catch(() => { + if (closed || socket) return + setStore("status", "disabled") + }) + .finally(() => { + zedSelection = undefined + }) scheduleReconnect(1000) return } @@ -196,7 +222,7 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create return { enabled() { - return Boolean(resolveEditorConnection()) + return Boolean(resolveEditorConnection() || resolveZedDbPath()) }, connected() { return store.status === "connected" @@ -289,6 +315,18 @@ function scoreEditorLock(lock: EditorLockFile, cwd: string) { return workspaceMatch * 1_000_000_000_000 + lock.mtimeMs } +function editorSelectionKey(selection: EditorSelection | undefined) { + if (!selection) return "" + return [ + selection.filePath, + selection.selection.start.line, + selection.selection.start.character, + selection.selection.end.line, + selection.selection.end.character, + selection.text, + ].join("\0") +} + function pathContains(parent: string, child: string) { const relative = path.relative(path.resolve(parent), path.resolve(child)) return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) @@ -313,7 +351,3 @@ function parseMessage(value: unknown) { return } } - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value) -} diff --git a/packages/opencode/src/cli/cmd/tui/context/kv.tsx b/packages/opencode/src/cli/cmd/tui/context/kv.tsx index 43266315bf01..2efa314d9128 100644 --- a/packages/opencode/src/cli/cmd/tui/context/kv.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/kv.tsx @@ -1,6 +1,6 @@ -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" import { rename, rm } from "fs/promises" import { createSignal, type Setter } from "solid-js" import { createStore, unwrap } from "solid-js/store" diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 910483764188..af06a2bf295d 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -5,7 +5,7 @@ import { useSync } from "@tui/context/sync" import { useTheme } from "@tui/context/theme" import { uniqueBy } from "remeda" import path from "path" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { iife } from "@/util/iife" import { useToast } from "../ui/toast" import { useArgs } from "./args" diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 6a240ceef875..96fa54487581 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -2,7 +2,7 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2" import type { GlobalEvent } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { batch, onCleanup, onMount } from "solid-js" export type EventSource = { diff --git a/packages/opencode/src/cli/cmd/tui/context/status-colors.ts b/packages/opencode/src/cli/cmd/tui/context/status-colors.ts new file mode 100644 index 000000000000..6e1630a3d4ce --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/context/status-colors.ts @@ -0,0 +1,100 @@ +/** + * Status Color Convention + * + * Based on ISO 3864 safety colors and WCAG accessibility standards. + * Each state includes: color + icon + text for accessibility. + * + * @see https://www.iso.org/standard/51000.html (ISO 3864) + * @see https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html (WCAG 1.4.1) + */ + +import { RGBA } from "@opentui/core" + +export const STATUS_COLORS = { + running: { + color: "#3B82F6", // Blue + bg: "rgba(59, 130, 246, 0.15)", + icon: "◐", + text: "Ejecutando...", + description: "Task is currently executing", + }, + waiting: { + color: "#F59E0B", // Yellow + bg: "rgba(245, 158, 11, 0.15)", + icon: "⏳", + text: "Esperando respuesta", + description: "Waiting for subagent response", + }, + attention: { + color: "#D4652F", // Orange + bg: "rgba(212, 101, 47, 0.15)", + icon: "⚠", + text: "Requiere atención", + description: "Requires user attention", + }, + error: { + color: "#EF4444", // Red + bg: "rgba(239, 68, 68, 0.15)", + icon: "✗", + text: "Error", + description: "An error occurred", + }, + done: { + color: "#22C55E", // Green + bg: "rgba(34, 197, 94, 0.15)", + icon: "✓", + text: "Completado", + description: "Task completed successfully", + }, + idle: { + color: "#6B7280", // Gray + bg: "rgba(107, 114, 128, 0.1)", + icon: "○", + text: "Inactivo", + description: "No activity", + }, +} as const + +export type StatusType = keyof typeof STATUS_COLORS + +/** + * Get RGBA from hex color for theme integration + */ +export function statusColorToRgba(hex: string, alpha: number = 1): RGBA { + return RGBA.fromHex(hex).withAlpha(alpha) +} + +/** + * Get RGBA background color for a status + */ +export function statusBackground(status: StatusType): RGBA { + const config = STATUS_COLORS[status] + const rgba = RGBA.fromHex(config.color) + return rgba.withAlpha(0.15) +} + +/** + * Check if a status is "active" (not idle or done) + */ +export function isActiveStatus(status: StatusType): boolean { + return status !== "idle" && status !== "done" +} + +/** + * Get toast variant mapping for existing toast system + */ +export function statusToToastVariant(status: StatusType): "error" | "warning" | "info" | "success" { + switch (status) { + case "error": + return "error" + case "attention": + case "waiting": + return "warning" + case "done": + return "success" + case "running": + case "idle": + default: + return "info" + } +} diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 57326e3a1aba..d35deb0b62a7 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -22,7 +22,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { useProject } from "@tui/context/project" import { useEvent } from "@tui/context/event" import { useSDK } from "@tui/context/sdk" -import { Binary } from "@opencode-ai/shared/util/binary" +import { Binary } from "@opencode-ai/core/util/binary" import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index af9582cfb063..ca6c0a6cf485 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -2,7 +2,7 @@ import { CliRenderEvents, SyntaxStyle, RGBA, type TerminalColors } from "@opentu import path from "path" import { createEffect, createMemo, onCleanup, onMount } from "solid-js" import { createSimpleContext } from "./helper" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import aura from "./theme/aura.json" with { type: "json" } import ayu from "./theme/ayu.json" with { type: "json" } import catppuccin from "./theme/catppuccin.json" with { type: "json" } @@ -39,7 +39,7 @@ import carbonfox from "./theme/carbonfox.json" with { type: "json" } import { useKV } from "./kv" import { useRenderer } from "@opentui/solid" import { createStore, produce } from "solid-js/store" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { useTuiConfig } from "./tui-config" import { isRecord } from "@/util/record" @@ -314,7 +314,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ setStore( produce((draft) => { const lock = pick(kv.get("theme_mode_lock")) - const mode = lock ?? props.mode + const mode = lock ?? pick(renderer.themeMode) ?? props.mode if (!lock && pick(kv.get("theme_mode")) !== undefined) { kv.set("theme_mode", undefined) } diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index ab85b1e64590..e11003bde205 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -35,6 +35,7 @@ export const TuiEvent = { Schema.Struct({ title: Schema.optional(Schema.String), message: Schema.String, + projectName: Schema.optional(Schema.String).annotate({ description: "Project name for multi-project context" }), variant: Schema.Literals(["info", "success", "warning", "error"]), duration: Schema.optional(Schema.Number).annotate({ description: "Duration in milliseconds" }), }), diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx index 8047c26458c6..7f2ef55e9b0e 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/footer.tsx @@ -1,6 +1,6 @@ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, Match, Show, Switch } from "solid-js" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" const id = "internal:home-footer" diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx index 1a9d907bb97e..c7a7b211f2d9 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx @@ -1,4 +1,4 @@ -import { For } from "solid-js" +import { createMemo, For } from "solid-js" import { DEFAULT_THEMES, useTheme } from "@tui/context/theme" const themeCount = Object.keys(DEFAULT_THEMES).length @@ -30,9 +30,12 @@ function parse(tip: string): TipPart[] { return parts } -export function Tips() { +const NO_MODELS_TIP = "Run {highlight}/connect{/highlight} to add an AI provider and start coding" + +export function Tips(props: { connected?: boolean }) { const theme = useTheme().theme - const parts = parse(TIPS[Math.floor(Math.random() * TIPS.length)]) + const randomTip = TIPS[Math.floor(Math.random() * TIPS.length)] + const parts = createMemo(() => parse(props.connected === false ? NO_MODELS_TIP : randomTip)) return ( @@ -40,7 +43,7 @@ export function Tips() { ● Tip{" "} - + {(part) => {part.text}} diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx index c0e02f74af12..26c03ee347bb 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/home/tips.tsx @@ -4,11 +4,11 @@ import { Tips } from "./tips-view" const id = "internal:home-tips" -function View(props: { show: boolean }) { +function View(props: { show: boolean; connected: boolean }) { return ( - + ) @@ -35,8 +35,13 @@ const tui: TuiPlugin = async (api) => { home_bottom() { const hidden = createMemo(() => api.kv.get("tips_hidden", false)) const first = createMemo(() => api.state.session.count() === 0) - const show = createMemo(() => !first() && !hidden()) - return + const connected = createMemo(() => + api.state.provider.some( + (item) => item.id !== "opencode" || Object.values(item.models).some((model) => model.cost?.input !== 0), + ), + ) + const show = createMemo(() => (!first() || !connected()) && !hidden()) + return }, }, }) diff --git a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx index b468d851b0c9..bb51d4f42638 100644 --- a/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +++ b/packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx @@ -1,6 +1,6 @@ import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" import { createMemo, Show } from "solid-js" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" const id = "internal:sidebar-footer" diff --git a/packages/opencode/src/cli/cmd/tui/layer.ts b/packages/opencode/src/cli/cmd/tui/layer.ts index 64cba08e82e7..a0c19e46d5bc 100644 --- a/packages/opencode/src/cli/cmd/tui/layer.ts +++ b/packages/opencode/src/cli/cmd/tui/layer.ts @@ -1,6 +1,6 @@ import { Layer } from "effect" import { TuiConfig } from "./config/tui" -import { Npm } from "@/npm" -import { Observability } from "@/effect/observability" +import { Npm } from "@opencode-ai/core/npm" +import { Observability } from "@opencode-ai/core/effect/observability" export const CliLayer = Observability.layer.pipe(Layer.merge(TuiConfig.layer), Layer.provide(Npm.defaultLayer)) diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx index 5bea4838077b..25ea3ac9edb4 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx +++ b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx @@ -18,7 +18,7 @@ import { DialogSelect, type DialogSelectOption as SelectOption } from "../ui/dia import { Prompt } from "../component/prompt" import { Slot as HostSlot } from "./slots" import type { useToast } from "../ui/toast" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" type RouteEntry = { key: symbol diff --git a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts index e4a0e59eb194..556e97684db8 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin/runtime.ts @@ -29,11 +29,11 @@ import { PluginLoader } from "@/plugin/loader" import { PluginMeta } from "@/plugin/meta" import { installPlugin as installModulePlugin, patchPluginConfig, readPluginManifest } from "@/plugin/install" import { hasTheme, upsertTheme } from "../context/theme" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" import { Process } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" -import { Flag } from "@/flag/flag" +import { Flock } from "@opencode-ai/core/util/flock" +import { Flag } from "@opencode-ai/core/flag/flag" import { INTERNAL_TUI_PLUGINS, type InternalTuiPlugin } from "./internal" import { setupSlots, Slot as View } from "./slots" import type { HostPluginApi, HostSlots } from "./slots" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index c04e58acecae..516f406aea07 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -64,7 +64,7 @@ import { DialogForkFromTimeline } from "./dialog-fork-from-timeline" import { DialogSessionRename } from "../../component/dialog-session-rename" import { Sidebar } from "./sidebar" import { SubagentFooter } from "./subagent-footer.tsx" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import parsers from "../../../../../../parsers-config.ts" import * as Clipboard from "../../util/clipboard" @@ -76,7 +76,7 @@ import stripAnsi from "strip-ansi" import { usePromptRef } from "../../context/prompt" import { useExit } from "../../context/exit" import { Filesystem } from "@/util" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { PermissionPrompt } from "./permission" import { QuestionPrompt } from "./question" import { DialogExportOptions } from "../../ui/dialog-export-options" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index e48f348b988c..d124734a3bb3 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -14,7 +14,7 @@ import path from "path" import { LANGUAGE_EXTENSIONS } from "@/lsp/language" import { Keybind } from "@/util" import { Locale } from "@/util" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { useDialog } from "../../ui/dialog" import { getScrollAcceleration } from "../../util/scroll" import { useTuiConfig } from "../../context/tui-config" diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 6d92752efe36..c49946df72d1 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -3,7 +3,7 @@ import { useSync } from "@tui/context/sync" import { createMemo, Show } from "solid-js" import { useTheme } from "../../context/theme" import { useTuiConfig } from "../../context/tui-config" -import { InstallationChannel, InstallationVersion } from "@/installation/version" +import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" import { TuiPluginRuntime } from "../../plugin" import { getScrollAcceleration } from "../../util/scroll" diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index a2a53ecafa0d..7a1c9e721284 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -15,7 +15,12 @@ import type { EventSource } from "./context/sdk" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { writeHeapSnapshot } from "v8" import { TuiConfig } from "./config/tui" -import { OPENCODE_PROCESS_ROLE, OPENCODE_RUN_ID, ensureRunID, sanitizedProcessEnv } from "@/util/opencode-process" +import { + OPENCODE_PROCESS_ROLE, + OPENCODE_RUN_ID, + ensureRunID, + sanitizedProcessEnv, +} from "@opencode-ai/core/util/opencode-process" import { validateSession } from "./validate-session" declare global { diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 29eb6fd4cb19..a5da735f6556 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -4,7 +4,7 @@ import { useTheme } from "@tui/context/theme" import { MouseButton, Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" import { useToast } from "./toast" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import * as Selection from "@tui/util/selection" export function Dialog( diff --git a/packages/opencode/src/cli/cmd/tui/ui/status-indicator.tsx b/packages/opencode/src/cli/cmd/tui/ui/status-indicator.tsx new file mode 100644 index 000000000000..a71fec976e85 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/ui/status-indicator.tsx @@ -0,0 +1,181 @@ +import { type ParentProps, Show } from "solid-js" +import { useTheme } from "@tui/context/theme" +import { TextAttributes } from "@opentui/core" +import { STATUS_COLORS, type StatusType, statusColorToRgba } from "../context/status-colors" + +/** + * Status Indicator Component + * + * Displays a colored indicator with icon and text for task states. + * Follows WCAG 1.4.1 accessibility guidelines - color + icon + text. + * + * Usage: + * ```tsx + * + * + * ``` + */ +export function StatusIndicator(props: ParentProps<{ + status: StatusType + showLabel?: boolean + showIcon?: boolean + size?: "small" | "medium" | "large" +}>) { + const { theme } = useTheme() + + const config = () => STATUS_COLORS[props.status] + const size = () => props.size ?? "medium" + + const padding = () => { + switch (size()) { + case "small": + return 0 + case "large": + return 2 + default: + return 1 + } + } + + const iconSize = () => { + switch (size()) { + case "small": + return 12 + case "large": + return 16 + default: + return 14 + } + } + + const textSize = () => { + switch (size()) { + case "small": + return 10 + case "large": + return 14 + default: + return 12 + } + } + + return ( + + + + {config().icon} + + + + + + {config().text} + + + + {props.children} + + ) +} + +/** + * Project Status Badge + * + * Shows project name with status indicator. + * Useful for multi-project views. + */ +export function ProjectStatusBadge(props: { + projectName: string + status: StatusType + onClick?: () => void +}) { + const { theme } = useTheme() + const config = () => STATUS_COLORS[props.status] + + return ( + + + {config().icon} + + + + [{props.projectName}] + + + + {config().text} + + + ) +} + +/** + * Session State Banner + * + * Full-width banner for session state changes. + * Appears at top of session view to indicate current state. + */ +export function SessionStateBanner(props: { + status: StatusType + projectName?: string +}) { + const { theme } = useTheme() + const config = () => STATUS_COLORS[props.status] + + return ( + + + {config().icon} {props.projectName ? `[${props.projectName}] ` : ""}{config().text} + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/ui/toast.tsx b/packages/opencode/src/cli/cmd/tui/ui/toast.tsx index 69674ba7ce61..f304dd60f76f 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/toast.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/toast.tsx @@ -3,9 +3,10 @@ import { createStore } from "solid-js/store" import { useTheme } from "@tui/context/theme" import { useTerminalDimensions } from "@opentui/solid" import { SplitBorder } from "../component/border" -import { TextAttributes } from "@opentui/core" +import { TextAttributes, RGBA } from "@opentui/core" import { Schema } from "effect" import { type TuiEvent } from "../event" +import { STATUS_COLORS, statusToToastVariant, type StatusType } from "../context/status-colors" export type ToastOptions = Schema.Schema.Type @@ -16,33 +17,50 @@ export function Toast() { return ( - {(current) => ( - - - - {current().title} + {(current) => { + const variant = () => current().variant + const statusColor = () => { + const status: StatusType = statusToToastVariant(variant()) + return STATUS_COLORS[status].color + } + + return ( + + + + [{current().projectName}] + + + + + {current().title} + + + + {current().message} - - - {current().message} - - - )} + + ) + }} ) } diff --git a/packages/opencode/src/cli/cmd/tui/util/terminal.ts b/packages/opencode/src/cli/cmd/tui/util/terminal.ts index a61390f2cf3f..c026b7381cef 100644 --- a/packages/opencode/src/cli/cmd/tui/util/terminal.ts +++ b/packages/opencode/src/cli/cmd/tui/util/terminal.ts @@ -17,12 +17,6 @@ function parse(color: string): RGBA | null { return null } -function mode(background: RGBA | null): "dark" | "light" { - if (!background) return "dark" - const luminance = (0.299 * background.r + 0.587 * background.g + 0.114 * background.b) / 255 - return luminance > 0.5 ? "light" : "dark" -} - /** * Query terminal colors including background, foreground, and palette (0-15). * Uses OSC escape sequences to retrieve actual terminal color values. @@ -100,36 +94,3 @@ export async function colors(): Promise<{ }, 1000) }) } - -// Keep startup mode detection separate from `colors()`: the TUI boot path only -// needs OSC 11 and should resolve on the first background response instead of -// waiting on the full palette query used by system theme generation. -export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { - if (!process.stdin.isTTY) return "dark" - - return new Promise((resolve) => { - let timeout: NodeJS.Timeout - - const cleanup = () => { - process.stdin.setRawMode(false) - process.stdin.removeListener("data", handler) - clearTimeout(timeout) - } - - const handler = (data: Buffer) => { - const match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/) - if (!match) return - cleanup() - resolve(mode(parse(match[1]))) - } - - process.stdin.setRawMode(true) - process.stdin.on("data", handler) - process.stdout.write("\x1b]11;?\x07") - - timeout = setTimeout(() => { - cleanup() - resolve("dark") - }, 1000) - }) -} diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index 8cec99c615fd..df09d5cc9c7b 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -7,11 +7,11 @@ import { Rpc } from "@/util" import { upgrade } from "@/cli/upgrade" import { Config } from "@/config" import { GlobalBus } from "@/bus/global" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { writeHeapSnapshot } from "node:v8" import { Heap } from "@/cli/heap" import { AppRuntime } from "@/effect/app-runtime" -import { ensureProcessMetadata } from "@/util/opencode-process" +import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process" ensureProcessMetadata("worker") diff --git a/packages/opencode/src/cli/cmd/uninstall.ts b/packages/opencode/src/cli/cmd/uninstall.ts index c0517d491dfe..dc076913b8e6 100644 --- a/packages/opencode/src/cli/cmd/uninstall.ts +++ b/packages/opencode/src/cli/cmd/uninstall.ts @@ -3,7 +3,7 @@ import { UI } from "../ui" import * as prompts from "@clack/prompts" import { AppRuntime } from "@/effect/app-runtime" import { Installation } from "../../installation" -import { Global } from "../../global" +import { Global } from "@opencode-ai/core/global" import fs from "fs/promises" import path from "path" import os from "os" diff --git a/packages/opencode/src/cli/cmd/upgrade.ts b/packages/opencode/src/cli/cmd/upgrade.ts index b80648c24fe1..a60b1fb0bf4c 100644 --- a/packages/opencode/src/cli/cmd/upgrade.ts +++ b/packages/opencode/src/cli/cmd/upgrade.ts @@ -3,7 +3,7 @@ import { UI } from "../ui" import * as prompts from "@clack/prompts" import { AppRuntime } from "@/effect/app-runtime" import { Installation } from "../../installation" -import { InstallationVersion } from "../../installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" export const UpgradeCommand = { command: "upgrade [target]", diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 9dd8796d6e94..19ee38ff536b 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -2,7 +2,7 @@ import { Server } from "../../server/server" import { UI } from "../ui" import { cmd } from "./cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" -import { Flag } from "../../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import open from "open" import { networkInterfaces } from "os" diff --git a/packages/opencode/src/cli/error.ts b/packages/opencode/src/cli/error.ts index f286b5166f72..adf52f5683a2 100644 --- a/packages/opencode/src/cli/error.ts +++ b/packages/opencode/src/cli/error.ts @@ -1,4 +1,4 @@ -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { errorFormat } from "@/util/error" interface ErrorLike { diff --git a/packages/opencode/src/cli/heap.ts b/packages/opencode/src/cli/heap.ts index 87b7b2ebf95c..45557391a5f9 100644 --- a/packages/opencode/src/cli/heap.ts +++ b/packages/opencode/src/cli/heap.ts @@ -1,7 +1,7 @@ import path from "path" import { writeHeapSnapshot } from "node:v8" -import { Flag } from "@/flag/flag" -import { Global } from "@/global" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Global } from "@opencode-ai/core/global" import { Log } from "@/util" const log = Log.create({ service: "heap" }) diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts index 46335d24a815..7b4cf7f3452a 100644 --- a/packages/opencode/src/cli/ui.ts +++ b/packages/opencode/src/cli/ui.ts @@ -1,6 +1,6 @@ import z from "zod" import { EOL } from "os" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { logo as glyphs } from "./logo" const wordmark = [ diff --git a/packages/opencode/src/cli/upgrade.ts b/packages/opencode/src/cli/upgrade.ts index a3e3f3013deb..da0451c55c4a 100644 --- a/packages/opencode/src/cli/upgrade.ts +++ b/packages/opencode/src/cli/upgrade.ts @@ -1,9 +1,9 @@ import { Bus } from "@/bus" import { Config } from "@/config" import { AppRuntime } from "@/effect/app-runtime" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Installation } from "@/installation" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" export async function upgrade() { const config = await AppRuntime.runPromise(Config.Service.use((cfg) => cfg.getGlobal())) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 478a12f66465..7001d4f96a62 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -5,6 +5,8 @@ import type { InstanceContext } from "@/project/instance" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, Context, Schema } from "effect" import z from "zod" +import { zod, ZodOverride } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import { Config } from "../config" import { MCP } from "../mcp" import { Skill } from "../skill" @@ -27,25 +29,22 @@ export const Event = { ), } -export const Info = z - .object({ - name: z.string(), - description: z.string().optional(), - agent: z.string().optional(), - model: z.string().optional(), - source: z.enum(["command", "mcp", "skill"]).optional(), - // workaround for zod not supporting async functions natively so we use getters - // https://zod.dev/v4/changelog?id=zfunction - template: z.promise(z.string()).or(z.string()), - subtask: z.boolean().optional(), - hints: z.array(z.string()), - }) - .meta({ - ref: "Command", - }) +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.optional(Schema.String), + agent: Schema.optional(Schema.String), + model: Schema.optional(Schema.String), + source: Schema.optional(Schema.Literals(["command", "mcp", "skill"])), + // Some command templates are lazy promises from MCP prompt resolution. + template: Schema.Unknown.annotate({ [ZodOverride]: z.promise(z.string()).or(z.string()) }), + subtask: Schema.optional(Schema.Boolean), + hints: Schema.Array(Schema.String), +}) + .annotate({ identifier: "Command" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) // for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it -export type Info = Omit, "template"> & { template: Promise | string } +export type Info = Omit, "template"> & { template: Promise | string } export function hints(template: string) { const result: string[] = [] diff --git a/packages/opencode/src/config/agent.ts b/packages/opencode/src/config/agent.ts index 2978916b570d..1d1c66a131f7 100644 --- a/packages/opencode/src/config/agent.ts +++ b/packages/opencode/src/config/agent.ts @@ -1,17 +1,16 @@ export * as ConfigAgent from "./agent" -import { Schema } from "effect" -import z from "zod" +import { Exit, Schema, SchemaGetter } from "effect" import { Bus } from "@/bus" import { zod } from "@/util/effect-zod" -import { PositiveInt } from "@/util/schema" +import { PositiveInt, withStatics } from "@/util/schema" import { Log } from "../util" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Glob } from "@opencode-ai/shared/util/glob" +import { NamedError } from "@opencode-ai/core/util/error" +import { Glob } from "@opencode-ai/core/util/glob" import { configEntryNameFromPath } from "./entry-name" -import { InvalidError } from "./error" import * as ConfigMarkdown from "./markdown" import { ConfigModelID } from "./model-id" +import { ConfigParse } from "./parse" import { ConfigPermission } from "./permission" const log = Log.create({ service: "config" }) @@ -77,7 +76,7 @@ const KNOWN_KEYS = new Set([ // - Translate the deprecated `tools: { name: boolean }` map into the new // `permission` shape (write-adjacent tools collapse into `permission.edit`). // - Coalesce `steps ?? maxSteps` so downstream can ignore the deprecated alias. -const normalize = (agent: z.infer) => { +const normalize = (agent: Schema.Schema.Type): Schema.Schema.Type => { const options: Record = { ...agent.options } for (const [key, value] of Object.entries(agent)) { if (!KNOWN_KEYS.has(key)) options[key] = value @@ -98,14 +97,15 @@ const normalize = (agent: z.infer) => { return { ...agent, options, permission, ...(steps !== undefined ? { steps } : {}) } } -export const Info = zod(AgentSchema).transform(normalize).meta({ ref: "AgentConfig" }) as unknown as z.ZodType< - Omit>>, "options" | "permission" | "steps"> & { - options?: Record - permission?: ConfigPermission.Info - steps?: number - } -> -export type Info = z.infer +export const Info = AgentSchema.pipe( + Schema.decodeTo(AgentSchema, { + decode: SchemaGetter.transform(normalize), + encode: SchemaGetter.passthrough({ strict: false }), + }), +) + .annotate({ identifier: "AgentConfig" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Schema.Schema.Type export async function load(dir: string) { const result: Record = {} @@ -134,12 +134,7 @@ export async function load(dir: string) { ...md.data, prompt: md.content.trim(), } - const parsed = Info.safeParse(config) - if (parsed.success) { - result[config.name] = parsed.data - continue - } - throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error }) + result[config.name] = ConfigParse.effectSchema(Info, config, item) } return result } @@ -168,10 +163,10 @@ export async function loadMode(dir: string) { ...md.data, prompt: md.content.trim(), } - const parsed = Info.safeParse(config) - if (parsed.success) { + const parsed = Schema.decodeUnknownExit(Info)(config, { errors: "all", propertyOrder: "original" }) + if (Exit.isSuccess(parsed)) { result[config.name] = { - ...parsed.data, + ...parsed.value, mode: "primary" as const, } } diff --git a/packages/opencode/src/config/command.ts b/packages/opencode/src/config/command.ts index 3e0adccc303b..36cae6f97c26 100644 --- a/packages/opencode/src/config/command.ts +++ b/packages/opencode/src/config/command.ts @@ -2,8 +2,8 @@ export * as ConfigCommand from "./command" import { Log } from "../util" import { Schema } from "effect" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Glob } from "@opencode-ai/shared/util/glob" +import { NamedError } from "@opencode-ai/core/util/error" +import { Glob } from "@opencode-ai/core/util/glob" import { Bus } from "@/bus" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f1ceb1b4ed39..eee835fce3d0 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -4,27 +4,27 @@ import { pathToFileURL } from "url" import os from "os" import z from "zod" import { mergeDeep, pipe } from "remeda" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import fsNode from "fs/promises" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Flag } from "../flag/flag" +import { NamedError } from "@opencode-ai/core/util/error" +import { Flag } from "@opencode-ai/core/flag/flag" import { Auth } from "../auth" import { Env } from "../env" import { applyEdits, modify } from "jsonc-parser" import { Instance, type InstanceContext } from "../project/instance" -import { InstallationLocal, InstallationVersion } from "@/installation/version" +import { InstallationLocal, InstallationVersion } from "@opencode-ai/core/installation/version" import { existsSync } from "fs" import { GlobalBus } from "@/bus/global" import { Event } from "../server/event" import { Account } from "@/account/account" import { isRecord } from "@/util/record" import type { ConsoleState } from "./console-state" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { InstanceState } from "@/effect" import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { InstanceRef } from "@/effect/instance-ref" -import { zod, ZodOverride } from "@/util/effect-zod" +import { zod } from "@/util/effect-zod" import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema" import { ConfigAgent } from "./agent" import { ConfigCommand } from "./command" @@ -42,7 +42,7 @@ import { ConfigProvider } from "./provider" import { ConfigServer } from "./server" import { ConfigSkills } from "./skills" import { ConfigVariable } from "./variable" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "config" }) @@ -81,12 +81,10 @@ export const Server = ConfigServer.Server.zod export const Layout = ConfigLayout.Layout.zod export type Layout = ConfigLayout.Layout -// Schemas that still live at the zod layer (have .transform / .preprocess / -// .meta not expressible in current Effect Schema) get referenced via a -// ZodOverride-annotated Schema.Any. Walker sees the annotation and emits the -// exact zod directly, preserving component $refs. -const AgentRef = Schema.Any.annotate({ [ZodOverride]: ConfigAgent.Info }) -const LogLevelRef = Schema.Any.annotate({ [ZodOverride]: Log.Level }) +const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({ + identifier: "LogLevel", + description: "Log level", +}) // The Effect Schema is the canonical source of truth. The `.zod` compatibility // surface is derived so existing Hono validators keep working without a parallel @@ -152,27 +150,27 @@ export const Info = Schema.Struct({ mode: Schema.optional( Schema.StructWithRest( Schema.Struct({ - build: Schema.optional(AgentRef), - plan: Schema.optional(AgentRef), + build: Schema.optional(ConfigAgent.Info), + plan: Schema.optional(ConfigAgent.Info), }), - [Schema.Record(Schema.String, AgentRef)], + [Schema.Record(Schema.String, ConfigAgent.Info)], ), ).annotate({ description: "@deprecated Use `agent` field instead." }), agent: Schema.optional( Schema.StructWithRest( Schema.Struct({ // primary - plan: Schema.optional(AgentRef), - build: Schema.optional(AgentRef), + plan: Schema.optional(ConfigAgent.Info), + build: Schema.optional(ConfigAgent.Info), // subagent - general: Schema.optional(AgentRef), - explore: Schema.optional(AgentRef), + general: Schema.optional(ConfigAgent.Info), + explore: Schema.optional(ConfigAgent.Info), // specialized - title: Schema.optional(AgentRef), - summary: Schema.optional(AgentRef), - compaction: Schema.optional(AgentRef), + title: Schema.optional(ConfigAgent.Info), + summary: Schema.optional(ConfigAgent.Info), + compaction: Schema.optional(ConfigAgent.Info), }), - [Schema.Record(Schema.String, AgentRef)], + [Schema.Record(Schema.String, ConfigAgent.Info)], ), ).annotate({ description: "Agent configuration, see https://opencode.ai/docs/agents" }), provider: Schema.optional(Schema.Record(Schema.String, ConfigProvider.Info)).annotate({ @@ -184,7 +182,7 @@ export const Info = Schema.Struct({ Schema.Union([ ConfigMCP.Info, // Matches the legacy `{ enabled: false }` form used to disable a server. - Schema.Any.annotate({ [ZodOverride]: z.object({ enabled: z.boolean() }).strict() }), + Schema.Struct({ enabled: Schema.Boolean }), ]), ), ).annotate({ description: "MCP (Model Context Protocol) server configurations" }), @@ -282,7 +280,7 @@ export interface Interface { readonly get: () => Effect.Effect readonly getGlobal: () => Effect.Effect readonly getConsoleState: () => Effect.Effect - readonly update: (config: Info) => Effect.Effect + readonly update: (config: Info, options?: { dispose?: boolean }) => Effect.Effect readonly updateGlobal: (config: Info) => Effect.Effect readonly invalidate: (wait?: boolean) => Effect.Effect readonly directories: () => Effect.Effect @@ -362,7 +360,7 @@ export const layer = Layer.effect( ), ) const parsed = ConfigParse.jsonc(expanded, source) - const data = ConfigParse.schema(Info.zod, normalizeLoadedConfig(parsed, source), source) + const data = ConfigParse.effectSchema(Info, normalizeLoadedConfig(parsed, source), source) if (!("path" in options)) return data yield* Effect.promise(() => resolveLoadedPlugins(data, options.path)) @@ -721,14 +719,14 @@ export const layer = Layer.effect( ) }) - const update = Effect.fn("Config.update")(function* (config: Info) { + const update = Effect.fn("Config.update")(function* (config: Info, options?: { dispose?: boolean }) { const dir = yield* InstanceState.directory const file = path.join(dir, "config.json") const existing = yield* loadFile(file) yield* fs .writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2)) .pipe(Effect.orDie) - yield* Effect.promise(() => Instance.dispose()) + if (options?.dispose !== false) yield* Effect.promise(() => Instance.dispose()) }) const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) { @@ -754,13 +752,13 @@ export const layer = Layer.effect( let next: Info if (!file.endsWith(".jsonc")) { - const existing = ConfigParse.schema(Info.zod, ConfigParse.jsonc(before, file), file) + const existing = ConfigParse.effectSchema(Info, ConfigParse.jsonc(before, file), file) const merged = mergeDeep(writable(existing), writable(config)) yield* fs.writeFileString(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie) next = merged } else { const updated = patchJsonc(before, writable(config)) - next = ConfigParse.schema(Info.zod, ConfigParse.jsonc(updated, file), file) + next = ConfigParse.effectSchema(Info, ConfigParse.jsonc(updated, file), file) yield* fs.writeFileString(file, updated).pipe(Effect.orDie) } diff --git a/packages/opencode/src/config/error.ts b/packages/opencode/src/config/error.ts index 06f549fd85e0..c43598048a54 100644 --- a/packages/opencode/src/config/error.ts +++ b/packages/opencode/src/config/error.ts @@ -1,7 +1,7 @@ export * as ConfigError from "./error" import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" export const JsonError = NamedError.create( "ConfigJsonError", diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index 7cad692665b9..d782d655e62c 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -1,4 +1,4 @@ -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import matter from "gray-matter" import { z } from "zod" import { Filesystem } from "../util" diff --git a/packages/opencode/src/config/parse.ts b/packages/opencode/src/config/parse.ts index 7472029ead54..9351047894a5 100644 --- a/packages/opencode/src/config/parse.ts +++ b/packages/opencode/src/config/parse.ts @@ -1,10 +1,12 @@ export * as ConfigParse from "./parse" import { type ParseError as JsoncParseError, parse as parseJsoncImpl, printParseErrorCode } from "jsonc-parser" +import { Cause, Exit, Schema as EffectSchema, SchemaIssue } from "effect" import z from "zod" +import type { DeepMutable } from "@/util/schema" import { InvalidError, JsonError } from "./error" -type Schema = z.ZodType +type ZodSchema = z.ZodType export function jsonc(text: string, filepath: string): unknown { const errors: JsoncParseError[] = [] @@ -33,7 +35,7 @@ export function jsonc(text: string, filepath: string): unknown { return data } -export function schema(schema: Schema, data: unknown, source: string): T { +export function schema(schema: ZodSchema, data: unknown, source: string): T { const parsed = schema.safeParse(data) if (parsed.success) return parsed.data @@ -42,3 +44,45 @@ export function schema(schema: Schema, data: unknown, source: string): T { issues: parsed.error.issues, }) } + +export function effectSchema>( + schema: S, + data: unknown, + source: string, +): DeepMutable { + const extra = topLevelExtraKeys(schema, data) + if (extra.length) { + throw new InvalidError({ + path: source, + issues: [ + { + code: "unrecognized_keys", + keys: extra, + path: [], + message: `Unrecognized key${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}`, + } as z.core.$ZodIssue, + ], + }) + } + + const decoded = EffectSchema.decodeUnknownExit(schema)(data, { errors: "all", propertyOrder: "original" }) + if (Exit.isSuccess(decoded)) return decoded.value as DeepMutable + const error = Cause.squash(decoded.cause) + + throw new InvalidError( + { + path: source, + issues: EffectSchema.isSchemaError(error) + ? (SchemaIssue.makeFormatterStandardSchemaV1()(error.issue).issues as z.core.$ZodIssue[]) + : ([{ code: "custom", message: String(error), path: [] }] as z.core.$ZodIssue[]), + }, + { cause: error }, + ) +} + +function topLevelExtraKeys(schema: EffectSchema.Top, data: unknown) { + if (typeof data !== "object" || data === null || Array.isArray(data)) return [] + if (schema.ast._tag !== "Objects" || schema.ast.indexSignatures.length > 0) return [] + const known = new Set(schema.ast.propertySignatures.map((item) => String(item.name))) + return Object.keys(data).filter((key) => !known.has(key)) +} diff --git a/packages/opencode/src/config/paths.ts b/packages/opencode/src/config/paths.ts index db4b914f76c3..92c1f45e1e38 100644 --- a/packages/opencode/src/config/paths.ts +++ b/packages/opencode/src/config/paths.ts @@ -2,12 +2,12 @@ export * as ConfigPaths from "./paths" import path from "path" import { Filesystem } from "@/util" -import { Flag } from "@/flag/flag" -import { Global } from "@/global" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Global } from "@opencode-ai/core/global" import { unique } from "remeda" import { JsonError } from "./error" import * as Effect from "effect/Effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" export const files = Effect.fn("ConfigPaths.projectFiles")(function* ( name: string, diff --git a/packages/opencode/src/config/permission.ts b/packages/opencode/src/config/permission.ts index fdd574683705..29278338dc26 100644 --- a/packages/opencode/src/config/permission.ts +++ b/packages/opencode/src/config/permission.ts @@ -18,17 +18,9 @@ export const Rule = Schema.Union([Action, Object]) .pipe(withStatics((s) => ({ zod: zod(s) }))) export type Rule = Schema.Schema.Type -// Known permission keys get explicit types — most are full Rule (either a -// single Action or a per-pattern object), but a handful of tools take no -// sub-target patterns and are Action-only. Unknown keys fall through the -// Record rest signature as Rule. -// -// StructWithRest canonicalises key order on decode (known first, then rest), -// which used to require the `__originalKeys` preprocess hack because -// `Permission.fromConfig` depended on the user's insertion order. That -// dependency is gone — `fromConfig` now sorts top-level keys so wildcard -// permissions come before specifics, making the final precedence -// order-independent. +// Known permission keys get explicit types in the Effect schema for generated +// docs/types. Runtime config parsing uses Effect's `propertyOrder: "original"` +// parse option so user key order is preserved for permission precedence. const InputObject = Schema.StructWithRest( Schema.Struct({ read: Schema.optional(Rule), diff --git a/packages/opencode/src/config/plugin.ts b/packages/opencode/src/config/plugin.ts index 4277c1cd6d77..9667dbb59ab6 100644 --- a/packages/opencode/src/config/plugin.ts +++ b/packages/opencode/src/config/plugin.ts @@ -1,4 +1,4 @@ -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import { Schema } from "effect" import { pathToFileURL } from "url" import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared" diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index 8d421b9a3360..9c080daa385a 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/packages/opencode/src/control-plane/adaptors/worktree.ts @@ -2,14 +2,13 @@ import { Schema } from "effect" import { AppRuntime } from "@/effect/app-runtime" import { Worktree } from "@/worktree" import { type WorkspaceAdaptor, WorkspaceInfo } from "../types" -import { zod } from "@/util/effect-zod" -import { withStatics } from "@/util/schema" const WorktreeConfig = Schema.Struct({ name: WorkspaceInfo.fields.name, branch: Schema.String, directory: Schema.String, -}).pipe(withStatics((s) => ({ zod: zod(s) }))) +}) +const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig) export const WorktreeAdaptor: WorkspaceAdaptor = { name: "Worktree", @@ -24,7 +23,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = { } }, async create(info) { - const config = WorktreeConfig.zod.parse(info) + const config = decodeWorktreeConfig(info) await AppRuntime.runPromise( Worktree.Service.use((svc) => svc.createFromInfo({ @@ -36,11 +35,11 @@ export const WorktreeAdaptor: WorkspaceAdaptor = { ) }, async remove(info) { - const config = WorktreeConfig.zod.parse(info) + const config = decodeWorktreeConfig(info) await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory }))) }, target(info) { - const config = WorktreeConfig.zod.parse(info) + const config = decodeWorktreeConfig(info) return { type: "local", directory: config.directory, diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index 107f2d9903e6..fbc4336fee82 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -8,11 +8,11 @@ import { GlobalBus } from "@/bus/global" import { Auth } from "@/auth" import { SyncEvent } from "@/sync" import { EventSequenceTable, EventTable } from "@/sync/event.sql" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Log } from "@/util" import { Filesystem } from "@/util" import { ProjectID } from "@/project/schema" -import { Slug } from "@opencode-ai/shared/util/slug" +import { Slug } from "@opencode-ai/core/util/slug" import { WorkspaceTable } from "./workspace.sql" import { getAdaptor } from "./adaptors" import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types" diff --git a/packages/opencode/src/effect/app-runtime.ts b/packages/opencode/src/effect/app-runtime.ts index d68e00a323b0..fcf64b9d20e9 100644 --- a/packages/opencode/src/effect/app-runtime.ts +++ b/packages/opencode/src/effect/app-runtime.ts @@ -1,8 +1,8 @@ import { Layer, ManagedRuntime } from "effect" import { attach } from "./run-service" -import * as Observability from "./observability" +import * as Observability from "@opencode-ai/core/effect/observability" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Bus } from "@/bus" import { Auth } from "@/auth" import { Account } from "@/account/account" @@ -46,8 +46,8 @@ import { Pty } from "@/pty" import { Installation } from "@/installation" import { ShareNext } from "@/share" import { SessionShare } from "@/share" -import { Npm } from "@/npm" -import { memoMap } from "./memo-map" +import { Npm } from "@opencode-ai/core/npm" +import { memoMap } from "@opencode-ai/core/effect/memo-map" export const AppLayer = Layer.mergeAll( Npm.defaultLayer, diff --git a/packages/opencode/src/effect/bootstrap-runtime.ts b/packages/opencode/src/effect/bootstrap-runtime.ts index 37698c43a5d6..2d542bf24178 100644 --- a/packages/opencode/src/effect/bootstrap-runtime.ts +++ b/packages/opencode/src/effect/bootstrap-runtime.ts @@ -10,8 +10,8 @@ import { Vcs } from "@/project" import { Snapshot } from "@/snapshot" import { Bus } from "@/bus" import { Config } from "@/config" -import * as Observability from "./observability" -import { memoMap } from "./memo-map" +import * as Observability from "@opencode-ai/core/effect/observability" +import { memoMap } from "@opencode-ai/core/effect/memo-map" export const BootstrapLayer = Layer.mergeAll( Config.defaultLayer, diff --git a/packages/opencode/src/effect/index.ts b/packages/opencode/src/effect/index.ts index 410ce00c22f0..623bd5f0b7f6 100644 --- a/packages/opencode/src/effect/index.ts +++ b/packages/opencode/src/effect/index.ts @@ -1,5 +1,5 @@ export * as InstanceState from "./instance-state" export * as EffectBridge from "./bridge" export * as Runner from "./runner" -export * as Observability from "./observability" -export * as EffectLogger from "./logger" +export * as Observability from "@opencode-ai/core/effect/observability" +export * as EffectLogger from "@opencode-ai/core/effect/logger" diff --git a/packages/opencode/src/effect/instance-state.ts b/packages/opencode/src/effect/instance-state.ts index 7095657f5d49..dc9214494cf3 100644 --- a/packages/opencode/src/effect/instance-state.ts +++ b/packages/opencode/src/effect/instance-state.ts @@ -1,5 +1,5 @@ import { Effect, Fiber, ScopedCache, Scope, Context } from "effect" -import * as EffectLogger from "./logger" +import * as EffectLogger from "@opencode-ai/core/effect/logger" import { Instance, type InstanceContext } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" diff --git a/packages/opencode/src/effect/run-service.ts b/packages/opencode/src/effect/run-service.ts index 98ff83ea59d0..2a54979af36d 100644 --- a/packages/opencode/src/effect/run-service.ts +++ b/packages/opencode/src/effect/run-service.ts @@ -3,10 +3,10 @@ import * as Context from "effect/Context" import { Instance } from "@/project/instance" import { LocalContext } from "@/util" import { InstanceRef, WorkspaceRef } from "./instance-ref" -import * as Observability from "./observability" +import * as Observability from "@opencode-ai/core/effect/observability" import { WorkspaceContext } from "@/control-plane/workspace-context" import type { InstanceContext } from "@/project/instance" -import { memoMap } from "./memo-map" +import { memoMap } from "@opencode-ai/core/effect/memo-map" type Refs = { instance?: InstanceContext diff --git a/packages/opencode/src/file/ignore.ts b/packages/opencode/src/file/ignore.ts index efce8728089f..68c359b9ab75 100644 --- a/packages/opencode/src/file/ignore.ts +++ b/packages/opencode/src/file/ignore.ts @@ -1,4 +1,4 @@ -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" const FOLDERS = new Set([ "node_modules", diff --git a/packages/opencode/src/file/index.ts b/packages/opencode/src/file/index.ts index ca791e412879..1308d3f698bd 100644 --- a/packages/opencode/src/file/index.ts +++ b/packages/opencode/src/file/index.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Git } from "@/git" import { Effect, Layer, Context, Schema, Scope } from "effect" import * as Stream from "effect/Stream" @@ -9,69 +9,63 @@ import { formatPatch, structuredPatch } from "diff" import fuzzysort from "fuzzysort" import ignore from "ignore" import path from "path" -import z from "zod" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Instance } from "../project/instance" import { Log } from "../util" import { Protected } from "./protected" import { Ripgrep } from "./ripgrep" - -export const Info = z - .object({ - path: z.string(), - added: z.number().int(), - removed: z.number().int(), - status: z.enum(["added", "deleted", "modified"]), - }) - .meta({ - ref: "File", - }) - -export type Info = z.infer - -export const Node = z - .object({ - name: z.string(), - path: z.string(), - absolute: z.string(), - type: z.enum(["file", "directory"]), - ignored: z.boolean(), - }) - .meta({ - ref: "FileNode", - }) -export type Node = z.infer - -export const Content = z - .object({ - type: z.enum(["text", "binary"]), - content: z.string(), - diff: z.string().optional(), - patch: z - .object({ - oldFileName: z.string(), - newFileName: z.string(), - oldHeader: z.string().optional(), - newHeader: z.string().optional(), - hunks: z.array( - z.object({ - oldStart: z.number(), - oldLines: z.number(), - newStart: z.number(), - newLines: z.number(), - lines: z.array(z.string()), - }), - ), - index: z.string().optional(), - }) - .optional(), - encoding: z.literal("base64").optional(), - mimeType: z.string().optional(), - }) - .meta({ - ref: "FileContent", - }) -export type Content = z.infer +import { zod } from "@/util/effect-zod" +import { type DeepMutable, withStatics } from "@/util/schema" + +export const Info = Schema.Struct({ + path: Schema.String, + added: Schema.Int, + removed: Schema.Int, + status: Schema.Literals(["added", "deleted", "modified"]), +}) + .annotate({ identifier: "File" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = DeepMutable> + +export const Node = Schema.Struct({ + name: Schema.String, + path: Schema.String, + absolute: Schema.String, + type: Schema.Literals(["file", "directory"]), + ignored: Schema.Boolean, +}) + .annotate({ identifier: "FileNode" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Node = DeepMutable> + +const Hunk = Schema.Struct({ + oldStart: Schema.Number, + oldLines: Schema.Number, + newStart: Schema.Number, + newLines: Schema.Number, + lines: Schema.Array(Schema.String), +}) + +const Patch = Schema.Struct({ + oldFileName: Schema.String, + newFileName: Schema.String, + oldHeader: Schema.optional(Schema.String), + newHeader: Schema.optional(Schema.String), + hunks: Schema.Array(Hunk), + index: Schema.optional(Schema.String), +}) + +export const Content = Schema.Struct({ + type: Schema.Literals(["text", "binary"]), + content: Schema.String, + diff: Schema.optional(Schema.String), + patch: Schema.optional(Patch), + encoding: Schema.optional(Schema.Literal("base64")), + mimeType: Schema.optional(Schema.String), +}) + .annotate({ identifier: "FileContent" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Content = DeepMutable> export const Event = { Edited: BusEvent.define( diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts index d6fd61f1d0a3..da5981737605 100644 --- a/packages/opencode/src/file/ripgrep.ts +++ b/packages/opencode/src/file/ripgrep.ts @@ -1,17 +1,18 @@ import path from "path" -import z from "zod" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Cause, Context, Effect, Fiber, Layer, Queue, Stream } from "effect" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Cause, Context, Effect, Fiber, Layer, Queue, Schema, Stream } from "effect" import type { PlatformError } from "effect/PlatformError" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" -import { Global } from "@/global" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { Global } from "@opencode-ai/core/global" import { Log } from "@/util" -import { sanitizedProcessEnv } from "@/util/opencode-process" +import { sanitizedProcessEnv } from "@opencode-ai/core/util/opencode-process" import { which } from "@/util/which" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "ripgrep" }) const VERSION = "15.1.0" @@ -25,83 +26,82 @@ const PLATFORM = { "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" }, } as const -const Stats = z.object({ - elapsed: z.object({ - secs: z.number(), - nanos: z.number(), - human: z.string(), - }), - searches: z.number(), - searches_with_match: z.number(), - bytes_searched: z.number(), - bytes_printed: z.number(), - matched_lines: z.number(), - matches: z.number(), +const TimeStats = Schema.Struct({ + secs: Schema.Number, + nanos: Schema.Number, + human: Schema.String, }) -const Begin = z.object({ - type: z.literal("begin"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - }), +const Stats = Schema.Struct({ + elapsed: TimeStats, + searches: Schema.Number, + searches_with_match: Schema.Number, + bytes_searched: Schema.Number, + bytes_printed: Schema.Number, + matched_lines: Schema.Number, + matches: Schema.Number, }) -export const Match = z.object({ - type: z.literal("match"), - data: z.object({ - path: z.object({ - text: z.string(), - }), - lines: z.object({ - text: z.string(), - }), - line_number: z.number(), - absolute_offset: z.number(), - submatches: z.array( - z.object({ - match: z.object({ - text: z.string(), - }), - start: z.number(), - end: z.number(), - }), - ), +const PathText = Schema.Struct({ + text: Schema.String, +}) + +const Begin = Schema.Struct({ + type: Schema.Literal("begin"), + data: Schema.Struct({ + path: PathText, }), }) -const End = z.object({ - type: z.literal("end"), - data: z.object({ - path: z.object({ - text: z.string(), +export const SearchMatch = Schema.Struct({ + path: PathText, + lines: Schema.Struct({ + text: Schema.String, + }), + line_number: Schema.Number, + absolute_offset: Schema.Number, + submatches: Schema.Array( + Schema.Struct({ + match: Schema.Struct({ + text: Schema.String, + }), + start: Schema.Number, + end: Schema.Number, }), - binary_offset: z.number().nullable(), + ), +}).pipe(withStatics((s) => ({ zod: zod(s) }))) + +export const Match = Schema.Struct({ + type: Schema.Literal("match"), + data: SearchMatch, +}) + +const End = Schema.Struct({ + type: Schema.Literal("end"), + data: Schema.Struct({ + path: PathText, + binary_offset: Schema.NullOr(Schema.Number), stats: Stats, }), }) -const Summary = z.object({ - type: z.literal("summary"), - data: z.object({ - elapsed_total: z.object({ - human: z.string(), - nanos: z.number(), - secs: z.number(), - }), +const Summary = Schema.Struct({ + type: Schema.Literal("summary"), + data: Schema.Struct({ + elapsed_total: TimeStats, stats: Stats, }), }) -const Result = z.union([Begin, Match, End, Summary]) +const Result = Schema.Union([Begin, Match, End, Summary]) +const decodeResult = Schema.decodeUnknownEffect(Schema.fromJsonString(Result)) -export type Result = z.infer -export type Match = z.infer +export type Result = Schema.Schema.Type +export type Match = Schema.Schema.Type export type Item = Match["data"] -export type Begin = z.infer -export type End = z.infer -export type Summary = z.infer +export type Begin = Schema.Schema.Type +export type End = Schema.Schema.Type +export type Summary = Schema.Schema.Type export type Row = Match["data"] export interface SearchResult { @@ -187,10 +187,7 @@ function row(data: Row): Row { } function parse(line: string) { - return Effect.try({ - try: () => Result.parse(JSON.parse(line)), - catch: (cause) => new Error("invalid ripgrep output", { cause }), - }) + return decodeResult(line).pipe(Effect.mapError((cause) => new Error("invalid ripgrep output", { cause }))) } function fail(queue: Queue.Queue, err: PlatformError | Error) { diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 0ac98b9c2d8a..57f3dda9f191 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -8,7 +8,7 @@ import z from "zod" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Git } from "@/git" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" diff --git a/packages/opencode/src/format/formatter.ts b/packages/opencode/src/format/formatter.ts index 03f8365274a3..82666b799e28 100644 --- a/packages/opencode/src/format/formatter.ts +++ b/packages/opencode/src/format/formatter.ts @@ -1,9 +1,9 @@ -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" import type { InstanceContext } from "../project/instance" import { Filesystem } from "../util" import { Process } from "../util" import { which } from "../util/which" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" export interface Context extends Pick {} diff --git a/packages/opencode/src/format/index.ts b/packages/opencode/src/format/index.ts index 53a2c10119b1..9fa53293f780 100644 --- a/packages/opencode/src/format/index.ts +++ b/packages/opencode/src/format/index.ts @@ -1,26 +1,25 @@ -import { Effect, Layer, Context } from "effect" +import { Effect, Layer, Context, Schema } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { InstanceState } from "@/effect" import path from "path" import { mergeDeep } from "remeda" -import z from "zod" import { Config } from "../config" import { Log } from "../util" import * as Formatter from "./formatter" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "format" }) -export const Status = z - .object({ - name: z.string(), - extensions: z.string().array(), - enabled: z.boolean(), - }) - .meta({ - ref: "FormatterStatus", - }) -export type Status = z.infer +export const Status = Schema.Struct({ + name: Schema.String, + extensions: Schema.Array(Schema.String), + enabled: Schema.Boolean, +}) + .annotate({ identifier: "FormatterStatus" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Status = Schema.Schema.Type export interface Interface { readonly init: () => Effect.Effect diff --git a/packages/opencode/src/git/index.ts b/packages/opencode/src/git/index.ts index 719b5607fb62..16a8624474f0 100644 --- a/packages/opencode/src/git/index.ts +++ b/packages/opencode/src/git/index.ts @@ -1,4 +1,4 @@ -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Effect, Layer, Context, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts deleted file mode 100644 index 27bac598fb75..000000000000 --- a/packages/opencode/src/global/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -import fs from "fs/promises" -import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" -import path from "path" -import os from "os" -import { Filesystem } from "../util" -import { Flock } from "@opencode-ai/shared/util/flock" - -const app = "opencode" - -const data = path.join(xdgData!, app) -const cache = path.join(xdgCache!, app) -const config = path.join(xdgConfig!, app) -const state = path.join(xdgState!, app) - -export const Path = { - // Allow override via OPENCODE_TEST_HOME for test isolation - get home() { - return process.env.OPENCODE_TEST_HOME || os.homedir() - }, - data, - bin: path.join(cache, "bin"), - log: path.join(data, "log"), - cache, - config, - state, -} - -// Initialize Flock with global state path -Flock.setGlobal({ state }) - -await Promise.all([ - fs.mkdir(Path.data, { recursive: true }), - fs.mkdir(Path.config, { recursive: true }), - fs.mkdir(Path.state, { recursive: true }), - fs.mkdir(Path.log, { recursive: true }), - fs.mkdir(Path.bin, { recursive: true }), -]) - -const CACHE_VERSION = "21" - -const version = await Filesystem.readText(path.join(Path.cache, "version")).catch(() => "0") - -if (version !== CACHE_VERSION) { - try { - const contents = await fs.readdir(Path.cache) - await Promise.all( - contents.map((item) => - fs.rm(path.join(Path.cache, item), { - recursive: true, - force: true, - }), - ), - ) - } catch {} - await Filesystem.write(path.join(Path.cache, "version"), CACHE_VERSION) -} - -export * as Global from "." diff --git a/packages/opencode/src/ide/index.ts b/packages/opencode/src/ide/index.ts index f9ce1ec635dd..4a2576f68fba 100644 --- a/packages/opencode/src/ide/index.ts +++ b/packages/opencode/src/ide/index.ts @@ -1,7 +1,7 @@ import { BusEvent } from "@/bus/bus-event" import z from "zod" import { Schema } from "effect" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { Log } from "../util" import { Process } from "@/util" diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 0a3a927b46ed..3c475f133a55 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -11,8 +11,8 @@ import { UninstallCommand } from "./cli/cmd/uninstall" import { ModelsCommand } from "./cli/cmd/models" import { UI } from "./cli/ui" import { Installation } from "./installation" -import { InstallationVersion } from "./installation/version" -import { NamedError } from "@opencode-ai/shared/util/error" +import { InstallationVersion } from "@opencode-ai/core/installation/version" +import { NamedError } from "@opencode-ai/core/util/error" import { FormatError } from "./cli/error" import { ServeCommand } from "./cli/cmd/serve" import { Filesystem } from "./util" @@ -31,14 +31,14 @@ import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { DbCommand } from "./cli/cmd/db" import path from "path" -import { Global } from "./global" +import { Global } from "@opencode-ai/core/global" import { JsonMigration } from "./storage" import { Database } from "./storage" import { errorMessage } from "./util/error" import { PluginCommand } from "./cli/cmd/plug" import { Heap } from "./cli/heap" import { drizzle } from "drizzle-orm/bun-sqlite" -import { ensureProcessMetadata } from "./util/opencode-process" +import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process" const processMetadata = ensureProcessMetadata("main") diff --git a/packages/opencode/src/installation/index.ts b/packages/opencode/src/installation/index.ts index bb3de3f3b58d..84fd02cb397b 100644 --- a/packages/opencode/src/installation/index.ts +++ b/packages/opencode/src/installation/index.ts @@ -1,16 +1,16 @@ import { Effect, Layer, Schema, Context, Stream } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { withTransientReadRetry } from "@/util/effect-http-client" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import path from "path" import z from "zod" import { BusEvent } from "@/bus/bus-event" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Log } from "../util" import semver from "semver" -import { InstallationChannel, InstallationVersion } from "./version" +import { InstallationChannel, InstallationVersion } from "@opencode-ai/core/installation/version" const log = Log.create({ service: "installation" }) diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts index e8050babfdb2..4eaa32f777cf 100644 --- a/packages/opencode/src/lsp/client.ts +++ b/packages/opencode/src/lsp/client.ts @@ -10,7 +10,7 @@ import { LANGUAGE_EXTENSIONS } from "./language" import z from "zod" import { Schema } from "effect" import type * as LSPServer from "./server" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { withTimeout } from "../util/timeout" import { Filesystem } from "../util" diff --git a/packages/opencode/src/lsp/language.ts b/packages/opencode/src/lsp/language.ts index 58f4c8488ba4..07a2e97231e8 100644 --- a/packages/opencode/src/lsp/language.ts +++ b/packages/opencode/src/lsp/language.ts @@ -14,6 +14,7 @@ export const LANGUAGE_EXTENSIONS: Record = { ".cc": "cpp", ".c++": "cpp", ".cs": "csharp", + ".csx": "csharp", ".css": "css", ".d": "d", ".pas": "pascal", diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 7741ff60e530..96741b6876da 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -7,12 +7,12 @@ import { pathToFileURL, fileURLToPath } from "url" import * as LSPServer from "./server" import z from "zod" import { Config } from "../config" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Process } from "../util" import { spawn as lspspawn } from "./launch" import { Effect, Layer, Context, Schema } from "effect" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { withStatics } from "@/util/schema" import { zod, ZodOverride } from "@/util/effect-zod" diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts index a0cb8fe3881f..32a5239be596 100644 --- a/packages/opencode/src/lsp/server.ts +++ b/packages/opencode/src/lsp/server.ts @@ -1,19 +1,19 @@ import type { ChildProcessWithoutNullStreams } from "child_process" import path from "path" import os from "os" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { text } from "node:stream/consumers" import fs from "fs/promises" import { Filesystem } from "../util" import type { InstanceContext } from "../project/instance" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Archive } from "../util" import { Process } from "../util" import { which } from "../util/which" -import { Module } from "@opencode-ai/shared/util/module" +import { Module } from "@opencode-ai/core/util/module" import { spawn } from "./launch" -import { Npm } from "../npm" +import { Npm } from "@opencode-ai/core/npm" const log = Log.create({ service: "lsp.server" }) const pathExists = async (p: string) => @@ -703,31 +703,10 @@ export const Zls: Info = { export const CSharp: Info = { id: "csharp", root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]), - extensions: [".cs"], + extensions: [".cs", ".csx"], async spawn(root) { - let bin = which("roslyn-language-server") - if (!bin) { - if (!which("dotnet")) { - log.error(".NET SDK is required to install roslyn-language-server") - return - } - - if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return - log.info("installing roslyn-language-server via dotnet tool") - const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], { - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - }) - const exit = await proc.exited - if (exit !== 0) { - log.error("Failed to install roslyn-language-server") - return - } - - bin = path.join(Global.Path.bin, "roslyn-language-server" + (process.platform === "win32" ? ".exe" : "")) - log.info(`installed roslyn-language-server`, { bin }) - } + const bin = await getRoslynLanguageServer() + if (!bin) return return { process: spawn(bin, ["--stdio", "--autoLoadProjects"], { @@ -737,6 +716,135 @@ export const CSharp: Info = { }, } +export const Razor: Info = { + id: "razor", + root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]), + extensions: [".razor", ".cshtml"], + async spawn(root) { + const bin = await getRoslynLanguageServer() + if (!bin) return + + const razor = await findVscodeRazorExtension() + if (!razor) { + log.info("VS Code C# extension with Razor support not found, skipping Razor LSP") + return + } + + log.info("using VS Code Razor extension for roslyn-language-server", { extension: razor.extension }) + return { + process: spawn( + bin, + [ + "--stdio", + "--autoLoadProjects", + `--razorSourceGenerator=${razor.compiler}`, + `--razorDesignTimePath=${razor.targets}`, + "--extension", + razor.extension, + ], + { + cwd: root, + }, + ), + } + }, +} + +let roslynLanguageServerInstall: Promise | undefined + +async function getRoslynLanguageServer() { + const existing = which("roslyn-language-server") + if (existing) return existing + + const global = await roslynLanguageServerGlobalPath() + if (global) return global + + roslynLanguageServerInstall ||= installRoslynLanguageServer().finally(() => { + roslynLanguageServerInstall = undefined + }) + return roslynLanguageServerInstall +} + +async function installRoslynLanguageServer() { + if (!which("dotnet")) { + log.error(".NET SDK is required to install roslyn-language-server") + return + } + + if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return + log.info("installing roslyn-language-server via dotnet tool") + const proc = Process.spawn(["dotnet", "tool", "install", "--global", "roslyn-language-server", "--prerelease"], { + stdout: "pipe", + stderr: "pipe", + stdin: "pipe", + }) + const exit = await proc.exited + if (exit !== 0) { + log.error("Failed to install roslyn-language-server") + return + } + + const resolved = which("roslyn-language-server") + if (resolved) { + log.info(`installed roslyn-language-server`, { bin: resolved }) + return resolved + } + + const global = await roslynLanguageServerGlobalPath() + if (global) { + log.info(`installed roslyn-language-server`, { bin: global }) + return global + } + + log.error("Installed roslyn-language-server but could not resolve executable") +} + +async function roslynLanguageServerGlobalPath() { + const bin = path.join( + process.env.DOTNET_CLI_HOME ?? os.homedir(), + ".dotnet", + "tools", + "roslyn-language-server" + (process.platform === "win32" ? ".cmd" : ""), + ) + return (await pathExists(bin)) ? bin : undefined +} + +async function findVscodeRazorExtension() { + const roots = [ + process.env.VSCODE_EXTENSIONS, + path.join(os.homedir(), ".vscode", "extensions"), + path.join(os.homedir(), ".vscode-insiders", "extensions"), + path.join(os.homedir(), ".vscode-server", "extensions"), + path.join(os.homedir(), ".vscode-server-insiders", "extensions"), + ].filter((item) => item !== undefined) + + for (const root of [...new Set(roots)]) { + const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []) + const candidates = await Promise.all( + entries + .filter((entry) => entry.isDirectory() && entry.name.startsWith("ms-dotnettools.csharp-")) + .map(async (entry) => ({ + path: path.join(root, entry.name, ".razorExtension"), + modified: (await fs.stat(path.join(root, entry.name)).catch(() => undefined))?.mtimeMs ?? 0, + })), + ) + for (const entry of candidates.sort((a, b) => b.modified - a.modified).map((candidate) => candidate.path)) { + const result = { + compiler: path.join(entry, "Microsoft.CodeAnalysis.Razor.Compiler.dll"), + targets: path.join(entry, "Targets", "Microsoft.NET.Sdk.Razor.DesignTime.targets"), + extension: path.join(entry, "Microsoft.VisualStudioCode.RazorExtension.dll"), + } + if ( + (await pathExists(result.compiler)) && + (await pathExists(result.targets)) && + (await pathExists(result.extension)) + ) { + return result + } + } + } +} + export const FSharp: Info = { id: "fsharp", root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]), diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts index efb046d7a7a1..b07d59870bcb 100644 --- a/packages/opencode/src/mcp/auth.ts +++ b/packages/opencode/src/mcp/auth.ts @@ -1,8 +1,8 @@ import path from "path" import z from "zod" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Effect, Layer, Context } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" export const Tokens = z.object({ accessToken: z.string(), diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 385d7782a6e8..22adf73904a7 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -9,15 +9,14 @@ import { type Tool as MCPToolDef, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js" -import { Config } from "../config" -import { ConfigMCP } from "../config/mcp" -import { Log } from "../util" -import { NamedError } from "@opencode-ai/shared/util/error" +import { Config } from "../config/config" +import { Log } from "../util/log" +import { Process } from "../util/process" +import { NamedError } from "@opencode-ai/util/error" import z from "zod/v4" +import { Instance } from "../project/instance" import { Installation } from "../installation" -import { InstallationVersion } from "../installation/version" import { withTimeout } from "@/util/timeout" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" import { McpOAuthProvider } from "./oauth-provider" import { McpOAuthCallback } from "./oauth-callback" import { McpAuth } from "./auth" @@ -25,261 +24,350 @@ import { BusEvent } from "../bus/bus-event" import { Bus } from "@/bus" import { TuiEvent } from "@/cli/cmd/tui/event" import open from "open" -import { Effect, Exit, Layer, Option, Context, Schema, Stream } from "effect" -import { EffectBridge } from "@/effect" -import { InstanceState } from "@/effect" -import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" - -const log = Log.create({ service: "mcp" }) -const DEFAULT_TIMEOUT = 30_000 - -export const Resource = z - .object({ - name: z.string(), - uri: z.string(), - description: z.string().optional(), - mimeType: z.string().optional(), - client: z.string(), - }) - .meta({ ref: "McpResource" }) -export type Resource = z.infer - -export const ToolsChanged = BusEvent.define( - "mcp.tools.changed", - Schema.Struct({ - server: Schema.String, - }), -) - -export const BrowserOpenFailed = BusEvent.define( - "mcp.browser.open.failed", - Schema.Struct({ - mcpName: Schema.String, - url: Schema.String, - }), -) - -export const Failed = NamedError.create( - "MCPFailed", - z.object({ - name: z.string(), - }), -) - -type MCPClient = Client - -export const Status = z - .discriminatedUnion("status", [ - z - .object({ - status: z.literal("connected"), - }) - .meta({ - ref: "MCPStatusConnected", - }), - z - .object({ - status: z.literal("disabled"), - }) - .meta({ - ref: "MCPStatusDisabled", - }), - z - .object({ - status: z.literal("failed"), - error: z.string(), - }) - .meta({ - ref: "MCPStatusFailed", - }), - z - .object({ - status: z.literal("needs_auth"), - }) - .meta({ - ref: "MCPStatusNeedsAuth", - }), - z - .object({ - status: z.literal("needs_client_registration"), - error: z.string(), - }) - .meta({ - ref: "MCPStatusNeedsClientRegistration", - }), - ]) - .meta({ - ref: "MCPStatus", - }) -export type Status = z.infer - -// Store transports for OAuth servers to allow finishing auth -type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport -const pendingOAuthTransports = new Map() - -// Prompt cache types -type PromptInfo = Awaited>["prompts"][number] -type ResourceInfo = Awaited>["resources"][number] -type McpEntry = NonNullable[string] - -function isMcpConfigured(entry: McpEntry): entry is ConfigMCP.Info { - return typeof entry === "object" && entry !== null && "type" in entry -} - -const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_-]/g, "_") - -// Convert MCP tool definition to AI SDK Tool type -function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number): Tool { - const inputSchema = mcpTool.inputSchema - // Spread first, then override type to ensure it's always "object" - const schema: JSONSchema7 = { - ...(inputSchema as JSONSchema7), - type: "object", - properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"], - additionalProperties: false, - } +export namespace MCP { + const log = Log.create({ service: "mcp" }) + const DEFAULT_TIMEOUT = 30_000 + + export const Resource = z + .object({ + name: z.string(), + uri: z.string(), + description: z.string().optional(), + mimeType: z.string().optional(), + client: z.string(), + }) + .meta({ ref: "McpResource" }) + export type Resource = z.infer - return dynamicTool({ - description: mcpTool.description ?? "", - inputSchema: jsonSchema(schema), - execute: async (args: unknown) => { - return client.callTool( - { - name: mcpTool.name, - arguments: (args || {}) as Record, - }, - CallToolResultSchema, - { - resetTimeoutOnProgress: true, - timeout, - }, - ) - }, - }) -} + export const ToolsChanged = BusEvent.define( + "mcp.tools.changed", + z.object({ + server: z.string(), + }), + ) -function defs(key: string, client: MCPClient, timeout?: number) { - return Effect.tryPromise({ - try: () => withTimeout(client.listTools(), timeout ?? DEFAULT_TIMEOUT), - catch: (err) => (err instanceof Error ? err : new Error(String(err))), - }).pipe( - Effect.map((result) => result.tools), - Effect.catch((err) => { - log.error("failed to get tools from client", { key, error: err }) - return Effect.succeed(undefined) + export const BrowserOpenFailed = BusEvent.define( + "mcp.browser.open.failed", + z.object({ + mcpName: z.string(), + url: z.string(), }), ) -} -function fetchFromClient( - clientName: string, - client: Client, - listFn: (c: Client) => Promise, - label: string, -) { - return Effect.tryPromise({ - try: () => listFn(client), - catch: (e: any) => { - log.error(`failed to get ${label}`, { clientName, error: e.message }) - return e - }, - }).pipe( - Effect.map((items) => { - const out: Record = {} - const sanitizedClient = sanitize(clientName) - for (const item of items) { - out[sanitizedClient + ":" + sanitize(item.name)] = { ...item, client: clientName } - } - return out + export const Failed = NamedError.create( + "MCPFailed", + z.object({ + name: z.string(), }), - Effect.orElseSucceed(() => undefined), ) -} -interface CreateResult { - mcpClient?: MCPClient - status: Status - defs?: MCPToolDef[] -} + type MCPClient = Client -interface AuthResult { - authorizationUrl: string - oauthState: string - client?: MCPClient -} + export const Status = z + .discriminatedUnion("status", [ + z + .object({ + status: z.literal("connected"), + }) + .meta({ + ref: "MCPStatusConnected", + }), + z + .object({ + status: z.literal("disabled"), + }) + .meta({ + ref: "MCPStatusDisabled", + }), + z + .object({ + status: z.literal("failed"), + error: z.string(), + }) + .meta({ + ref: "MCPStatusFailed", + }), + z + .object({ + status: z.literal("needs_auth"), + }) + .meta({ + ref: "MCPStatusNeedsAuth", + }), + z + .object({ + status: z.literal("needs_client_registration"), + error: z.string(), + }) + .meta({ + ref: "MCPStatusNeedsClientRegistration", + }), + ]) + .meta({ + ref: "MCPStatus", + }) + export type Status = z.infer -// --- Effect Service --- + // Register notification handlers for MCP client + function registerNotificationHandlers(client: MCPClient, serverName: string) { + client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { + log.info("tools list changed notification received", { server: serverName }) + Bus.publish(ToolsChanged, { server: serverName }) + }) + } -interface State { - status: Record - clients: Record - defs: Record -} + function isNetworkError(err: unknown): boolean { + if (err instanceof UnauthorizedError) return false + if (!(err instanceof Error)) return false + if ("code" in err && typeof (err as { code: unknown }).code === "number") return false + const msg = err.message.toLowerCase() + return ( + msg.includes("econnreset") || + msg.includes("econnrefused") || + msg.includes("etimedout") || + msg.includes("fetch failed") || + msg.includes("socket") || + msg.includes("network") || + msg.includes("connection") + ) + } -export interface Interface { - readonly status: () => Effect.Effect> - readonly clients: () => Effect.Effect> - readonly tools: () => Effect.Effect> - readonly prompts: () => Effect.Effect> - readonly resources: () => Effect.Effect> - readonly add: (name: string, mcp: ConfigMCP.Info) => Effect.Effect<{ status: Record | Status }> - readonly connect: (name: string) => Effect.Effect - readonly disconnect: (name: string) => Effect.Effect - readonly getPrompt: ( - clientName: string, - name: string, - args?: Record, - ) => Effect.Effect> | undefined> - readonly readResource: ( - clientName: string, - resourceUri: string, - ) => Effect.Effect> | undefined> - readonly startAuth: (mcpName: string) => Effect.Effect<{ authorizationUrl: string; oauthState: string }> - readonly authenticate: (mcpName: string) => Effect.Effect - readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect - readonly removeAuth: (mcpName: string) => Effect.Effect - readonly supportsOAuth: (mcpName: string) => Effect.Effect - readonly hasStoredTokens: (mcpName: string) => Effect.Effect - readonly getAuthStatus: (mcpName: string) => Effect.Effect -} + // Convert MCP tool definition to AI SDK Tool type + async function convertMcpTool( + mcpTool: MCPToolDef, + client: MCPClient, + timeout?: number, + reconnect?: () => Promise, + ): Promise { + const inputSchema = mcpTool.inputSchema + + // Spread first, then override type to ensure it's always "object" + const schema: JSONSchema7 = { + ...(inputSchema as JSONSchema7), + type: "object", + properties: (inputSchema.properties ?? {}) as JSONSchema7["properties"], + additionalProperties: false, + } -export class Service extends Context.Service()("@opencode/MCP") {} - -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const spawner = yield* ChildProcessSpawner.ChildProcessSpawner - const auth = yield* McpAuth.Service - const bus = yield* Bus.Service - - type Transport = StdioClientTransport | StreamableHTTPClientTransport | SSEClientTransport - - /** - * Connect a client via the given transport with resource safety: - * on failure the transport is closed; on success the caller owns it. - */ - const connectTransport = (transport: Transport, timeout: number) => - Effect.acquireUseRelease( - Effect.succeed(transport), - (t) => - Effect.tryPromise({ - try: () => { - const client = new Client({ name: "opencode", version: InstallationVersion }) - return withTimeout(client.connect(t), timeout).then(() => client) + return dynamicTool({ + description: mcpTool.description ?? "", + inputSchema: jsonSchema(schema), + execute: async (args: unknown) => { + const call = (c: MCPClient) => + c.callTool( + { + name: mcpTool.name, + arguments: (args || {}) as Record, + }, + CallToolResultSchema, + { + resetTimeoutOnProgress: true, + timeout, }, - catch: (e) => (e instanceof Error ? e : new Error(String(e))), + ) + if (!reconnect) return call(client) + try { + return await call(client) + } catch (err) { + if (!isNetworkError(err)) throw err + const fresh = await reconnect().catch(() => undefined) + if (!fresh) throw err + return call(fresh) + } + }, + }) + } + + // Store transports for OAuth servers to allow finishing auth + type TransportWithAuth = StreamableHTTPClientTransport | SSEClientTransport + const pendingOAuthTransports = new Map() + + // Prompt cache types + type PromptInfo = Awaited>["prompts"][number] + + type ResourceInfo = Awaited>["resources"][number] + type McpEntry = NonNullable[string] + function isMcpConfigured(entry: McpEntry): entry is Config.Mcp { + return typeof entry === "object" && entry !== null && "type" in entry + } + + async function descendants(pid: number): Promise { + if (process.platform === "win32") return [] + const pids: number[] = [] + const queue = [pid] + while (queue.length > 0) { + const current = queue.shift()! + const lines = await Process.lines(["pgrep", "-P", String(current)], { nothrow: true }) + for (const tok of lines) { + const cpid = parseInt(tok, 10) + if (!isNaN(cpid) && !pids.includes(cpid)) { + pids.push(cpid) + queue.push(cpid) + } + } + } + return pids + } + + const state = Instance.state( + async () => { + const cfg = await Config.get() + const config = cfg.mcp ?? {} + const clients: Record = {} + const status: Record = {} + + await Promise.all( + Object.entries(config).map(async ([key, mcp]) => { + if (!isMcpConfigured(mcp)) { + log.error("Ignoring MCP config entry without type", { key }) + return + } + + // If disabled by config, mark as disabled without trying to connect + if (mcp.enabled === false) { + status[key] = { status: "disabled" } + return + } + + const result = await create(key, mcp).catch(() => undefined) + if (!result) return + + status[key] = result.status + + if (result.mcpClient) { + clients[key] = result.mcpClient + } + }), + ) + return { + status, + clients, + } + }, + async (state) => { + // The MCP SDK only signals the direct child process on close. + // Servers like chrome-devtools-mcp spawn grandchild processes + // (e.g. Chrome) that the SDK never reaches, leaving them orphaned. + // Kill the full descendant tree first so the server exits promptly + // and no processes are left behind. + for (const client of Object.values(state.clients)) { + const pid = (client.transport as any)?.pid + if (typeof pid !== "number") continue + for (const dpid of await descendants(pid)) { + try { + process.kill(dpid, "SIGTERM") + } catch {} + } + } + + await Promise.all( + Object.values(state.clients).map((client) => + client.close().catch((error) => { + log.error("Failed to close MCP client", { + error, + }) }), - (t, exit) => (Exit.isFailure(exit) ? Effect.tryPromise(() => t.close()).pipe(Effect.ignore) : Effect.void), + ), ) + pendingOAuthTransports.clear() + }, + ) + + // Helper function to fetch prompts for a specific client + async function fetchPromptsForClient(clientName: string, client: Client) { + const prompts = await client.listPrompts().catch((e) => { + log.error("failed to get prompts", { clientName, error: e.message }) + return undefined + }) + + if (!prompts) { + return + } + + const commands: Record = {} + + for (const prompt of prompts.prompts) { + const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_") + const sanitizedPromptName = prompt.name.replace(/[^a-zA-Z0-9_-]/g, "_") + const key = sanitizedClientName + ":" + sanitizedPromptName + + commands[key] = { ...prompt, client: clientName } + } + return commands + } + + async function fetchResourcesForClient(clientName: string, client: Client) { + const resources = await client.listResources().catch((e) => { + log.error("failed to get prompts", { clientName, error: e.message }) + return undefined + }) + + if (!resources) { + return + } + + const commands: Record = {} + + for (const resource of resources.resources) { + const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_") + const sanitizedResourceName = resource.name.replace(/[^a-zA-Z0-9_-]/g, "_") + const key = sanitizedClientName + ":" + sanitizedResourceName + + commands[key] = { ...resource, client: clientName } + } + return commands + } + + export async function add(name: string, mcp: Config.Mcp) { + const s = await state() + const result = await create(name, mcp) + if (!result) { + const status = { + status: "failed" as const, + error: "unknown error", + } + s.status[name] = status + return { + status, + } + } + if (!result.mcpClient) { + s.status[name] = result.status + return { + status: s.status, + } + } + // Close existing client if present to prevent memory leaks + const existingClient = s.clients[name] + if (existingClient) { + await existingClient.close().catch((error) => { + log.error("Failed to close existing MCP client", { name, error }) + }) + } + s.clients[name] = result.mcpClient + s.status[name] = result.status + + return { + status: s.status, + } + } + + async function create(key: string, mcp: Config.Mcp) { + if (mcp.enabled === false) { + log.info("mcp server disabled", { key }) + return { + mcpClient: undefined, + status: { status: "disabled" as const }, + } + } - const DISABLED_RESULT: CreateResult = { status: { status: "disabled" } } + log.info("found", { key, type: mcp.type }) + let mcpClient: MCPClient | undefined + let status: Status | undefined = undefined - const connectRemote = Effect.fn("MCP.connectRemote")(function* ( - key: string, - mcp: ConfigMCP.Info & { type: "remote" }, - ) { + if (mcp.type === "remote") { + // OAuth is enabled by default for remote servers unless explicitly disabled with oauth: false const oauthDisabled = mcp.oauth === false const oauthConfig = typeof mcp.oauth === "object" ? mcp.oauth : undefined let authProvider: McpOAuthProvider | undefined @@ -292,14 +380,13 @@ export const layer = Layer.effect( clientId: oauthConfig?.clientId, clientSecret: oauthConfig?.clientSecret, scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, }, { onRedirect: async (url) => { log.info("oauth redirect requested", { key, url: url.toString() }) + // Store the URL - actual browser opening is handled by startAuth }, }, - auth, ) } @@ -320,77 +407,78 @@ export const layer = Layer.effect( }, ] + let lastError: Error | undefined const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT - let lastStatus: Status | undefined - for (const { name, transport } of transports) { - const result = yield* connectTransport(transport, connectTimeout).pipe( - Effect.map((client) => ({ client, transportName: name })), - Effect.catch((error) => { - const lastError = error instanceof Error ? error : new Error(String(error)) - const isAuthError = - error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) - - if (isAuthError) { - log.info("mcp server requires authentication", { key, transport: name }) - - if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { - lastStatus = { - status: "needs_client_registration" as const, - error: "Server does not support dynamic client registration. Please provide clientId in config.", - } - return bus - .publish(TuiEvent.ToastShow, { - title: "MCP Authentication Required", - message: `Server "${key}" requires a pre-registered client ID. Add clientId to your config.`, - variant: "warning", - duration: 8000, - }) - .pipe(Effect.ignore, Effect.as(undefined)) - } else { - pendingOAuthTransports.set(key, transport) - lastStatus = { status: "needs_auth" as const } - return bus - .publish(TuiEvent.ToastShow, { - title: "MCP Authentication Required", - message: `Server "${key}" requires authentication. Run: opencode mcp auth ${key}`, - variant: "warning", - duration: 8000, - }) - .pipe(Effect.ignore, Effect.as(undefined)) + try { + const client = new Client({ + name: "opencode", + version: Installation.VERSION, + }) + await withTimeout(client.connect(transport), connectTimeout) + registerNotificationHandlers(client, key) + mcpClient = client + log.info("connected", { key, transport: name }) + status = { status: "connected" } + break + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)) + + // Handle OAuth-specific errors. + // The SDK throws UnauthorizedError when auth() returns 'REDIRECT', + // but may also throw plain Errors when auth() fails internally + // (e.g. during discovery, registration, or state generation). + // When an authProvider is attached, treat both cases as auth-related. + const isAuthError = + error instanceof UnauthorizedError || (authProvider && lastError.message.includes("OAuth")) + if (isAuthError) { + log.info("mcp server requires authentication", { key, transport: name }) + + // Check if this is a "needs registration" error + if (lastError.message.includes("registration") || lastError.message.includes("client_id")) { + status = { + status: "needs_client_registration" as const, + error: "Server does not support dynamic client registration. Please provide clientId in config.", } + // Show toast for needs_client_registration + Bus.publish(TuiEvent.ToastShow, { + title: "MCP Authentication Required", + message: `Server "${key}" requires a pre-registered client ID. Add clientId to your config.`, + variant: "warning", + duration: 8000, + }).catch((e) => log.debug("failed to show toast", { error: e })) + } else { + // Store transport for later finishAuth call + pendingOAuthTransports.set(key, transport) + status = { status: "needs_auth" as const } + // Show toast for needs_auth + Bus.publish(TuiEvent.ToastShow, { + title: "MCP Authentication Required", + message: `Server "${key}" requires authentication. Run: opencode mcp auth ${key}`, + variant: "warning", + duration: 8000, + }).catch((e) => log.debug("failed to show toast", { error: e })) } + break + } - log.debug("transport connection failed", { - key, - transport: name, - url: mcp.url, - error: lastError.message, - }) - lastStatus = { status: "failed" as const, error: lastError.message } - return Effect.succeed(undefined) - }), - ) - if (result) { - log.info("connected", { key, transport: result.transportName }) - return { client: result.client as MCPClient | undefined, status: { status: "connected" } as Status } + log.debug("transport connection failed", { + key, + transport: name, + url: mcp.url, + error: lastError.message, + }) + status = { + status: "failed" as const, + error: lastError.message, + } } - // If this was an auth error, stop trying other transports - if (lastStatus?.status === "needs_auth" || lastStatus?.status === "needs_client_registration") break } + } - return { - client: undefined as MCPClient | undefined, - status: (lastStatus ?? { status: "failed", error: "Unknown error" }) as Status, - } - }) - - const connectLocal = Effect.fn("MCP.connectLocal")(function* ( - key: string, - mcp: ConfigMCP.Info & { type: "local" }, - ) { + if (mcp.type === "local") { const [cmd, ...args] = mcp.command - const cwd = yield* InstanceState.directory + const cwd = Instance.directory const transport = new StdioClientTransport({ stderr: "pipe", command: cmd, @@ -407,526 +495,536 @@ export const layer = Layer.effect( }) const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT - return yield* connectTransport(transport, connectTimeout).pipe( - Effect.map((client): { client: MCPClient | undefined; status: Status } => ({ - client, - status: { status: "connected" }, - })), - Effect.catch((error): Effect.Effect<{ client: MCPClient | undefined; status: Status }> => { - const msg = error instanceof Error ? error.message : String(error) - log.error("local mcp startup failed", { key, command: mcp.command, cwd, error: msg }) - return Effect.succeed({ client: undefined, status: { status: "failed", error: msg } }) - }), - ) - }) - - const create = Effect.fn("MCP.create")(function* (key: string, mcp: ConfigMCP.Info) { - if (mcp.enabled === false) { - log.info("mcp server disabled", { key }) - return DISABLED_RESULT + try { + const client = new Client({ + name: "opencode", + version: Installation.VERSION, + }) + await withTimeout(client.connect(transport), connectTimeout) + registerNotificationHandlers(client, key) + mcpClient = client + status = { + status: "connected", + } + } catch (error) { + log.error("local mcp startup failed", { + key, + command: mcp.command, + cwd, + error: error instanceof Error ? error.message : String(error), + }) + status = { + status: "failed" as const, + error: error instanceof Error ? error.message : String(error), + } } + } - log.info("found", { key, type: mcp.type }) - - const { client: mcpClient, status } = - mcp.type === "remote" - ? yield* connectRemote(key, mcp as ConfigMCP.Info & { type: "remote" }) - : yield* connectLocal(key, mcp as ConfigMCP.Info & { type: "local" }) - - if (!mcpClient) { - return { status } satisfies CreateResult + if (!status) { + status = { + status: "failed" as const, + error: "Unknown error", } + } - const listed = yield* defs(key, mcpClient, mcp.timeout) - if (!listed) { - yield* Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore) - return { status: { status: "failed", error: "Failed to get tools" } } satisfies CreateResult + if (!mcpClient) { + return { + mcpClient: undefined, + status, } + } - log.info("create() successfully created client", { key, toolCount: listed.length }) - return { mcpClient, status, defs: listed } satisfies CreateResult + const result = await withTimeout(mcpClient.listTools(), mcp.timeout ?? DEFAULT_TIMEOUT).catch((err) => { + log.error("failed to get tools from client", { key, error: err }) + return undefined }) - const cfgSvc = yield* Config.Service - - const descendants = Effect.fnUntraced( - function* (pid: number) { - if (process.platform === "win32") return [] as number[] - const pids: number[] = [] - const queue = [pid] - while (queue.length > 0) { - const current = queue.shift()! - const handle = yield* spawner.spawn(ChildProcess.make("pgrep", ["-P", String(current)], { stdin: "ignore" })) - const text = yield* Stream.mkString(Stream.decodeText(handle.stdout)) - yield* handle.exitCode - for (const tok of text.split("\n")) { - const cpid = parseInt(tok, 10) - if (!isNaN(cpid) && !pids.includes(cpid)) { - pids.push(cpid) - queue.push(cpid) - } - } - } - return pids - }, - Effect.scoped, - Effect.catch(() => Effect.succeed([] as number[])), - ) - - function watch(s: State, name: string, client: MCPClient, bridge: EffectBridge.Shape, timeout?: number) { - client.setNotificationHandler(ToolListChangedNotificationSchema, async () => { - log.info("tools list changed notification received", { server: name }) - if (s.clients[name] !== client || s.status[name]?.status !== "connected") return - - const listed = await bridge.promise(defs(name, client, timeout)) - if (!listed) return - if (s.clients[name] !== client || s.status[name]?.status !== "connected") return - - s.defs[name] = listed - await bridge.promise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore)) + if (!result) { + await mcpClient.close().catch((error) => { + log.error("Failed to close MCP client", { + error, + }) }) + status = { + status: "failed", + error: "Failed to get tools", + } + return { + mcpClient: undefined, + status: { + status: "failed" as const, + error: "Failed to get tools", + }, + } } - const state = yield* InstanceState.make( - Effect.fn("MCP.state")(function* () { - const cfg = yield* cfgSvc.get() - const bridge = yield* EffectBridge.make() - const config = cfg.mcp ?? {} - const s: State = { - status: {}, - clients: {}, - defs: {}, - } - - yield* Effect.forEach( - Object.entries(config), - ([key, mcp]) => - Effect.gen(function* () { - if (!isMcpConfigured(mcp)) { - log.error("Ignoring MCP config entry without type", { key }) - return - } + log.info("create() successfully created client", { key, toolCount: result.tools.length }) + return { + mcpClient, + status, + } + } - if (mcp.enabled === false) { - s.status[key] = { status: "disabled" } - return - } + export async function status() { + const s = await state() + const cfg = await Config.get() + const config = cfg.mcp ?? {} + const result: Record = {} - const result = yield* create(key, mcp).pipe(Effect.catch(() => Effect.void)) - if (!result) return + // Include all configured MCPs from config, not just connected ones + for (const [key, mcp] of Object.entries(config)) { + if (!isMcpConfigured(mcp)) continue + result[key] = s.status[key] ?? { status: "disabled" } + } - s.status[key] = result.status - if (result.mcpClient) { - s.clients[key] = result.mcpClient - s.defs[key] = result.defs! - watch(s, key, result.mcpClient, bridge, mcp.timeout) - } - }), - { concurrency: "unbounded" }, - ) + return result + } - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - yield* Effect.forEach( - Object.values(s.clients), - (client) => - Effect.gen(function* () { - const pid = client.transport instanceof StdioClientTransport ? client.transport.pid : null - if (typeof pid === "number") { - const pids = yield* descendants(pid) - for (const dpid of pids) { - try { - process.kill(dpid, "SIGTERM") - } catch {} - } - } - yield* Effect.tryPromise(() => client.close()).pipe(Effect.ignore) - }), - { concurrency: "unbounded" }, - ) - pendingOAuthTransports.clear() - }), - ) + export async function clients() { + return state().then((state) => state.clients) + } - return s - }), - ) + export async function connect(name: string) { + const cfg = await Config.get() + const config = cfg.mcp ?? {} + const mcp = config[name] + if (!mcp) { + log.error("MCP config not found", { name }) + return + } - function closeClient(s: State, name: string) { - const client = s.clients[name] - delete s.defs[name] - if (!client) return Effect.void - return Effect.tryPromise(() => client.close()).pipe(Effect.ignore) - } - - const storeClient = Effect.fnUntraced(function* ( - s: State, - name: string, - client: MCPClient, - listed: MCPToolDef[], - timeout?: number, - ) { - const bridge = yield* EffectBridge.make() - yield* closeClient(s, name) - s.status[name] = { status: "connected" } - s.clients[name] = client - s.defs[name] = listed - watch(s, name, client, bridge, timeout) - return s.status[name] - }) + if (!isMcpConfigured(mcp)) { + log.error("Ignoring MCP connect request for config without type", { name }) + return + } - const status = Effect.fn("MCP.status")(function* () { - const s = yield* InstanceState.get(state) + const result = await create(name, { ...mcp, enabled: true }) - const cfg = yield* cfgSvc.get() - const config = cfg.mcp ?? {} - const result: Record = {} - - for (const [key, mcp] of Object.entries(config)) { - if (!isMcpConfigured(mcp)) continue - result[key] = s.status[key] ?? { status: "disabled" } + if (!result) { + const s = await state() + s.status[name] = { + status: "failed", + error: "Unknown error during connection", } + return + } - return result - }) - - const clients = Effect.fn("MCP.clients")(function* () { - const s = yield* InstanceState.get(state) - return s.clients - }) + const s = await state() + s.status[name] = result.status + if (result.mcpClient) { + // Close existing client if present to prevent memory leaks + const existingClient = s.clients[name] + if (existingClient) { + await existingClient.close().catch((error) => { + log.error("Failed to close existing MCP client", { name, error }) + }) + } + s.clients[name] = result.mcpClient + } + } - const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCP.Info) { - const s = yield* InstanceState.get(state) - const result = yield* create(name, mcp) + export async function disconnect(name: string) { + const s = await state() + const client = s.clients[name] + if (client) { + await client.close().catch((error) => { + log.error("Failed to close MCP client", { name, error }) + }) + delete s.clients[name] + } + s.status[name] = { status: "disabled" } + } - s.status[name] = result.status - if (!result.mcpClient) { - yield* closeClient(s, name) - delete s.clients[name] - return result.status - } + export async function tools() { + const result: Record = {} + const s = await state() + const cfg = await Config.get() + const config = cfg.mcp ?? {} + const clientsSnapshot = await clients() + const defaultTimeout = cfg.experimental?.mcp_timeout - return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout) - }) + const connectedClients = Object.entries(clientsSnapshot).filter( + ([clientName]) => s.status[clientName]?.status === "connected", + ) - const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCP.Info) { - yield* createAndStore(name, mcp) - const s = yield* InstanceState.get(state) - return { status: s.status } - }) + const toolsResults = await Promise.all( + connectedClients.map(async ([clientName, client]) => { + const toolsResult = await client.listTools().catch((e) => { + log.error("failed to get tools", { clientName, error: e.message }) + const failedStatus = { + status: "failed" as const, + error: e instanceof Error ? e.message : String(e), + } + s.status[clientName] = failedStatus + delete s.clients[clientName] + return undefined + }) + return { clientName, client, toolsResult } + }), + ) - const connect = Effect.fn("MCP.connect")(function* (name: string) { - const mcp = yield* getMcpConfig(name) - if (!mcp) { - log.error("MCP config not found or invalid", { name }) - return + for (const { clientName, client, toolsResult } of toolsResults) { + if (!toolsResult) continue + const mcpConfig = config[clientName] + const entry = isMcpConfigured(mcpConfig) ? mcpConfig : undefined + const timeout = entry?.timeout ?? defaultTimeout + const reconnect: (() => Promise) | undefined = + entry && entry.type === "remote" + ? async () => { + const cur = await state() + const old = cur.clients[clientName] + if (old) { + await old.close().catch(() => {}) + delete cur.clients[clientName] + } + log.info("reconnecting remote mcp server after tool call failure", { clientName }) + const r = await create(clientName, entry).catch(() => undefined) + if (!r?.mcpClient) return undefined + cur.clients[clientName] = r.mcpClient + cur.status[clientName] = r.status + return r.mcpClient + } + : undefined + for (const mcpTool of toolsResult.tools) { + const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_") + const sanitizedToolName = mcpTool.name.replace(/[^a-zA-Z0-9_-]/g, "_") + result[sanitizedClientName + "_" + sanitizedToolName] = await convertMcpTool( + mcpTool, + client, + timeout, + reconnect, + ) } - yield* createAndStore(name, { ...mcp, enabled: true }) - }) + } + return result + } - const disconnect = Effect.fn("MCP.disconnect")(function* (name: string) { - const s = yield* InstanceState.get(state) - yield* closeClient(s, name) - delete s.clients[name] - s.status[name] = { status: "disabled" } - }) + export async function prompts() { + const s = await state() + const clientsSnapshot = await clients() - const tools = Effect.fn("MCP.tools")(function* () { - const result: Record = {} - const s = yield* InstanceState.get(state) + const prompts = Object.fromEntries( + ( + await Promise.all( + Object.entries(clientsSnapshot).map(async ([clientName, client]) => { + if (s.status[clientName]?.status !== "connected") { + return [] + } - const cfg = yield* cfgSvc.get() - const config = cfg.mcp ?? {} - const defaultTimeout = cfg.experimental?.mcp_timeout + return Object.entries((await fetchPromptsForClient(clientName, client)) ?? {}) + }), + ) + ).flat(), + ) - const connectedClients = Object.entries(s.clients).filter( - ([clientName]) => s.status[clientName]?.status === "connected", - ) + return prompts + } - yield* Effect.forEach( - connectedClients, - ([clientName, client]) => - Effect.gen(function* () { - const mcpConfig = config[clientName] - const entry = mcpConfig && isMcpConfigured(mcpConfig) ? mcpConfig : undefined - - const listed = s.defs[clientName] - if (!listed) { - log.warn("missing cached tools for connected server", { clientName }) - return - } + export async function resources() { + const s = await state() + const clientsSnapshot = await clients() - const timeout = entry?.timeout ?? defaultTimeout - for (const mcpTool of listed) { - result[sanitize(clientName) + "_" + sanitize(mcpTool.name)] = convertMcpTool(mcpTool, client, timeout) + const result = Object.fromEntries( + ( + await Promise.all( + Object.entries(clientsSnapshot).map(async ([clientName, client]) => { + if (s.status[clientName]?.status !== "connected") { + return [] } + + return Object.entries((await fetchResourcesForClient(clientName, client)) ?? {}) }), - { concurrency: "unbounded" }, - ) - return result - }) + ) + ).flat(), + ) - function collectFromConnected( - s: State, - listFn: (c: Client) => Promise, - label: string, - ) { - return Effect.forEach( - Object.entries(s.clients).filter(([name]) => s.status[name]?.status === "connected"), - ([clientName, client]) => - fetchFromClient(clientName, client, listFn, label).pipe(Effect.map((items) => Object.entries(items ?? {}))), - { concurrency: "unbounded" }, - ).pipe(Effect.map((results) => Object.fromEntries(results.flat()))) - } - - const prompts = Effect.fn("MCP.prompts")(function* () { - const s = yield* InstanceState.get(state) - return yield* collectFromConnected(s, (c) => c.listPrompts().then((r) => r.prompts), "prompts") - }) + return result + } - const resources = Effect.fn("MCP.resources")(function* () { - const s = yield* InstanceState.get(state) - return yield* collectFromConnected(s, (c) => c.listResources().then((r) => r.resources), "resources") - }) + export async function getPrompt(clientName: string, name: string, args?: Record) { + const clientsSnapshot = await clients() + const client = clientsSnapshot[clientName] - const withClient = Effect.fnUntraced(function* ( - clientName: string, - fn: (client: MCPClient) => Promise, - label: string, - meta?: Record, - ) { - const s = yield* InstanceState.get(state) - const client = s.clients[clientName] - if (!client) { - log.warn(`client not found for ${label}`, { clientName }) + if (!client) { + log.warn("client not found for prompt", { + clientName, + }) + return undefined + } + + const result = await client + .getPrompt({ + name: name, + arguments: args, + }) + .catch((e) => { + log.error("failed to get prompt from MCP server", { + clientName, + promptName: name, + error: e.message, + }) return undefined - } - return yield* Effect.tryPromise({ - try: () => fn(client), - catch: (e: any) => { - log.error(`failed to ${label}`, { clientName, ...meta, error: e?.message }) - return e - }, - }).pipe(Effect.orElseSucceed(() => undefined)) - }) + }) + + return result + } + + export async function readResource(clientName: string, resourceUri: string) { + const clientsSnapshot = await clients() + const client = clientsSnapshot[clientName] - const getPrompt = Effect.fn("MCP.getPrompt")(function* ( - clientName: string, - name: string, - args?: Record, - ) { - return yield* withClient(clientName, (client) => client.getPrompt({ name, arguments: args }), "getPrompt", { - promptName: name, + if (!client) { + log.warn("client not found for prompt", { + clientName: clientName, }) - }) + return undefined + } - const readResource = Effect.fn("MCP.readResource")(function* (clientName: string, resourceUri: string) { - return yield* withClient(clientName, (client) => client.readResource({ uri: resourceUri }), "readResource", { - resourceUri, + const result = await client + .readResource({ + uri: resourceUri, + }) + .catch((e) => { + log.error("failed to get prompt from MCP server", { + clientName: clientName, + resourceUri: resourceUri, + error: e.message, + }) + return undefined }) - }) - const getMcpConfig = Effect.fnUntraced(function* (mcpName: string) { - const cfg = yield* cfgSvc.get() - const mcpConfig = cfg.mcp?.[mcpName] - if (!mcpConfig || !isMcpConfigured(mcpConfig)) return undefined - return mcpConfig - }) + return result + } - const startAuth = Effect.fn("MCP.startAuth")(function* (mcpName: string) { - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) throw new Error(`MCP server ${mcpName} not found or disabled`) - if (mcpConfig.type !== "remote") throw new Error(`MCP server ${mcpName} is not a remote server`) - if (mcpConfig.oauth === false) throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`) - - // OAuth config is optional - if not provided, we'll use auto-discovery - const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined - - // Start the callback server with custom redirectUri if configured - yield* Effect.promise(() => McpOAuthCallback.ensureRunning(oauthConfig?.redirectUri)) - - const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32))) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - yield* auth.updateOAuthState(mcpName, oauthState) - let capturedUrl: URL | undefined - const authProvider = new McpOAuthProvider( - mcpName, - mcpConfig.url, - { - clientId: oauthConfig?.clientId, - clientSecret: oauthConfig?.clientSecret, - scope: oauthConfig?.scope, - redirectUri: oauthConfig?.redirectUri, - }, - { - onRedirect: async (url) => { - capturedUrl = url - }, - }, - auth, - ) + /** + * Start OAuth authentication flow for an MCP server. + * Returns the authorization URL that should be opened in a browser. + */ + export async function startAuth(mcpName: string): Promise<{ authorizationUrl: string }> { + const cfg = await Config.get() + const mcpConfig = cfg.mcp?.[mcpName] - const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider }) + if (!mcpConfig) { + throw new Error(`MCP server not found: ${mcpName}`) + } - return yield* Effect.tryPromise({ - try: () => { - const client = new Client({ name: "opencode", version: InstallationVersion }) - return client - .connect(transport) - .then(() => ({ authorizationUrl: "", oauthState, client }) satisfies AuthResult) - }, - catch: (error) => error, - }).pipe( - Effect.catch((error) => { - if (error instanceof UnauthorizedError && capturedUrl) { - pendingOAuthTransports.set(mcpName, transport) - return Effect.succeed({ authorizationUrl: capturedUrl.toString(), oauthState } satisfies AuthResult) - } - return Effect.die(error) - }), - ) - }) + if (!isMcpConfigured(mcpConfig)) { + throw new Error(`MCP server ${mcpName} is disabled or missing configuration`) + } - const authenticate = Effect.fn("MCP.authenticate")(function* (mcpName: string) { - const result = yield* startAuth(mcpName) - if (!result.authorizationUrl) { - const client = "client" in result ? result.client : undefined - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) { - yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore) - return { status: "failed", error: "MCP config not found after auth" } as Status - } + if (mcpConfig.type !== "remote") { + throw new Error(`MCP server ${mcpName} is not a remote server`) + } - const listed = client ? yield* defs(mcpName, client, mcpConfig.timeout) : undefined - if (!client || !listed) { - yield* Effect.tryPromise(() => client?.close() ?? Promise.resolve()).pipe(Effect.ignore) - return { status: "failed", error: "Failed to get tools" } as Status - } + if (mcpConfig.oauth === false) { + throw new Error(`MCP server ${mcpName} has OAuth explicitly disabled`) + } + + // Start the callback server + await McpOAuthCallback.ensureRunning() + + // Generate and store a cryptographically secure state parameter BEFORE creating the provider + // The SDK will call provider.state() to read this value + const oauthState = Array.from(crypto.getRandomValues(new Uint8Array(32))) + .map((b) => b.toString(16).padStart(2, "0")) + .join("") + await McpAuth.updateOAuthState(mcpName, oauthState) + + // Create a new auth provider for this flow + // OAuth config is optional - if not provided, we'll use auto-discovery + const oauthConfig = typeof mcpConfig.oauth === "object" ? mcpConfig.oauth : undefined + let capturedUrl: URL | undefined + const authProvider = new McpOAuthProvider( + mcpName, + mcpConfig.url, + { + clientId: oauthConfig?.clientId, + clientSecret: oauthConfig?.clientSecret, + scope: oauthConfig?.scope, + }, + { + onRedirect: async (url) => { + capturedUrl = url + }, + }, + ) - const s = yield* InstanceState.get(state) - yield* auth.clearOAuthState(mcpName) - return yield* storeClient(s, mcpName, client, listed, mcpConfig.timeout) + // Create transport with auth provider + const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { + authProvider, + }) + + // Try to connect - this will trigger the OAuth flow + try { + const client = new Client({ + name: "opencode", + version: Installation.VERSION, + }) + await client.connect(transport) + // If we get here, we're already authenticated + return { authorizationUrl: "" } + } catch (error) { + if (error instanceof UnauthorizedError && capturedUrl) { + // Store transport for finishAuth + pendingOAuthTransports.set(mcpName, transport) + return { authorizationUrl: capturedUrl.toString() } } + throw error + } + } - log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: result.oauthState }) + /** + * Complete OAuth authentication after user authorizes in browser. + * Opens the browser and waits for callback. + */ + export async function authenticate(mcpName: string): Promise { + const { authorizationUrl } = await startAuth(mcpName) + + if (!authorizationUrl) { + // Already authenticated + const s = await state() + return s.status[mcpName] ?? { status: "connected" } + } - const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName) + // Get the state that was already generated and stored in startAuth() + const oauthState = await McpAuth.getOAuthState(mcpName) + if (!oauthState) { + throw new Error("OAuth state not found - this should not happen") + } - yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe( - Effect.flatMap((subprocess) => - Effect.callback((resume) => { - const timer = setTimeout(() => resume(Effect.void), 500) - subprocess.on("error", (err) => { - clearTimeout(timer) - resume(Effect.fail(err)) - }) - subprocess.on("exit", (code) => { - if (code !== null && code !== 0) { - clearTimeout(timer) - resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`))) - } - }) - }), - ), - Effect.catch(() => { - log.warn("failed to open browser, user must open URL manually", { mcpName }) - return bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore) - }), - ) + // The SDK has already added the state parameter to the authorization URL + // We just need to open the browser + log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState }) + + // Register the callback BEFORE opening the browser to avoid race condition + // when the IdP has an active SSO session and redirects immediately + const callbackPromise = McpOAuthCallback.waitForCallback(oauthState) + + try { + const subprocess = await open(authorizationUrl) + // The open package spawns a detached process and returns immediately. + // We need to listen for errors which fire asynchronously: + // - "error" event: command not found (ENOENT) + // - "exit" with non-zero code: command exists but failed (e.g., no display) + await new Promise((resolve, reject) => { + // Give the process a moment to fail if it's going to + const timeout = setTimeout(() => resolve(), 500) + subprocess.on("error", (error) => { + clearTimeout(timeout) + reject(error) + }) + subprocess.on("exit", (code) => { + if (code !== null && code !== 0) { + clearTimeout(timeout) + reject(new Error(`Browser open failed with exit code ${code}`)) + } + }) + }) + } catch (error) { + // Browser opening failed (e.g., in remote/headless sessions like SSH, devcontainers) + // Emit event so CLI can display the URL for manual opening + log.warn("failed to open browser, user must open URL manually", { mcpName, error }) + Bus.publish(BrowserOpenFailed, { mcpName, url: authorizationUrl }) + } - const code = yield* Effect.promise(() => callbackPromise) + // Wait for callback using the already-registered promise + const code = await callbackPromise - const storedState = yield* auth.getOAuthState(mcpName) - if (storedState !== result.oauthState) { - yield* auth.clearOAuthState(mcpName) - throw new Error("OAuth state mismatch - potential CSRF attack") - } - yield* auth.clearOAuthState(mcpName) - return yield* finishAuth(mcpName, code) - }) + // Validate and clear the state + const storedState = await McpAuth.getOAuthState(mcpName) + if (storedState !== oauthState) { + await McpAuth.clearOAuthState(mcpName) + throw new Error("OAuth state mismatch - potential CSRF attack") + } - const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) { - const transport = pendingOAuthTransports.get(mcpName) - if (!transport) throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`) + await McpAuth.clearOAuthState(mcpName) - const result = yield* Effect.tryPromise({ - try: () => transport.finishAuth(authorizationCode).then(() => true as const), - catch: (error) => { - log.error("failed to finish oauth", { mcpName, error }) - return error - }, - }).pipe(Effect.option) + // Finish auth + return finishAuth(mcpName, code) + } - if (Option.isNone(result)) { - return { status: "failed", error: "OAuth completion failed" } as Status - } + /** + * Complete OAuth authentication with the authorization code. + */ + export async function finishAuth(mcpName: string, authorizationCode: string): Promise { + const transport = pendingOAuthTransports.get(mcpName) - yield* auth.clearCodeVerifier(mcpName) - pendingOAuthTransports.delete(mcpName) + if (!transport) { + throw new Error(`No pending OAuth flow for MCP server: ${mcpName}`) + } - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) return { status: "failed", error: "MCP config not found after auth" } as Status + try { + // Call finishAuth on the transport + await transport.finishAuth(authorizationCode) - return yield* createAndStore(mcpName, mcpConfig) - }) + // Clear the code verifier after successful auth + await McpAuth.clearCodeVerifier(mcpName) - const removeAuth = Effect.fn("MCP.removeAuth")(function* (mcpName: string) { - yield* auth.remove(mcpName) - McpOAuthCallback.cancelPending(mcpName) - pendingOAuthTransports.delete(mcpName) - log.info("removed oauth credentials", { mcpName }) - }) + // Now try to reconnect + const cfg = await Config.get() + const mcpConfig = cfg.mcp?.[mcpName] - const supportsOAuth = Effect.fn("MCP.supportsOAuth")(function* (mcpName: string) { - const mcpConfig = yield* getMcpConfig(mcpName) - if (!mcpConfig) return false - return mcpConfig.type === "remote" && mcpConfig.oauth !== false - }) + if (!mcpConfig) { + throw new Error(`MCP server not found: ${mcpName}`) + } - const hasStoredTokens = Effect.fn("MCP.hasStoredTokens")(function* (mcpName: string) { - const entry = yield* auth.get(mcpName) - return !!entry?.tokens - }) + if (!isMcpConfigured(mcpConfig)) { + throw new Error(`MCP server ${mcpName} is disabled or missing configuration`) + } - const getAuthStatus = Effect.fn("MCP.getAuthStatus")(function* (mcpName: string) { - const entry = yield* auth.get(mcpName) - if (!entry?.tokens) return "not_authenticated" as AuthStatus - const expired = yield* auth.isTokenExpired(mcpName) - return (expired ? "expired" : "authenticated") as AuthStatus - }) + // Re-add the MCP server to establish connection + pendingOAuthTransports.delete(mcpName) + const result = await add(mcpName, mcpConfig) - return Service.of({ - status, - clients, - tools, - prompts, - resources, - add, - connect, - disconnect, - getPrompt, - readResource, - startAuth, - authenticate, - finishAuth, - removeAuth, - supportsOAuth, - hasStoredTokens, - getAuthStatus, - }) - }), -) + const statusRecord = result.status as Record + return statusRecord[mcpName] ?? { status: "failed", error: "Unknown error after auth" } + } catch (error) { + log.error("failed to finish oauth", { mcpName, error }) + return { + status: "failed", + error: error instanceof Error ? error.message : String(error), + } + } + } -export type AuthStatus = "authenticated" | "expired" | "not_authenticated" + /** + * Remove OAuth credentials for an MCP server. + */ + export async function removeAuth(mcpName: string): Promise { + await McpAuth.remove(mcpName) + McpOAuthCallback.cancelPending(mcpName) + pendingOAuthTransports.delete(mcpName) + await McpAuth.clearOAuthState(mcpName) + log.info("removed oauth credentials", { mcpName }) + } -// --- Per-service runtime --- + /** + * Check if an MCP server supports OAuth (remote servers support OAuth by default unless explicitly disabled). + */ + export async function supportsOAuth(mcpName: string): Promise { + const cfg = await Config.get() + const mcpConfig = cfg.mcp?.[mcpName] + if (!mcpConfig) return false + if (!isMcpConfigured(mcpConfig)) return false + return mcpConfig.type === "remote" && mcpConfig.oauth !== false + } -export const defaultLayer = layer.pipe( - Layer.provide(McpAuth.layer), - Layer.provide(Bus.layer), - Layer.provide(Config.defaultLayer), - Layer.provide(CrossSpawnSpawner.defaultLayer), - Layer.provide(AppFileSystem.defaultLayer), -) + /** + * Check if an MCP server has stored OAuth tokens. + */ + export async function hasStoredTokens(mcpName: string): Promise { + const entry = await McpAuth.get(mcpName) + return !!entry?.tokens + } + + export type AuthStatus = "authenticated" | "expired" | "not_authenticated" -export * as MCP from "." + /** + * Get the authentication status for an MCP server. + */ + export async function getAuthStatus(mcpName: string): Promise { + const hasTokens = await hasStoredTokens(mcpName) + if (!hasTokens) return "not_authenticated" + const expired = await McpAuth.isTokenExpired(mcpName) + return expired ? "expired" : "authenticated" + } +} diff --git a/packages/opencode/src/npm/config.ts b/packages/opencode/src/npm/config.ts deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/opencode/src/npmcli-config.d.ts b/packages/opencode/src/npmcli-config.d.ts deleted file mode 100644 index c9b20517ad9f..000000000000 --- a/packages/opencode/src/npmcli-config.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare module "@npmcli/config" { - type Data = Record - type Where = "default" | "builtin" | "global" | "user" | "project" | "env" | "cli" - - namespace Config { - interface Options { - definitions: Data - shorthands: Record - npmPath: string - flatten?: (input: Data, flat?: Data) => Data - nerfDarts?: string[] - argv?: string[] - cwd?: string - env?: NodeJS.ProcessEnv - execPath?: string - platform?: NodeJS.Platform - warn?: boolean - } - } - - class Config { - constructor(input: Config.Options) - - readonly data: Map - readonly flat: Data - - load(): Promise - } - - export = Config -} - -declare module "@npmcli/config/lib/definitions" { - export const definitions: Record - export const shorthands: Record - export const flatten: (input: Record, flat?: Record) => Record - export const nerfDarts: string[] - export const proxyEnv: string[] -} - -declare module "@npmcli/config/lib/definitions/index.js" { - export * from "@npmcli/config/lib/definitions" -} diff --git a/packages/opencode/src/permission/index.ts b/packages/opencode/src/permission/index.ts index 05c832016db3..2dfa8e940d01 100644 --- a/packages/opencode/src/permission/index.ts +++ b/packages/opencode/src/permission/index.ts @@ -22,13 +22,14 @@ export const Action = Schema.Literals(["allow", "deny", "ask"]) .pipe(withStatics((s) => ({ zod: zod(s) }))) export type Action = Schema.Schema.Type -export class Rule extends Schema.Class("PermissionRule")({ +export const Rule = Schema.Struct({ permission: Schema.String, pattern: Schema.String, action: Action, -}) { - static readonly zod = zod(this) -} +}) + .annotate({ identifier: "PermissionRule" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Rule = Schema.Schema.Type export const Ruleset = Schema.mutable(Schema.Array(Rule)) .annotate({ identifier: "PermissionRuleset" }) @@ -288,18 +289,8 @@ function expand(pattern: string): string { } export function fromConfig(permission: ConfigPermission.Info) { - // Sort top-level keys so wildcard permissions (`*`, `mcp_*`) come before - // specific ones. Combined with `findLast` in evaluate(), this gives the - // intuitive semantic "specific tool rules override the `*` fallback" - // regardless of the user's JSON key order. Sub-pattern order inside a - // single permission key is preserved — only top-level keys are sorted. - const entries = Object.entries(permission).sort(([a], [b]) => { - const aWild = a.includes("*") - const bWild = b.includes("*") - return aWild === bWild ? 0 : aWild ? -1 : 1 - }) const ruleset: Ruleset = [] - for (const [key, value] of entries) { + for (const [key, value] of Object.entries(permission)) { if (typeof value === "string") { ruleset.push({ permission: key, action: value, pattern: "*" }) continue diff --git a/packages/opencode/src/plugin/codex.ts b/packages/opencode/src/plugin/codex.ts index e05111fc6ad7..337a4e91f0ad 100644 --- a/packages/opencode/src/plugin/codex.ts +++ b/packages/opencode/src/plugin/codex.ts @@ -1,7 +1,7 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import { Log } from "../util" import { Installation } from "../installation" -import { InstallationVersion } from "../installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { OAUTH_DUMMY_KEY } from "../auth" import os from "os" import { setTimeout as sleep } from "node:timers/promises" @@ -390,6 +390,16 @@ export async function CodexAuthPlugin(input: PluginInput): Promise { output: 0, cache: { read: 0, write: 0 }, } + + // gpt-5.5 models temporarily have restricted context window size for codex plans + if (model.id.includes("gpt-5.5")) { + model.limit = { + context: 400_000, + //@ts-expect-error incorrect type for v1 sdk but works + input: 272_000, + output: 128_000, + } + } } return { diff --git a/packages/opencode/src/plugin/github-copilot/copilot.ts b/packages/opencode/src/plugin/github-copilot/copilot.ts index 9b6f54459ddc..6f0e46402110 100644 --- a/packages/opencode/src/plugin/github-copilot/copilot.ts +++ b/packages/opencode/src/plugin/github-copilot/copilot.ts @@ -1,6 +1,6 @@ import type { Hooks, PluginInput } from "@opencode-ai/plugin" import type { Model } from "@opencode-ai/sdk/v2" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { iife } from "@/util/iife" import { Log } from "../../util" import { setTimeout as sleep } from "node:timers/promises" diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index dd2a784694df..762d38be36f0 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -9,10 +9,10 @@ import { Config } from "../config" import { Bus } from "../bus" import { Log } from "../util" import { createOpencodeClient } from "@opencode-ai/sdk" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { CodexAuthPlugin } from "./codex" import { Session } from "../session" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { CopilotAuthPlugin } from "./github-copilot/copilot" import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth" import { PoeAuthPlugin } from "opencode-poe-auth" diff --git a/packages/opencode/src/plugin/install.ts b/packages/opencode/src/plugin/install.ts index 0525a7ba0b09..a760483126aa 100644 --- a/packages/opencode/src/plugin/install.ts +++ b/packages/opencode/src/plugin/install.ts @@ -8,9 +8,9 @@ import { } from "jsonc-parser" import * as ConfigPaths from "@/config/paths" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" import { isRecord } from "@/util/record" import { parsePluginSpecifier, readPackageThemes, readPluginPackage, resolvePluginTarget } from "./shared" diff --git a/packages/opencode/src/plugin/loader.ts b/packages/opencode/src/plugin/loader.ts index e61612561bcd..f8da9d6a95b4 100644 --- a/packages/opencode/src/plugin/loader.ts +++ b/packages/opencode/src/plugin/loader.ts @@ -9,7 +9,7 @@ import { type PluginSource, } from "./shared" import { ConfigPlugin } from "@/config/plugin" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" export namespace PluginLoader { // A normalized plugin declaration derived from config before any filesystem or npm work happens. diff --git a/packages/opencode/src/plugin/meta.ts b/packages/opencode/src/plugin/meta.ts index 86ad8fbab126..4bc8f5772b49 100644 --- a/packages/opencode/src/plugin/meta.ts +++ b/packages/opencode/src/plugin/meta.ts @@ -1,10 +1,10 @@ import path from "path" import { fileURLToPath } from "url" -import { Flag } from "@/flag/flag" -import { Global } from "@/global" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "@/util" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" import { parsePluginSpecifier, pluginSource } from "./shared" diff --git a/packages/opencode/src/plugin/shared.ts b/packages/opencode/src/plugin/shared.ts index ca821216d45c..a930d5b2616b 100644 --- a/packages/opencode/src/plugin/shared.ts +++ b/packages/opencode/src/plugin/shared.ts @@ -4,7 +4,7 @@ import npa from "npm-package-arg" import semver from "semver" import { Filesystem } from "@/util" import { isRecord } from "@/util/record" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" // Old npm package names for plugins that are now built-in export const DEPRECATED_PLUGIN_PACKAGES = ["opencode-openai-codex-auth", "opencode-copilot-auth"] diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index 1c5109620467..cd2013674954 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -1,7 +1,7 @@ import { GlobalBus } from "@/bus/global" import { disposeInstance } from "@/effect/instance-registry" import { makeRuntime } from "@/effect/run-service" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { iife } from "@/util/iife" import { Log } from "@/util" import { LocalContext } from "../util" diff --git a/packages/opencode/src/project/project.ts b/packages/opencode/src/project/project.ts index 70a959064094..fc34a6296f1e 100644 --- a/packages/opencode/src/project/project.ts +++ b/packages/opencode/src/project/project.ts @@ -3,7 +3,7 @@ import { and, Database, eq } from "../storage" import { ProjectTable } from "./project.sql" import { SessionTable } from "../session/session.sql" import { Log } from "../util" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" import { which } from "../util/which" @@ -11,8 +11,8 @@ import { ProjectID } from "./schema" import { Effect, Layer, Path, Scope, Context, Stream, Types, Schema } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/project/vcs.ts b/packages/opencode/src/project/vcs.ts index e8c6ff2ac7a7..2fbab4f63c48 100644 --- a/packages/opencode/src/project/vcs.ts +++ b/packages/opencode/src/project/vcs.ts @@ -4,11 +4,12 @@ import path from "path" import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { FileWatcher } from "@/file/watcher" import { Git } from "@/git" import { Log } from "@/util" -import z from "zod" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "vcs" }) @@ -101,8 +102,8 @@ const compare = Effect.fnUntraced(function* ( ) }) -export const Mode = z.enum(["git", "branch"]) -export type Mode = z.infer +export const Mode = Schema.Literals(["git", "branch"]).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Mode = Schema.Schema.Type export const Event = { BranchUpdated: BusEvent.define( @@ -113,28 +114,24 @@ export const Event = { ), } -export const Info = z - .object({ - branch: z.string().optional(), - default_branch: z.string().optional(), - }) - .meta({ - ref: "VcsInfo", - }) -export type Info = z.infer - -export const FileDiff = z - .object({ - file: z.string(), - patch: z.string(), - additions: z.number(), - deletions: z.number(), - status: z.enum(["added", "deleted", "modified"]).optional(), - }) - .meta({ - ref: "VcsFileDiff", - }) -export type FileDiff = z.infer +export const Info = Schema.Struct({ + branch: Schema.optional(Schema.String), + default_branch: Schema.optional(Schema.String), +}) + .annotate({ identifier: "VcsInfo" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Schema.Schema.Type + +export const FileDiff = Schema.Struct({ + file: Schema.String, + patch: Schema.String, + additions: Schema.Number, + deletions: Schema.Number, + status: Schema.optional(Schema.Literals(["added", "deleted", "modified"])), +}) + .annotate({ identifier: "VcsFileDiff" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type FileDiff = Schema.Schema.Type export interface Interface { readonly init: () => Effect.Effect diff --git a/packages/opencode/src/provider/models.ts b/packages/opencode/src/provider/models.ts index 36c4d8c23c61..8d7d7b03b797 100644 --- a/packages/opencode/src/provider/models.ts +++ b/packages/opencode/src/provider/models.ts @@ -1,13 +1,13 @@ -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import path from "path" import { Schema } from "effect" import { Installation } from "../installation" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "../util" -import { Flock } from "@opencode-ai/shared/util/flock" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Flock } from "@opencode-ai/core/util/flock" +import { Hash } from "@opencode-ai/core/util/hash" // Try to import bundled snapshot (generated at build time) // Falls back to undefined in dev mode when snapshot doesn't exist diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d826f6b35050..1fe16c606999 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1,166 +1,176 @@ +import z from "zod" import os from "os" import fuzzysort from "fuzzysort" -import { Config } from "../config" +import { Config } from "../config/config" import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda" import { NoSuchModelError, type Provider as SDK } from "ai" -import { Log } from "../util" -import { Npm } from "../npm" -import { Hash } from "@opencode-ai/shared/util/hash" +import { Log } from "../util/log" +import { BunProc } from "../bun" +import { Hash } from "../util/hash" import { Plugin } from "../plugin" -import { type LanguageModelV3 } from "@ai-sdk/provider" -import * as ModelsDev from "./models" +import { NamedError } from "@opencode-ai/util/error" +import { ModelsDev } from "./models" import { Auth } from "../auth" import { Env } from "../env" -import { InstallationVersion } from "../installation/version" +import { Instance } from "../project/instance" import { Flag } from "../flag/flag" -import { zod } from "@/util/effect-zod" -import { namedSchemaError } from "@/util/named-schema-error" import { iife } from "@/util/iife" import { Global } from "../global" import path from "path" -import { pathToFileURL } from "url" -import { Effect, Layer, Context, Schema, Types } from "effect" -import { EffectBridge } from "@/effect" -import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { isRecord } from "@/util/record" -import { withStatics } from "@/util/schema" - -import * as ProviderTransform from "./transform" +import { Filesystem } from "../util/filesystem" + +// Direct imports for bundled providers +import { createAmazonBedrock, type AmazonBedrockProviderSettings } from "@ai-sdk/amazon-bedrock" +import { createAnthropic } from "@ai-sdk/anthropic" +import { createAzure } from "@ai-sdk/azure" +import { createGoogleGenerativeAI } from "@ai-sdk/google" +import { createVertex } from "@ai-sdk/google-vertex" +import { createVertexAnthropic } from "@ai-sdk/google-vertex/anthropic" +import { createOpenAI } from "@ai-sdk/openai" +import { createOpenAICompatible } from "@ai-sdk/openai-compatible" +import { createOpenRouter, type LanguageModelV2 } from "@openrouter/ai-sdk-provider" +import { createOpenaiCompatible as createGitHubCopilotOpenAICompatible } from "./sdk/copilot" +import { createXai } from "@ai-sdk/xai" +import { createMistral } from "@ai-sdk/mistral" +import { createGroq } from "@ai-sdk/groq" +import { createDeepInfra } from "@ai-sdk/deepinfra" +import { createCerebras } from "@ai-sdk/cerebras" +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 { createVercel } from "@ai-sdk/vercel" +import { + createGitLab, + VERSION as GITLAB_PROVIDER_VERSION, + isWorkflowModel, + discoverWorkflowModels, +} from "gitlab-ai-provider" +import { fromNodeProviderChain } from "@aws-sdk/credential-providers" +import { GoogleAuth } from "google-auth-library" +import { ProviderTransform } from "./transform" +import { Installation } from "../installation" import { ModelID, ProviderID } from "./schema" -const log = Log.create({ service: "provider" }) +export namespace Provider { + const log = Log.create({ service: "provider" }) -function shouldUseCopilotResponsesApi(modelID: string): boolean { - const match = /^gpt-(\d+)/.exec(modelID) - if (!match) return false - return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini") -} + function shouldUseCopilotResponsesApi(modelID: string): boolean { + const match = /^gpt-(\d+)/.exec(modelID) + if (!match) return false + return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini") + } -function wrapSSE(res: Response, ms: number, ctl: AbortController) { - if (typeof ms !== "number" || ms <= 0) return res - if (!res.body) return res - if (!res.headers.get("content-type")?.includes("text/event-stream")) return res - - const reader = res.body.getReader() - const body = new ReadableStream({ - async pull(ctrl) { - const part = await new Promise>>((resolve, reject) => { - const id = setTimeout(() => { - const err = new Error("SSE read timed out") - ctl.abort(err) - void reader.cancel(err) - reject(err) - }, ms) - - reader.read().then( - (part) => { - clearTimeout(id) - resolve(part) - }, - (err) => { - clearTimeout(id) + function wrapSSE(res: Response, ms: number, ctl: AbortController) { + if (typeof ms !== "number" || ms <= 0) return res + if (!res.body) return res + if (!res.headers.get("content-type")?.includes("text/event-stream")) return res + + const reader = res.body.getReader() + const body = new ReadableStream({ + async pull(ctrl) { + const part = await new Promise>>((resolve, reject) => { + const id = setTimeout(() => { + const err = new Error("SSE read timed out") + ctl.abort(err) + void reader.cancel(err) reject(err) - }, - ) - }) - - if (part.done) { - ctrl.close() - return - } - - ctrl.enqueue(part.value) - }, - async cancel(reason) { - ctl.abort(reason) - await reader.cancel(reason) - }, - }) + }, ms) + + reader.read().then( + (part) => { + clearTimeout(id) + resolve(part) + }, + (err) => { + clearTimeout(id) + reject(err) + }, + ) + }) - return new Response(body, { - headers: new Headers(res.headers), - status: res.status, - statusText: res.statusText, - }) -} + if (part.done) { + ctrl.close() + return + } -type BundledSDK = { - languageModel(modelId: string): LanguageModelV3 -} + ctrl.enqueue(part.value) + }, + async cancel(reason) { + ctl.abort(reason) + await reader.cancel(reason) + }, + }) -const BUNDLED_PROVIDERS: Record Promise<(opts: any) => BundledSDK>> = { - "@ai-sdk/amazon-bedrock": () => import("@ai-sdk/amazon-bedrock").then((m) => m.createAmazonBedrock), - "@ai-sdk/anthropic": () => import("@ai-sdk/anthropic").then((m) => m.createAnthropic), - "@ai-sdk/azure": () => import("@ai-sdk/azure").then((m) => m.createAzure), - "@ai-sdk/google": () => import("@ai-sdk/google").then((m) => m.createGoogleGenerativeAI), - "@ai-sdk/google-vertex": () => import("@ai-sdk/google-vertex").then((m) => m.createVertex), - "@ai-sdk/google-vertex/anthropic": () => - import("@ai-sdk/google-vertex/anthropic").then((m) => m.createVertexAnthropic), - "@ai-sdk/openai": () => import("@ai-sdk/openai").then((m) => m.createOpenAI), - "@ai-sdk/openai-compatible": () => import("@ai-sdk/openai-compatible").then((m) => m.createOpenAICompatible), - "@openrouter/ai-sdk-provider": () => import("@openrouter/ai-sdk-provider").then((m) => m.createOpenRouter), - "@ai-sdk/xai": () => import("@ai-sdk/xai").then((m) => m.createXai), - "@ai-sdk/mistral": () => import("@ai-sdk/mistral").then((m) => m.createMistral), - "@ai-sdk/groq": () => import("@ai-sdk/groq").then((m) => m.createGroq), - "@ai-sdk/deepinfra": () => import("@ai-sdk/deepinfra").then((m) => m.createDeepInfra), - "@ai-sdk/cerebras": () => import("@ai-sdk/cerebras").then((m) => m.createCerebras), - "@ai-sdk/cohere": () => import("@ai-sdk/cohere").then((m) => m.createCohere), - "@ai-sdk/gateway": () => import("@ai-sdk/gateway").then((m) => m.createGateway), - "@ai-sdk/togetherai": () => import("@ai-sdk/togetherai").then((m) => m.createTogetherAI), - "@ai-sdk/perplexity": () => import("@ai-sdk/perplexity").then((m) => m.createPerplexity), - "@ai-sdk/vercel": () => import("@ai-sdk/vercel").then((m) => m.createVercel), - "@ai-sdk/alibaba": () => import("@ai-sdk/alibaba").then((m) => m.createAlibaba), - "gitlab-ai-provider": () => import("gitlab-ai-provider").then((m) => m.createGitLab), - "@ai-sdk/github-copilot": () => import("./sdk/copilot").then((m) => m.createOpenaiCompatible), - "venice-ai-sdk-provider": () => import("venice-ai-sdk-provider").then((m) => m.createVenice), -} + return new Response(body, { + headers: new Headers(res.headers), + status: res.status, + statusText: res.statusText, + }) + } -type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise -type CustomVarsLoader = (options: Record) => Record -type CustomDiscoverModels = () => Promise> -type CustomLoader = (provider: Info) => Effect.Effect<{ - autoload: boolean - getModel?: CustomModelLoader - vars?: CustomVarsLoader - options?: Record - discoverModels?: CustomDiscoverModels -}> - -type CustomDep = { - auth: (id: string) => Effect.Effect - config: () => Effect.Effect - env: () => Effect.Effect> - get: (key: string) => Effect.Effect -} + const BUNDLED_PROVIDERS: Record SDK> = { + "@ai-sdk/amazon-bedrock": createAmazonBedrock, + "@ai-sdk/anthropic": createAnthropic, + "@ai-sdk/azure": createAzure, + "@ai-sdk/google": createGoogleGenerativeAI, + "@ai-sdk/google-vertex": createVertex, + "@ai-sdk/google-vertex/anthropic": createVertexAnthropic, + "@ai-sdk/openai": createOpenAI, + "@ai-sdk/openai-compatible": createOpenAICompatible, + "@openrouter/ai-sdk-provider": createOpenRouter, + "@ai-sdk/xai": createXai, + "@ai-sdk/mistral": createMistral, + "@ai-sdk/groq": createGroq, + "@ai-sdk/deepinfra": createDeepInfra, + "@ai-sdk/cerebras": createCerebras, + "@ai-sdk/cohere": createCohere, + "@ai-sdk/gateway": createGateway, + "@ai-sdk/togetherai": createTogetherAI, + "@ai-sdk/perplexity": createPerplexity, + "@ai-sdk/vercel": createVercel, + "gitlab-ai-provider": createGitLab, + // @ts-ignore (TODO: kill this code so we dont have to maintain it) + "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, + } -function useLanguageModel(sdk: any) { - return sdk.responses === undefined && sdk.chat === undefined -} + type CustomModelLoader = (sdk: any, modelID: string, options?: Record) => Promise + type CustomVarsLoader = (options: Record) => Record + type CustomDiscoverModels = () => Promise> + type CustomLoader = (provider: Info) => Promise<{ + autoload: boolean + getModel?: CustomModelLoader + vars?: CustomVarsLoader + options?: Record + discoverModels?: CustomDiscoverModels + }> + + function useLanguageModel(sdk: any) { + return sdk.responses === undefined && sdk.chat === undefined + } -function custom(dep: CustomDep): Record { - return { - anthropic: () => - Effect.succeed({ + const CUSTOM_LOADERS: Record = { + async anthropic() { + return { autoload: false, options: { headers: { "anthropic-beta": "interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14", }, }, - }), - opencode: Effect.fnUntraced(function* (input: Info) { - const env = yield* dep.env() - const hasKey = iife(() => { + } + }, + async opencode(input) { + const hasKey = await (async () => { + const env = Env.all() if (input.env.some((item) => env[item])) return true + if (await Auth.get(input.id)) return true + const config = await Config.get() + if (config.provider?.["opencode"]?.options?.apiKey) return true return false - }) - const ok = - hasKey || - Boolean(yield* dep.auth(input.id)) || - Boolean((yield* dep.config()).provider?.["opencode"]?.options?.apiKey) + })() - if (!ok) { + if (!hasKey) { for (const [key, value] of Object.entries(input.models)) { if (value.cost.input === 0) continue delete input.models[key] @@ -169,40 +179,42 @@ function custom(dep: CustomDep): Record { return { autoload: Object.keys(input.models).length > 0, - options: ok ? {} : { apiKey: "public" }, + options: hasKey ? {} : { apiKey: "public" }, } - }), - openai: () => - Effect.succeed({ + }, + openai: async () => { + return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { return sdk.responses(modelID) }, options: {}, - }), - xai: () => - Effect.succeed({ + } + }, + xai: async () => { + return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { return sdk.responses(modelID) }, options: {}, - }), - "github-copilot": () => - Effect.succeed({ + } + }, + "github-copilot": async () => { + return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { if (useLanguageModel(sdk)) return sdk.languageModel(modelID) return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) }, options: {}, - }), - azure: Effect.fnUntraced(function* (provider: Info) { - const env = yield* dep.env() + } + }, + azure: async (provider) => { const resource = iife(() => { const name = provider.options?.resourceName if (typeof name === "string" && name.trim() !== "") return name - return env["AZURE_RESOURCE_NAME"] + return Env.get("AZURE_RESOURCE_NAME") }) return { @@ -222,9 +234,9 @@ function custom(dep: CustomDep): Record { } }, } - }), - "azure-cognitive-services": Effect.fnUntraced(function* () { - const resourceName = yield* dep.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") + }, + "azure-cognitive-services": async () => { + const resourceName = Env.get("AZURE_COGNITIVE_SERVICES_RESOURCE_NAME") return { autoload: false, async getModel(sdk: any, modelID: string, options?: Record) { @@ -239,23 +251,24 @@ function custom(dep: CustomDep): Record { baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined, }, } - }), - "amazon-bedrock": Effect.fnUntraced(function* () { - const providerConfig = (yield* dep.config()).provider?.["amazon-bedrock"] - const auth = yield* dep.auth("amazon-bedrock") - const env = yield* dep.env() + }, + "amazon-bedrock": async () => { + const config = await Config.get() + const providerConfig = config.provider?.["amazon-bedrock"] + + const auth = await Auth.get("amazon-bedrock") // Region precedence: 1) config file, 2) env var, 3) default const configRegion = providerConfig?.options?.region - const envRegion = env["AWS_REGION"] + const envRegion = Env.get("AWS_REGION") const defaultRegion = configRegion ?? envRegion ?? "us-east-1" // Profile: config file takes precedence over env var const configProfile = providerConfig?.options?.profile - const envProfile = env["AWS_PROFILE"] + const envProfile = Env.get("AWS_PROFILE") const profile = configProfile ?? envProfile - const awsAccessKeyId = env["AWS_ACCESS_KEY_ID"] + const awsAccessKeyId = Env.get("AWS_ACCESS_KEY_ID") // TODO: Using process.env directly because Env.set only updates a process.env shallow copy, // until the scope of the Env API is clarified (test only or runtime?) @@ -269,7 +282,7 @@ function custom(dep: CustomDep): Record { return undefined }) - const awsWebIdentityTokenFile = env["AWS_WEB_IDENTITY_TOKEN_FILE"] + const awsWebIdentityTokenFile = Env.get("AWS_WEB_IDENTITY_TOKEN_FILE") const containerCreds = Boolean( process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI || process.env.AWS_CONTAINER_CREDENTIALS_FULL_URI, @@ -278,9 +291,7 @@ function custom(dep: CustomDep): Record { if (!profile && !awsAccessKeyId && !awsBearerToken && !awsWebIdentityTokenFile && !containerCreds) return { autoload: false } - const { fromNodeProviderChain } = yield* Effect.promise(() => import("@aws-sdk/credential-providers")) - - const providerOptions: Record = { + const providerOptions: AmazonBedrockProviderSettings = { region: defaultRegion, } @@ -388,30 +399,9 @@ function custom(dep: CustomDep): Record { return sdk.languageModel(modelID) }, } - }), - llmgateway: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "HTTP-Referer": "https://opencode.ai/", - "X-Title": "opencode", - "X-Source": "opencode", - }, - }, - }), - openrouter: () => - Effect.succeed({ - autoload: false, - options: { - headers: { - "HTTP-Referer": "https://opencode.ai/", - "X-Title": "opencode", - }, - }, - }), - nvidia: () => - Effect.succeed({ + }, + openrouter: async () => { + return { autoload: false, options: { headers: { @@ -419,9 +409,10 @@ function custom(dep: CustomDep): Record { "X-Title": "opencode", }, }, - }), - vercel: () => - Effect.succeed({ + } + }, + vercel: async () => { + return { autoload: false, options: { headers: { @@ -429,17 +420,20 @@ function custom(dep: CustomDep): Record { "x-title": "opencode", }, }, - }), - "google-vertex": Effect.fnUntraced(function* (provider: Info) { - const env = yield* dep.env() + } + }, + "google-vertex": async (provider) => { const project = - provider.options?.project ?? env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] + provider.options?.project ?? + Env.get("GOOGLE_CLOUD_PROJECT") ?? + Env.get("GCP_PROJECT") ?? + Env.get("GCLOUD_PROJECT") const location = String( provider.options?.location ?? - env["GOOGLE_VERTEX_LOCATION"] ?? - env["GOOGLE_CLOUD_LOCATION"] ?? - env["VERTEX_LOCATION"] ?? + Env.get("GOOGLE_VERTEX_LOCATION") ?? + Env.get("GOOGLE_CLOUD_LOCATION") ?? + Env.get("VERTEX_LOCATION") ?? "us-central1", ) @@ -459,7 +453,6 @@ function custom(dep: CustomDep): Record { project, location, fetch: async (input: RequestInfo | URL, init?: RequestInit) => { - const { GoogleAuth } = await import("google-auth-library") const auth = new GoogleAuth() const client = await auth.getApplicationDefault() const token = await client.credential.getAccessToken() @@ -475,11 +468,10 @@ function custom(dep: CustomDep): Record { return sdk.languageModel(id) }, } - }), - "google-vertex-anthropic": Effect.fnUntraced(function* () { - const env = yield* dep.env() - const project = env["GOOGLE_CLOUD_PROJECT"] ?? env["GCP_PROJECT"] ?? env["GCLOUD_PROJECT"] - const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global" + }, + "google-vertex-anthropic": async () => { + const project = Env.get("GOOGLE_CLOUD_PROJECT") ?? Env.get("GCP_PROJECT") ?? Env.get("GCLOUD_PROJECT") + const location = Env.get("GOOGLE_CLOUD_LOCATION") ?? Env.get("VERTEX_LOCATION") ?? "global" const autoload = Boolean(project) if (!autoload) return { autoload: false } return { @@ -493,9 +485,9 @@ function custom(dep: CustomDep): Record { return sdk.languageModel(id) }, } - }), - "sap-ai-core": Effect.fnUntraced(function* () { - const auth = yield* dep.auth("sap-ai-core") + }, + "sap-ai-core": async () => { + const auth = await Auth.get("sap-ai-core") // TODO: Using process.env directly because Env.set only updates a shallow copy (not process.env), // until the scope of the Env API is clarified (test only or runtime?) const envServiceKey = iife(() => { @@ -517,9 +509,9 @@ function custom(dep: CustomDep): Record { return sdk(modelID) }, } - }), - zenmux: () => - Effect.succeed({ + }, + zenmux: async () => { + return { autoload: false, options: { headers: { @@ -527,57 +519,48 @@ function custom(dep: CustomDep): Record { "X-Title": "opencode", }, }, - }), - gitlab: Effect.fnUntraced(function* (input: Info) { - const { - VERSION: GITLAB_PROVIDER_VERSION, - isWorkflowModel, - discoverWorkflowModels, - } = yield* Effect.promise(() => import("gitlab-ai-provider")) - - const instanceUrl = (yield* dep.get("GITLAB_INSTANCE_URL")) || "https://gitlab.com" + } + }, + gitlab: async (input) => { + const instanceUrl = Env.get("GITLAB_INSTANCE_URL") || "https://gitlab.com" - const auth = yield* dep.auth(input.id) - const apiKey = yield* Effect.sync(() => { + const auth = await Auth.get(input.id) + const apiKey = await (async () => { if (auth?.type === "oauth") return auth.access if (auth?.type === "api") return auth.key - return undefined - }) - const token = apiKey ?? (yield* dep.get("GITLAB_TOKEN")) + return Env.get("GITLAB_TOKEN") + })() - const providerConfig = (yield* dep.config()).provider?.["gitlab"] - const directory = yield* InstanceState.directory + const config = await Config.get() + const providerConfig = config.provider?.["gitlab"] const aiGatewayHeaders = { - "User-Agent": `opencode/${InstallationVersion} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, + "User-Agent": `opencode/${Installation.VERSION} gitlab-ai-provider/${GITLAB_PROVIDER_VERSION} (${os.platform()} ${os.release()}; ${os.arch()})`, "anthropic-beta": "context-1m-2025-08-07", - ...providerConfig?.options?.aiGatewayHeaders, + ...(providerConfig?.options?.aiGatewayHeaders || {}), } const featureFlags = { duo_agent_platform_agentic_chat: true, duo_agent_platform: true, - ...providerConfig?.options?.featureFlags, + ...(providerConfig?.options?.featureFlags || {}), } return { - autoload: !!token, + autoload: !!apiKey, options: { instanceUrl, - apiKey: token, + apiKey, aiGatewayHeaders, featureFlags, }, - async getModel(sdk: any, modelID: string, options?: Record) { + async getModel(sdk: ReturnType, modelID: string, options?: Record) { if (modelID.startsWith("duo-workflow-")) { - const workflowRef = typeof options?.workflowRef === "string" ? options.workflowRef : undefined + const workflowRef = options?.workflowRef as string | undefined // Use the static mapping if it exists, otherwise use duo-workflow with selectedModelRef const sdkModelID = isWorkflowModel(modelID) ? modelID : "duo-workflow" - const workflowDefinition = - typeof options?.workflowDefinition === "string" ? options.workflowDefinition : undefined const model = sdk.workflowChat(sdkModelID, { featureFlags, - workflowDefinition, }) if (workflowRef) { model.selectedModelRef = workflowRef @@ -601,16 +584,14 @@ function custom(dep: CustomDep): Record { auth?.type === "api" ? { "PRIVATE-TOKEN": token } : { Authorization: `Bearer ${token}` } log.info("gitlab model discovery starting", { instanceUrl }) - const result = await discoverWorkflowModels({ instanceUrl, getHeaders }, { workingDirectory: directory }) + const result = await discoverWorkflowModels( + { instanceUrl, getHeaders }, + { workingDirectory: Instance.directory }, + ) if (!result.models.length) { log.info("gitlab model discovery skipped: no models found", { - project: result.project - ? { - id: result.project.id, - path: result.project.pathWithNamespace, - } - : null, + project: result.project ? { id: result.project.id, path: result.project.pathWithNamespace } : null, }) return {} } @@ -638,20 +619,8 @@ function custom(dep: CustomDep): Record { 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, - }, + input: { text: true, audio: false, image: true, video: false, pdf: true }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, interleaved: false, }, release_date: "", @@ -671,28 +640,15 @@ function custom(dep: CustomDep): Record { } }, } - }), - "cloudflare-workers-ai": Effect.fnUntraced(function* (input: Info) { - // When baseURL is already configured (e.g. corporate config routing through a proxy/gateway), - // skip the account ID check because the URL is already fully specified. - if (input.options?.baseURL) return { autoload: false } - - const auth = yield* dep.auth(input.id) - const env = yield* dep.env() - const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) - if (!accountId) - return { - autoload: false, - async getModel() { - throw new Error( - "CLOUDFLARE_ACCOUNT_ID is missing. Set it with: export CLOUDFLARE_ACCOUNT_ID=", - ) - }, - } + }, + "cloudflare-workers-ai": async (input) => { + const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID") + if (!accountId) return { autoload: false } - const apiKey = yield* Effect.gen(function* () { - const envToken = env["CLOUDFLARE_API_KEY"] + const apiKey = await iife(async () => { + const envToken = Env.get("CLOUDFLARE_API_KEY") if (envToken) return envToken + const auth = await Auth.get(input.id) if (auth?.type === "api") return auth.key return undefined }) @@ -701,9 +657,6 @@ function custom(dep: CustomDep): Record { autoload: !!apiKey, options: { apiKey, - headers: { - "User-Agent": `opencode/${InstallationVersion} cloudflare-workers-ai (${os.platform()} ${os.release()}; ${os.arch()})`, - }, }, async getModel(sdk: any, modelID: string) { return sdk.languageModel(modelID) @@ -714,38 +667,21 @@ function custom(dep: CustomDep): Record { } }, } - }), - "cloudflare-ai-gateway": Effect.fnUntraced(function* (input: Info) { - // When baseURL is already configured (e.g. corporate config), skip the ID checks. - if (input.options?.baseURL) return { autoload: false } - - const auth = yield* dep.auth(input.id) - const env = yield* dep.env() - const accountId = env["CLOUDFLARE_ACCOUNT_ID"] || (auth?.type === "api" ? auth.metadata?.accountId : undefined) - const gateway = env["CLOUDFLARE_GATEWAY_ID"] || (auth?.type === "api" ? auth.metadata?.gatewayId : undefined) - - if (!accountId || !gateway) { - const missing = [ - !accountId ? "CLOUDFLARE_ACCOUNT_ID" : undefined, - !gateway ? "CLOUDFLARE_GATEWAY_ID" : undefined, - ].filter((x): x is string => Boolean(x)) - return { - autoload: false, - async getModel() { - throw new Error( - `${missing.join(" and ")} missing. Set with: ${missing.map((x) => `export ${x}=`).join(" && ")}`, - ) - }, - } - } + }, + "cloudflare-ai-gateway": async (input) => { + const accountId = Env.get("CLOUDFLARE_ACCOUNT_ID") + const gateway = Env.get("CLOUDFLARE_GATEWAY_ID") + + if (!accountId || !gateway) return { autoload: false } // Get API token from env or auth - required for authenticated gateways - const apiToken = yield* Effect.gen(function* () { - const envToken = env["CLOUDFLARE_API_TOKEN"] || env["CF_AIG_TOKEN"] + const apiToken = await (async () => { + const envToken = Env.get("CLOUDFLARE_API_TOKEN") || Env.get("CF_AIG_TOKEN") if (envToken) return envToken + const auth = await Auth.get(input.id) if (auth?.type === "api") return auth.key return undefined - }) + })() if (!apiToken) { throw new Error( @@ -755,8 +691,8 @@ function custom(dep: CustomDep): Record { } // Use official ai-gateway-provider package (v2.x for AI SDK v5 compatibility) - const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")) - const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")) + const { createAiGateway } = await import("ai-gateway-provider") + const { createUnified } = await import("ai-gateway-provider/providers/unified") const metadata = iife(() => { if (input.options?.metadata) return input.options.metadata @@ -772,9 +708,6 @@ function custom(dep: CustomDep): Record { cacheKey: input.options?.cacheKey, skipCache: input.options?.skipCache, collectLog: input.options?.collectLog, - headers: { - "User-Agent": `opencode/${InstallationVersion} cloudflare-ai-gateway (${os.platform()} ${os.release()}; ${os.arch()})`, - }, } const aigateway = createAiGateway({ @@ -793,18 +726,19 @@ function custom(dep: CustomDep): Record { }, options: {}, } - }), - cerebras: () => - Effect.succeed({ + }, + cerebras: async () => { + return { autoload: false, options: { headers: { "X-Cerebras-3rd-Party-Integration": "opencode", }, }, - }), - kilo: () => - Effect.succeed({ + } + }, + kilo: async () => { + return { autoload: false, options: { headers: { @@ -812,796 +746,652 @@ function custom(dep: CustomDep): Record { "X-Title": "opencode", }, }, - }), - } -} - -const ProviderApiInfo = Schema.Struct({ - id: Schema.String, - url: Schema.String, - npm: Schema.String, -}) - -const ProviderModalities = Schema.Struct({ - text: Schema.Boolean, - audio: Schema.Boolean, - image: Schema.Boolean, - video: Schema.Boolean, - pdf: Schema.Boolean, -}) - -const ProviderInterleaved = Schema.Union([ - Schema.Boolean, - Schema.Struct({ - field: Schema.Literals(["reasoning_content", "reasoning_details"]), - }), -]) - -const ProviderCapabilities = Schema.Struct({ - temperature: Schema.Boolean, - reasoning: Schema.Boolean, - attachment: Schema.Boolean, - toolcall: Schema.Boolean, - input: ProviderModalities, - output: ProviderModalities, - interleaved: ProviderInterleaved, -}) - -const ProviderCacheCost = Schema.Struct({ - read: Schema.Number, - write: Schema.Number, -}) - -const ProviderCost = Schema.Struct({ - input: Schema.Number, - output: Schema.Number, - cache: ProviderCacheCost, - experimentalOver200K: Schema.optional( - Schema.Struct({ - input: Schema.Number, - output: Schema.Number, - cache: ProviderCacheCost, - }), - ), -}) - -const ProviderLimit = Schema.Struct({ - context: Schema.Number, - input: Schema.optional(Schema.Number), - output: Schema.Number, -}) - -export const Model = Schema.Struct({ - id: ModelID, - providerID: ProviderID, - api: ProviderApiInfo, - name: Schema.String, - family: Schema.optional(Schema.String), - capabilities: ProviderCapabilities, - cost: ProviderCost, - limit: ProviderLimit, - status: Schema.Literals(["alpha", "beta", "deprecated", "active"]), - options: Schema.Record(Schema.String, Schema.Any), - headers: Schema.Record(Schema.String, Schema.String), - release_date: Schema.String, - variants: Schema.optional(Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Any))), -}) - .annotate({ identifier: "Model" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Model = Types.DeepMutable> - -export const Info = Schema.Struct({ - id: ProviderID, - name: Schema.String, - source: Schema.Literals(["env", "config", "custom", "api"]), - env: Schema.Array(Schema.String), - key: Schema.optional(Schema.String), - options: Schema.Record(Schema.String, Schema.Any), - models: Schema.Record(Schema.String, Model), -}) - .annotate({ identifier: "Provider" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Info = Types.DeepMutable> - -const DefaultModelIDs = Schema.Record(Schema.String, Schema.String) - -export const ListResult = Schema.Struct({ - all: Schema.Array(Info), - default: DefaultModelIDs, - connected: Schema.Array(Schema.String), -}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ListResult = Types.DeepMutable> - -export const ConfigProvidersResult = Schema.Struct({ - providers: Schema.Array(Info), - default: DefaultModelIDs, -}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ConfigProvidersResult = Types.DeepMutable> - -export function defaultModelIDs }>(providers: Record) { - return mapValues(providers, (item) => sort(Object.values(item.models))[0].id) -} - -export interface Interface { - readonly list: () => Effect.Effect> - readonly getProvider: (providerID: ProviderID) => Effect.Effect - readonly getModel: (providerID: ProviderID, modelID: ModelID) => Effect.Effect - readonly getLanguage: (model: Model) => Effect.Effect - readonly closest: ( - providerID: ProviderID, - query: string[], - ) => Effect.Effect<{ providerID: ProviderID; modelID: string } | undefined> - readonly getSmallModel: (providerID: ProviderID) => Effect.Effect - readonly defaultModel: () => Effect.Effect<{ providerID: ProviderID; modelID: ModelID }> -} - -interface State { - models: Map - providers: Record - sdk: Map - modelLoaders: Record - varsLoaders: Record -} - -export class Service extends Context.Service()("@opencode/Provider") {} - -function cost(c: ModelsDev.Model["cost"]): Model["cost"] { - const result: Model["cost"] = { - input: c?.input ?? 0, - output: c?.output ?? 0, - cache: { - read: c?.cache_read ?? 0, - write: c?.cache_write ?? 0, + } }, } - if (c?.context_over_200k) { - result.experimentalOver200K = { - cache: { - read: c.context_over_200k.cache_read ?? 0, - write: c.context_over_200k.cache_write ?? 0, - }, - input: c.context_over_200k.input, - output: c.context_over_200k.output, - } - } - return result -} -function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { - const base: Model = { - id: ModelID.make(model.id), - providerID: ProviderID.make(provider.id), - name: model.name, - family: model.family, - api: { - id: model.id, - url: model.provider?.api ?? provider.api ?? "", - npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", - }, - status: model.status ?? "active", - headers: {}, - options: {}, - cost: cost(model.cost), - limit: { - context: model.limit.context, - input: model.limit.input, - output: model.limit.output, - }, - capabilities: { - temperature: model.temperature ?? false, - reasoning: model.reasoning ?? false, - attachment: model.attachment ?? false, - toolcall: model.tool_call ?? true, - input: { - text: model.modalities?.input?.includes("text") ?? false, - audio: model.modalities?.input?.includes("audio") ?? false, - image: model.modalities?.input?.includes("image") ?? false, - video: model.modalities?.input?.includes("video") ?? false, - pdf: model.modalities?.input?.includes("pdf") ?? false, + export const Model = z + .object({ + id: ModelID.zod, + providerID: ProviderID.zod, + api: z.object({ + id: z.string(), + url: z.string(), + npm: z.string(), + }), + name: z.string(), + family: z.string().optional(), + capabilities: z.object({ + temperature: z.boolean(), + reasoning: z.boolean(), + attachment: z.boolean(), + toolcall: z.boolean(), + input: z.object({ + text: z.boolean(), + audio: z.boolean(), + image: z.boolean(), + video: z.boolean(), + pdf: z.boolean(), + }), + output: z.object({ + text: z.boolean(), + audio: z.boolean(), + image: z.boolean(), + video: z.boolean(), + pdf: z.boolean(), + }), + interleaved: z.union([ + z.boolean(), + z.object({ + field: z.enum(["reasoning_content", "reasoning_details"]), + }), + ]), + }), + cost: z.object({ + input: z.number(), + output: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + experimentalOver200K: z + .object({ + input: z.number(), + output: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + }) + .optional(), + }), + limit: z.object({ + context: z.number(), + input: z.number().optional(), + output: z.number(), + }), + status: z.enum(["alpha", "beta", "deprecated", "active"]), + options: z.record(z.string(), z.any()), + headers: z.record(z.string(), z.string()), + release_date: z.string(), + variants: z.record(z.string(), z.record(z.string(), z.any())).optional(), + }) + .meta({ + ref: "Model", + }) + export type Model = z.infer + + export const Info = z + .object({ + id: ProviderID.zod, + name: z.string(), + source: z.enum(["env", "config", "custom", "api"]), + env: z.string().array(), + key: z.string().optional(), + options: z.record(z.string(), z.any()), + models: z.record(z.string(), Model), + }) + .meta({ + ref: "Provider", + }) + export type Info = z.infer + + function fromModelsDevModel(provider: ModelsDev.Provider, model: ModelsDev.Model): Model { + const m: Model = { + id: ModelID.make(model.id), + providerID: ProviderID.make(provider.id), + name: model.name, + family: model.family, + api: { + id: model.id, + url: model.provider?.api ?? provider.api!, + npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", }, - output: { - text: model.modalities?.output?.includes("text") ?? false, - audio: model.modalities?.output?.includes("audio") ?? false, - image: model.modalities?.output?.includes("image") ?? false, - video: model.modalities?.output?.includes("video") ?? false, - pdf: model.modalities?.output?.includes("pdf") ?? false, + status: model.status ?? "active", + headers: model.headers ?? {}, + options: model.options ?? {}, + cost: { + input: model.cost?.input ?? 0, + output: model.cost?.output ?? 0, + cache: { + read: model.cost?.cache_read ?? 0, + write: model.cost?.cache_write ?? 0, + }, + experimentalOver200K: model.cost?.context_over_200k + ? { + cache: { + read: model.cost.context_over_200k.cache_read ?? 0, + write: model.cost.context_over_200k.cache_write ?? 0, + }, + input: model.cost.context_over_200k.input, + output: model.cost.context_over_200k.output, + } + : undefined, }, - interleaved: model.interleaved ?? false, - }, - release_date: model.release_date ?? "", - variants: {}, - } + limit: { + context: model.limit.context, + input: model.limit.input, + output: model.limit.output, + }, + capabilities: { + temperature: model.temperature, + reasoning: model.reasoning, + attachment: model.attachment, + toolcall: model.tool_call, + input: { + text: model.modalities?.input?.includes("text") ?? false, + audio: model.modalities?.input?.includes("audio") ?? false, + image: model.modalities?.input?.includes("image") ?? false, + video: model.modalities?.input?.includes("video") ?? false, + pdf: model.modalities?.input?.includes("pdf") ?? false, + }, + output: { + text: model.modalities?.output?.includes("text") ?? false, + audio: model.modalities?.output?.includes("audio") ?? false, + image: model.modalities?.output?.includes("image") ?? false, + video: model.modalities?.output?.includes("video") ?? false, + pdf: model.modalities?.output?.includes("pdf") ?? false, + }, + interleaved: model.interleaved ?? false, + }, + release_date: model.release_date, + variants: {}, + } - return { - ...base, - variants: mapValues(ProviderTransform.variants(base), (v) => v), + m.variants = mapValues(ProviderTransform.variants(m), (v) => v) + + return m } -} -export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { - const models: Record = {} - for (const [key, model] of Object.entries(provider.models)) { - models[key] = fromModelsDevModel(provider, model) - for (const [mode, opts] of Object.entries(model.experimental?.modes ?? {})) { - const id = `${model.id}-${mode}` - const base = fromModelsDevModel(provider, model) - models[id] = { - ...base, - id: ModelID.make(id), - name: `${model.name} ${mode[0].toUpperCase()}${mode.slice(1)}`, - cost: opts.cost ? mergeDeep(base.cost, cost(opts.cost)) : base.cost, - options: opts.provider?.body - ? Object.fromEntries( - Object.entries(opts.provider.body).map(([k, v]) => [ - k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), - v, - ]), - ) - : base.options, - headers: opts.provider?.headers ?? base.headers, - } + export function fromModelsDevProvider(provider: ModelsDev.Provider): Info { + return { + id: ProviderID.make(provider.id), + source: "custom", + name: provider.name, + env: provider.env ?? [], + options: {}, + models: mapValues(provider.models, (model) => fromModelsDevModel(provider, model)), } } - return { - id: ProviderID.make(provider.id), - source: "custom", - name: provider.name, - env: [...(provider.env ?? [])], - options: {}, - models, - } -} -const layer: Layer.Layer< - Service, - never, - Config.Service | Auth.Service | Plugin.Service | AppFileSystem.Service | Env.Service -> = Layer.effect( - Service, - Effect.gen(function* () { - const fs = yield* AppFileSystem.Service - const config = yield* Config.Service - const auth = yield* Auth.Service - const env = yield* Env.Service - const plugin = yield* Plugin.Service - - const state = yield* InstanceState.make(() => - Effect.gen(function* () { - using _ = log.time("state") - const bridge = yield* EffectBridge.make() - const cfg = yield* config.get() - const modelsDev = yield* Effect.promise(() => ModelsDev.get()) - const database = mapValues(modelsDev, fromModelsDevProvider) - - const providers: Record = {} as Record - const languages = new Map() - const modelLoaders: { - [providerID: string]: CustomModelLoader - } = {} - const varsLoaders: { - [providerID: string]: CustomVarsLoader - } = {} - const sdk = new Map() - const discoveryLoaders: { - [providerID: string]: CustomDiscoverModels - } = {} - const dep = { - auth: (id: string) => auth.get(id).pipe(Effect.orDie), - config: () => config.get(), - env: () => env.all(), - get: (key: string) => env.get(key), - } + const state = Instance.state(async () => { + using _ = log.time("state") + const config = await Config.get() + const modelsDev = await ModelsDev.get() + const database = mapValues(modelsDev, fromModelsDevProvider) - log.info("init") + const disabled = new Set(config.disabled_providers ?? []) + const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null - function mergeProvider(providerID: ProviderID, provider: Partial) { - const existing = providers[providerID] - if (existing) { - // @ts-expect-error - providers[providerID] = mergeDeep(existing, provider) - return - } - const match = database[providerID] - if (!match) return - // @ts-expect-error - providers[providerID] = mergeDeep(match, provider) - } + function isProviderAllowed(providerID: ProviderID): boolean { + if (enabled && !enabled.has(providerID)) return false + if (disabled.has(providerID)) return false + return true + } - // load plugins first so config() hook runs before reading cfg.provider - const plugins = yield* plugin.list() + const providers: Record = {} as Record + const languages = new Map() + const modelLoaders: { + [providerID: string]: CustomModelLoader + } = {} + const varsLoaders: { + [providerID: string]: CustomVarsLoader + } = {} + const discoveryLoaders: { + [providerID: string]: CustomDiscoverModels + } = {} + const sdk = new Map() + + log.info("init") + + const configProviders = Object.entries(config.provider ?? {}) + + function mergeProvider(providerID: ProviderID, provider: Partial) { + const existing = providers[providerID] + if (existing) { + // @ts-expect-error + providers[providerID] = mergeDeep(existing, provider) + return + } + const match = database[providerID] + if (!match) return + // @ts-expect-error + providers[providerID] = mergeDeep(match, provider) + } - // now read config providers - includes any modifications from plugin config() hook - const configProviders = Object.entries(cfg.provider ?? {}) - const disabled = new Set(cfg.disabled_providers ?? []) - const enabled = cfg.enabled_providers ? new Set(cfg.enabled_providers) : null + // extend database from config + for (const [providerID, provider] of configProviders) { + const existing = database[providerID] + const parsed: Info = { + id: ProviderID.make(providerID), + name: provider.name ?? existing?.name ?? providerID, + env: provider.env ?? existing?.env ?? [], + options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), + source: "config", + models: existing?.models ?? {}, + } - function isProviderAllowed(providerID: ProviderID): boolean { - if (enabled && !enabled.has(providerID)) return false - if (disabled.has(providerID)) return false - return true + for (const [modelID, model] of Object.entries(provider.models ?? {})) { + const existingModel = parsed.models[model.id ?? modelID] + const name = iife(() => { + if (model.name) return model.name + if (model.id && model.id !== modelID) return modelID + return existingModel?.name ?? modelID + }) + const parsedModel: Model = { + id: ModelID.make(modelID), + api: { + id: model.id ?? existingModel?.api.id ?? modelID, + npm: + model.provider?.npm ?? + provider.npm ?? + existingModel?.api.npm ?? + modelsDev[providerID]?.npm ?? + "@ai-sdk/openai-compatible", + url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api, + }, + status: model.status ?? existingModel?.status ?? "active", + name, + providerID: ProviderID.make(providerID), + capabilities: { + temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, + reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, + attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, + toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, + input: { + text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, + audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, + image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, + video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, + pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, + }, + output: { + text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, + audio: model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, + image: model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, + video: model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, + pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, + }, + interleaved: model.interleaved ?? false, + }, + cost: { + input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, + output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, + cache: { + read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, + write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, + }, + }, + options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}), + limit: { + context: model.limit?.context ?? existingModel?.limit?.context ?? 0, + output: model.limit?.output ?? existingModel?.limit?.output ?? 0, + }, + headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), + family: model.family ?? existingModel?.family ?? "", + release_date: model.release_date ?? existingModel?.release_date ?? "", + variants: {}, } + const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) + parsedModel.variants = mapValues( + pickBy(merged, (v) => !v.disabled), + (v) => omit(v, ["disabled"]), + ) + parsed.models[modelID] = parsedModel + } + database[providerID] = parsed + } - // extend database from config - for (const [providerID, provider] of configProviders) { - const existing = database[providerID] - const parsed: Info = { - id: ProviderID.make(providerID), - name: provider.name ?? existing?.name ?? providerID, - env: provider.env ?? existing?.env ?? [], - options: mergeDeep(existing?.options ?? {}, provider.options ?? {}), - source: "config", - models: existing?.models ?? {}, - } + // load env + const env = Env.all() + for (const [id, provider] of Object.entries(database)) { + const providerID = ProviderID.make(id) + if (disabled.has(providerID)) continue + const apiKey = provider.env.map((item) => env[item]).find(Boolean) + if (!apiKey) continue + mergeProvider(providerID, { + source: "env", + key: provider.env.length === 1 ? apiKey : undefined, + }) + } - for (const [modelID, model] of Object.entries(provider.models ?? {})) { - const existingModel = parsed.models[model.id ?? modelID] - const name = iife(() => { - if (model.name) return model.name - if (model.id && model.id !== modelID) return modelID - return existingModel?.name ?? modelID - }) - const parsedModel: Model = { - id: ModelID.make(modelID), - api: { - id: model.id ?? existingModel?.api.id ?? modelID, - npm: - model.provider?.npm ?? - provider.npm ?? - existingModel?.api.npm ?? - modelsDev[providerID]?.npm ?? - "@ai-sdk/openai-compatible", - url: model.provider?.api ?? provider?.api ?? existingModel?.api.url ?? modelsDev[providerID]?.api ?? "", - }, - status: model.status ?? existingModel?.status ?? "active", - name, - providerID: ProviderID.make(providerID), - capabilities: { - temperature: model.temperature ?? existingModel?.capabilities.temperature ?? false, - reasoning: model.reasoning ?? existingModel?.capabilities.reasoning ?? false, - attachment: model.attachment ?? existingModel?.capabilities.attachment ?? false, - toolcall: model.tool_call ?? existingModel?.capabilities.toolcall ?? true, - input: { - text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, - audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, - image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, - video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, - pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, - }, - output: { - text: model.modalities?.output?.includes("text") ?? existingModel?.capabilities.output.text ?? true, - audio: - model.modalities?.output?.includes("audio") ?? existingModel?.capabilities.output.audio ?? false, - image: - model.modalities?.output?.includes("image") ?? existingModel?.capabilities.output.image ?? false, - video: - model.modalities?.output?.includes("video") ?? existingModel?.capabilities.output.video ?? false, - pdf: model.modalities?.output?.includes("pdf") ?? existingModel?.capabilities.output.pdf ?? false, - }, - interleaved: model.interleaved ?? false, - }, - cost: { - input: model?.cost?.input ?? existingModel?.cost?.input ?? 0, - output: model?.cost?.output ?? existingModel?.cost?.output ?? 0, - cache: { - read: model?.cost?.cache_read ?? existingModel?.cost?.cache.read ?? 0, - write: model?.cost?.cache_write ?? existingModel?.cost?.cache.write ?? 0, - }, - }, - options: mergeDeep(existingModel?.options ?? {}, model.options ?? {}), - limit: { - context: model.limit?.context ?? existingModel?.limit?.context ?? 0, - input: model.limit?.input ?? existingModel?.limit?.input, - output: model.limit?.output ?? existingModel?.limit?.output ?? 0, - }, - headers: mergeDeep(existingModel?.headers ?? {}, model.headers ?? {}), - family: model.family ?? existingModel?.family ?? "", - release_date: model.release_date ?? existingModel?.release_date ?? "", - variants: {}, - } - const merged = mergeDeep(ProviderTransform.variants(parsedModel), model.variants ?? {}) - parsedModel.variants = mapValues( - pickBy(merged, (v) => !v.disabled), - (v) => omit(v, ["disabled"]), - ) - parsed.models[modelID] = parsedModel - } - database[providerID] = parsed - } + // load apikeys + for (const [id, provider] of Object.entries(await Auth.all())) { + const providerID = ProviderID.make(id) + if (disabled.has(providerID)) continue + if (provider.type === "api") { + mergeProvider(providerID, { + source: "api", + key: provider.key, + }) + } + } - // load env - const envs = yield* env.all() - for (const [id, provider] of Object.entries(database)) { - const providerID = ProviderID.make(id) - if (disabled.has(providerID)) continue - const apiKey = provider.env.map((item) => envs[item]).find(Boolean) - if (!apiKey) continue - mergeProvider(providerID, { - source: "env", - key: provider.env.length === 1 ? apiKey : undefined, - }) - } + for (const plugin of await Plugin.list()) { + if (!plugin.auth) continue + const providerID = ProviderID.make(plugin.auth.provider) + if (disabled.has(providerID)) continue - // load apikeys - const auths = yield* auth.all().pipe(Effect.orDie) - for (const [id, provider] of Object.entries(auths)) { - const providerID = ProviderID.make(id) - if (disabled.has(providerID)) continue - if (provider.type === "api") { - mergeProvider(providerID, { - source: "api", - key: provider.key, - }) - } - } + const auth = await Auth.get(providerID) + if (!auth) continue + if (!plugin.auth.loader) continue - // plugin auth loader - database now has entries for config providers - for (const plugin of plugins) { - if (!plugin.auth) continue - const providerID = ProviderID.make(plugin.auth.provider) - if (disabled.has(providerID)) continue - - const stored = yield* auth.get(providerID).pipe(Effect.orDie) - if (!stored) continue - if (!plugin.auth.loader) continue - - const options = yield* Effect.promise(() => - plugin.auth!.loader!( - () => bridge.promise(auth.get(providerID).pipe(Effect.orDie)) as any, - database[plugin.auth!.provider], - ), - ) - const opts = options ?? {} - const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } - mergeProvider(providerID, patch) - } + if (auth) { + const options = await plugin.auth.loader(() => Auth.get(providerID) as any, database[plugin.auth.provider]) + const opts = options ?? {} + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } + mergeProvider(providerID, patch) + } + } - for (const [id, fn] of Object.entries(custom(dep))) { - const providerID = ProviderID.make(id) - if (disabled.has(providerID)) continue - const data = database[providerID] - if (!data) { - log.error("Provider does not exist in model list " + providerID) - continue - } - const result = yield* fn(data) - if (result && (result.autoload || providers[providerID])) { - if (result.getModel) modelLoaders[providerID] = result.getModel - if (result.vars) varsLoaders[providerID] = result.vars - if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels - const opts = result.options ?? {} - const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } - mergeProvider(providerID, patch) - } - } + for (const [id, fn] of Object.entries(CUSTOM_LOADERS)) { + const providerID = ProviderID.make(id) + if (disabled.has(providerID)) continue + const data = database[providerID] + if (!data) { + log.error("Provider does not exist in model list " + providerID) + continue + } + const result = await fn(data) + if (result && (result.autoload || providers[providerID])) { + if (result.getModel) modelLoaders[providerID] = result.getModel + if (result.vars) varsLoaders[providerID] = result.vars + if (result.discoverModels) discoveryLoaders[providerID] = result.discoverModels + const opts = result.options ?? {} + const patch: Partial = providers[providerID] ? { options: opts } : { source: "custom", options: opts } + mergeProvider(providerID, patch) + } + } - // load config - re-apply with updated data - for (const [id, provider] of configProviders) { - const providerID = ProviderID.make(id) - const partial: Partial = { source: "config" } - if (provider.env) partial.env = provider.env - if (provider.name) partial.name = provider.name - if (provider.options) partial.options = provider.options - mergeProvider(providerID, partial) - } + // load config + for (const [id, provider] of configProviders) { + const providerID = ProviderID.make(id) + const partial: Partial = { source: "config" } + if (provider.env) partial.env = provider.env + if (provider.name) partial.name = provider.name + if (provider.options) partial.options = provider.options + mergeProvider(providerID, partial) + } - const gitlab = ProviderID.make("gitlab") - if (discoveryLoaders[gitlab] && providers[gitlab] && isProviderAllowed(gitlab)) { - yield* Effect.promise(async () => { - try { - const discovered = await discoveryLoaders[gitlab]() - for (const [modelID, model] of Object.entries(discovered)) { - if (!providers[gitlab].models[modelID]) { - providers[gitlab].models[modelID] = model - } - } - } catch (e) { - log.warn("state discovery error", { id: "gitlab", error: e }) - } - }) - } + for (const [id, provider] of Object.entries(providers)) { + const providerID = ProviderID.make(id) + if (!isProviderAllowed(providerID)) { + delete providers[providerID] + continue + } - for (const hook of plugins) { - const p = hook.provider - const models = p?.models - if (!p || !models) continue - - const providerID = ProviderID.make(p.id) - if (disabled.has(providerID)) continue - - const provider = providers[providerID] - if (!provider) continue - const pluginAuth = yield* auth.get(providerID).pipe(Effect.orDie) - - provider.models = yield* Effect.promise(async () => { - const next = await models(provider, { auth: pluginAuth }) - return Object.fromEntries( - Object.entries(next).map(([id, model]) => [ - id, - { - ...model, - id: ModelID.make(id), - providerID, - }, - ]), - ) - }) - } + const configProvider = config.provider?.[providerID] - for (const [id, provider] of Object.entries(providers)) { - const providerID = ProviderID.make(id) - if (!isProviderAllowed(providerID)) { - delete providers[providerID] - continue - } + for (const [modelID, model] of Object.entries(provider.models)) { + model.api.id = model.api.id ?? model.id ?? modelID + if ( + modelID === "gpt-5-chat-latest" || + (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat") + ) + delete provider.models[modelID] + if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID] + if (model.status === "deprecated") delete provider.models[modelID] + if ( + (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || + (configProvider?.whitelist && !configProvider.whitelist.includes(modelID)) + ) + delete provider.models[modelID] - const configProvider = cfg.provider?.[providerID] + model.variants = mapValues(ProviderTransform.variants(model), (v) => v) - for (const [modelID, model] of Object.entries(provider.models)) { - model.api.id = model.api.id ?? model.id ?? modelID - if ( - modelID === "gpt-5-chat-latest" || - (providerID === ProviderID.openrouter && modelID === "openai/gpt-5-chat") - ) - delete provider.models[modelID] - if (model.status === "alpha" && !Flag.OPENCODE_ENABLE_EXPERIMENTAL_MODELS) delete provider.models[modelID] - if (model.status === "deprecated") delete provider.models[modelID] - if ( - (configProvider?.blacklist && configProvider.blacklist.includes(modelID)) || - (configProvider?.whitelist && !configProvider.whitelist.includes(modelID)) - ) - delete provider.models[modelID] + // Filter out disabled variants from config + const configVariants = configProvider?.models?.[modelID]?.variants + if (configVariants && model.variants) { + const merged = mergeDeep(model.variants, configVariants) + model.variants = mapValues( + pickBy(merged, (v) => !v.disabled), + (v) => omit(v, ["disabled"]), + ) + } + } - model.variants = mapValues(ProviderTransform.variants(model), (v) => v) + if (Object.keys(provider.models).length === 0) { + delete providers[providerID] + continue + } - const configVariants = configProvider?.models?.[modelID]?.variants - if (configVariants && model.variants) { - const merged = mergeDeep(model.variants, configVariants) - model.variants = mapValues( - pickBy(merged, (v) => !v.disabled), - (v) => omit(v, ["disabled"]), - ) - } - } + log.info("found", { providerID }) + } - if (Object.keys(provider.models).length === 0) { - delete providers[providerID] - continue + const gitlab = ProviderID.make("gitlab") + if (discoveryLoaders[gitlab] && providers[gitlab]) { + await (async () => { + const discovered = await discoveryLoaders[gitlab]() + for (const [modelID, model] of Object.entries(discovered)) { + if (!providers[gitlab].models[modelID]) { + providers[gitlab].models[modelID] = model } - - log.info("found", { providerID }) } + })().catch((e) => log.warn("state discovery error", { id: "gitlab", error: e })) + } - return { - models: languages, - providers, - sdk, - modelLoaders, - varsLoaders, - } - }), - ) + return { + models: languages, + providers, + sdk, + modelLoaders, + varsLoaders, + } + }) - const list = Effect.fn("Provider.list")(() => InstanceState.use(state, (s) => s.providers)) + export async function list() { + return state().then((state) => state.providers) + } - async function resolveSDK(model: Model, s: State, envs: Record) { - try { - using _ = log.time("getSDK", { - providerID: model.providerID, - }) - const provider = s.providers[model.providerID] - const options = { ...provider.options } + async function getSDK(model: Model) { + try { + using _ = log.time("getSDK", { + providerID: model.providerID, + }) + const s = await state() + const provider = s.providers[model.providerID] + const options = { ...provider.options } - if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { - delete options.fetch - } + if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { + delete options.fetch + } - if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) { - options["includeUsage"] = true - } + if (model.api.npm.includes("@ai-sdk/openai-compatible") && options["includeUsage"] !== false) { + options["includeUsage"] = true + } - const baseURL = iife(() => { - let url = - typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url - if (!url) return - - const loader = s.varsLoaders[model.providerID] - if (loader) { - const vars = loader(options) - for (const [key, value] of Object.entries(vars)) { - const field = "${" + key + "}" - url = url.replaceAll(field, value) - } + const baseURL = iife(() => { + let url = + typeof options["baseURL"] === "string" && options["baseURL"] !== "" ? options["baseURL"] : model.api.url + if (!url) return + + // some models/providers have variable urls, ex: "https://${AZURE_RESOURCE_NAME}.services.ai.azure.com/anthropic/v1" + // We track this in models.dev, and then when we are resolving the baseURL + // we need to string replace that literal: "${AZURE_RESOURCE_NAME}" + const loader = s.varsLoaders[model.providerID] + if (loader) { + const vars = loader(options) + for (const [key, value] of Object.entries(vars)) { + const field = "${" + key + "}" + url = url.replaceAll(field, value) } + } - url = url.replace(/\$\{([^}]+)\}/g, (item, key) => { - const val = envs[String(key)] - return val ?? item - }) - return url + url = url.replace(/\$\{([^}]+)\}/g, (item, key) => { + const val = Env.get(String(key)) + return val ?? item }) + return url + }) - if (baseURL !== undefined) options["baseURL"] = baseURL - if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key - if (model.headers) - options["headers"] = { - ...options["headers"], - ...model.headers, - } + if (baseURL !== undefined) options["baseURL"] = baseURL + if (options["apiKey"] === undefined && provider.key) options["apiKey"] = provider.key + if (model.headers) + options["headers"] = { + ...options["headers"], + ...model.headers, + } - const key = Hash.fast( - JSON.stringify({ - providerID: model.providerID, - npm: model.api.npm, - options, - }), - ) - const existing = s.sdk.get(key) - if (existing) return existing - - const customFetch = options["fetch"] - const chunkTimeout = options["chunkTimeout"] - delete options["chunkTimeout"] - - options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { - const fetchFn = customFetch ?? fetch - const opts = init ?? {} - const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined - const signals: AbortSignal[] = [] - - if (opts.signal) signals.push(opts.signal) - if (chunkAbortCtl) signals.push(chunkAbortCtl.signal) - if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) - signals.push(AbortSignal.timeout(options["timeout"])) - - const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) - if (combined) opts.signal = combined - - // Strip openai itemId metadata following what codex does - if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") { - const body = JSON.parse(opts.body as string) - const isAzure = model.providerID.includes("azure") - const keepIds = isAzure && body.store === true - if (!keepIds && Array.isArray(body.input)) { - for (const item of body.input) { - if ("id" in item) { - delete item.id - } + const key = Hash.fast(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options })) + const existing = s.sdk.get(key) + if (existing) return existing + + const customFetch = options["fetch"] + const chunkTimeoutRaw = options["chunkTimeout"] + delete options["chunkTimeout"] + const chunkTimeout = typeof chunkTimeoutRaw === "number" && chunkTimeoutRaw > 0 ? chunkTimeoutRaw : 30_000 + + options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { + // Preserve custom fetch if it exists, wrap it with timeout logic + const fetchFn = customFetch ?? fetch + const opts = init ?? {} + const chunkAbortCtl = new AbortController() + const signals: AbortSignal[] = [] + + if (opts.signal) signals.push(opts.signal) + signals.push(chunkAbortCtl.signal) + if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) + signals.push(AbortSignal.timeout(options["timeout"])) + + const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) + if (combined) opts.signal = combined + + // Strip openai itemId metadata following what codex does + // Codex uses #[serde(skip_serializing)] on id fields for all item types: + // Message, Reasoning, FunctionCall, LocalShellCall, CustomToolCall, WebSearchCall + // IDs are only re-attached for Azure with store=true + if (model.api.npm === "@ai-sdk/openai" && opts.body && opts.method === "POST") { + const body = JSON.parse(opts.body as string) + const isAzure = model.providerID.includes("azure") + const keepIds = isAzure && body.store === true + if (!keepIds && Array.isArray(body.input)) { + for (const item of body.input) { + if ("id" in item) { + delete item.id } - opts.body = JSON.stringify(body) } + opts.body = JSON.stringify(body) } - - const res = await fetchFn(input, { - ...opts, - // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 - timeout: false, - }) - - if (!chunkAbortCtl) return res - return wrapSSE(res, chunkTimeout, chunkAbortCtl) - } - - const bundledLoader = BUNDLED_PROVIDERS[model.api.npm] - if (bundledLoader) { - log.info("using bundled provider", { - providerID: model.providerID, - pkg: model.api.npm, - }) - const factory = await bundledLoader() - const loaded = factory({ - name: model.providerID, - ...options, - }) - s.sdk.set(key, loaded) - return loaded as SDK } - let installedPath: string - if (!model.api.npm.startsWith("file://")) { - const item = await Npm.add(model.api.npm) - if (!item.entrypoint) throw new Error(`Package ${model.api.npm} has no import entrypoint`) - installedPath = item.entrypoint - } else { - log.info("loading local provider", { pkg: model.api.npm }) - installedPath = model.api.npm - } + const res = await fetchFn(input, { + ...opts, + // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 + timeout: false, + }) - // `installedPath` is a local entry path or an existing `file://` URL. Normalize - // only path inputs so Node on Windows accepts the dynamic import. - const importSpec = installedPath.startsWith("file://") ? installedPath : pathToFileURL(installedPath).href - const mod = await import(importSpec) + return wrapSSE(res, chunkTimeout, chunkAbortCtl) + } - const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] - const loaded = fn({ + const bundledFn = BUNDLED_PROVIDERS[model.api.npm] + if (bundledFn) { + log.info("using bundled provider", { providerID: model.providerID, pkg: model.api.npm }) + const loaded = bundledFn({ name: model.providerID, ...options, }) s.sdk.set(key, loaded) return loaded as SDK - } catch (e) { - throw new InitError({ providerID: model.providerID }, { cause: e }) } - } - - const getProvider = Effect.fn("Provider.getProvider")((providerID: ProviderID) => - InstanceState.use(state, (s) => s.providers[providerID]), - ) - const getModel = Effect.fn("Provider.getModel")(function* (providerID: ProviderID, modelID: ModelID) { - const s = yield* InstanceState.get(state) - const provider = s.providers[providerID] - if (!provider) { - const available = Object.keys(s.providers) - const matches = fuzzysort.go(providerID, available, { limit: 3, threshold: -10000 }) - throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) + let installedPath: string + if (!model.api.npm.startsWith("file://")) { + installedPath = await BunProc.install(model.api.npm, "latest") + } else { + log.info("loading local provider", { pkg: model.api.npm }) + installedPath = model.api.npm } - const info = provider.models[modelID] - if (!info) { - const available = Object.keys(provider.models) - const matches = fuzzysort.go(modelID, available, { limit: 3, threshold: -10000 }) - throw new ModelNotFoundError({ providerID, modelID, suggestions: matches.map((m) => m.target) }) - } - return info - }) + const mod = await import(installedPath) - const getLanguage = Effect.fn("Provider.getLanguage")(function* (model: Model) { - const s = yield* InstanceState.get(state) - const envs = yield* env.all() - const key = `${model.providerID}/${model.id}` - if (s.models.has(key)) return s.models.get(key)! + const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!] + const loaded = fn({ + name: model.providerID, + ...options, + }) + s.sdk.set(key, loaded) + return loaded as SDK + } catch (e) { + throw new InitError({ providerID: model.providerID }, { cause: e }) + } + } - return yield* Effect.promise(async () => { - const provider = s.providers[model.providerID] - const sdk = await resolveSDK(model, s, envs) + export async function getProvider(providerID: ProviderID) { + return state().then((s) => s.providers[providerID]) + } - try { - const language = s.modelLoaders[model.providerID] - ? await s.modelLoaders[model.providerID](sdk, model.api.id, { - ...provider.options, - ...model.options, - }) - : sdk.languageModel(model.api.id) - s.models.set(key, language) - return language - } catch (e) { - if (e instanceof NoSuchModelError) - throw new ModelNotFoundError( - { - modelID: model.id, - providerID: model.providerID, - }, - { cause: e }, - ) - throw e - } - }) - }) + export async function getModel(providerID: ProviderID, modelID: ModelID) { + const s = await state() + const provider = s.providers[providerID] + if (!provider) { + const availableProviders = Object.keys(s.providers) + const matches = fuzzysort.go(providerID, availableProviders, { limit: 3, threshold: -10000 }) + const suggestions = matches.map((m) => m.target) + throw new ModelNotFoundError({ providerID, modelID, suggestions }) + } - const closest = Effect.fn("Provider.closest")(function* (providerID: ProviderID, query: string[]) { - const s = yield* InstanceState.get(state) - const provider = s.providers[providerID] - if (!provider) return undefined - for (const item of query) { - for (const modelID of Object.keys(provider.models)) { - if (modelID.includes(item)) return { providerID, modelID } - } - } - return undefined - }) + const info = provider.models[modelID] + if (!info) { + const availableModels = Object.keys(provider.models) + const matches = fuzzysort.go(modelID, availableModels, { limit: 3, threshold: -10000 }) + const suggestions = matches.map((m) => m.target) + throw new ModelNotFoundError({ providerID, modelID, suggestions }) + } + return info + } - const getSmallModel = Effect.fn("Provider.getSmallModel")(function* (providerID: ProviderID) { - const cfg = yield* config.get() + export async function getLanguage(model: Model): Promise { + const s = await state() + const key = `${model.providerID}/${model.id}` + if (s.models.has(key)) return s.models.get(key)! + + const provider = s.providers[model.providerID] + const sdk = await getSDK(model) + + try { + const language = s.modelLoaders[model.providerID] + ? await s.modelLoaders[model.providerID](sdk, model.api.id, { ...provider.options, ...model.options }) + : sdk.languageModel(model.api.id) + s.models.set(key, language) + return language + } catch (e) { + if (e instanceof NoSuchModelError) + throw new ModelNotFoundError( + { + modelID: model.id, + providerID: model.providerID, + }, + { cause: e }, + ) + throw e + } + } - if (cfg.small_model) { - const parsed = parseModel(cfg.small_model) - return yield* getModel(parsed.providerID, parsed.modelID) + export async function closest(providerID: ProviderID, query: string[]) { + const s = await state() + const provider = s.providers[providerID] + if (!provider) return undefined + for (const item of query) { + for (const modelID of Object.keys(provider.models)) { + if (modelID.includes(item)) + return { + providerID, + modelID, + } } + } + } - const s = yield* InstanceState.get(state) - const provider = s.providers[providerID] - if (!provider) return undefined + export async function getSmallModel(providerID: ProviderID) { + const cfg = await Config.get() + if (cfg.small_model) { + const parsed = parseModel(cfg.small_model) + return getModel(parsed.providerID, parsed.modelID) + } + + const provider = await state().then((state) => state.providers[providerID]) + if (provider) { let priority = [ "claude-haiku-4-5", "claude-haiku-4.5", @@ -1615,6 +1405,7 @@ const layer: Layer.Layer< priority = ["gpt-5-nano"] } if (providerID.startsWith("github-copilot")) { + // prioritize free models for github copilot priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority] } for (const item of priority) { @@ -1622,102 +1413,93 @@ const layer: Layer.Layer< const crossRegionPrefixes = ["global.", "us.", "eu."] const candidates = Object.keys(provider.models).filter((m) => m.includes(item)) + // Model selection priority: + // 1. global. prefix (works everywhere) + // 2. User's region prefix (us., eu.) + // 3. Unprefixed model const globalMatch = candidates.find((m) => m.startsWith("global.")) - if (globalMatch) return yield* getModel(providerID, ModelID.make(globalMatch)) + if (globalMatch) return getModel(providerID, ModelID.make(globalMatch)) const region = provider.options?.region if (region) { const regionPrefix = region.split("-")[0] if (regionPrefix === "us" || regionPrefix === "eu") { const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`)) - if (regionalMatch) return yield* getModel(providerID, ModelID.make(regionalMatch)) + if (regionalMatch) return getModel(providerID, ModelID.make(regionalMatch)) } } const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p))) - if (unprefixed) return yield* getModel(providerID, ModelID.make(unprefixed)) + if (unprefixed) return getModel(providerID, ModelID.make(unprefixed)) } else { for (const model of Object.keys(provider.models)) { - if (model.includes(item)) return yield* getModel(providerID, ModelID.make(model)) + if (model.includes(item)) return getModel(providerID, ModelID.make(model)) } } } + } - return undefined - }) + return undefined + } - const defaultModel = Effect.fn("Provider.defaultModel")(function* () { - const cfg = yield* config.get() - if (cfg.model) return parseModel(cfg.model) - - const s = yield* InstanceState.get(state) - const recent = yield* fs.readJson(path.join(Global.Path.state, "model.json")).pipe( - Effect.map((x): { providerID: ProviderID; modelID: ModelID }[] => { - if (!isRecord(x) || !Array.isArray(x.recent)) return [] - return x.recent.flatMap((item) => { - if (!isRecord(item)) return [] - if (typeof item.providerID !== "string") return [] - if (typeof item.modelID !== "string") return [] - return [{ providerID: ProviderID.make(item.providerID), modelID: ModelID.make(item.modelID) }] - }) - }), - Effect.catch(() => Effect.succeed([] as { providerID: ProviderID; modelID: ModelID }[])), - ) - for (const entry of recent) { - const provider = s.providers[entry.providerID] - if (!provider) continue - if (!provider.models[entry.modelID]) continue - return { providerID: entry.providerID, modelID: entry.modelID } - } + const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] + export function sort(models: T[]) { + return sortBy( + models, + [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], + [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], + [(model) => model.id, "desc"], + ) + } - const provider = Object.values(s.providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)) - if (!provider) throw new Error("no providers found") - const [model] = sort(Object.values(provider.models)) - if (!model) throw new Error("no models found") - return { - providerID: provider.id, - modelID: model.id, - } - }) + export async function defaultModel() { + const cfg = await Config.get() + if (cfg.model) return parseModel(cfg.model) - return Service.of({ list, getProvider, getModel, getLanguage, closest, getSmallModel, defaultModel }) - }), -) - -export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(AppFileSystem.defaultLayer), - Layer.provide(Env.defaultLayer), - Layer.provide(Config.defaultLayer), - Layer.provide(Auth.defaultLayer), - Layer.provide(Plugin.defaultLayer), - ), -) - -const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"] -export function sort(models: T[]) { - return sortBy( - models, - [(model) => priority.findIndex((filter) => model.id.includes(filter)), "desc"], - [(model) => (model.id.includes("latest") ? 0 : 1), "asc"], - [(model) => model.id, "desc"], - ) -} + const providers = await list() + const recent = (await Filesystem.readJson<{ recent?: { providerID: ProviderID; modelID: ModelID }[] }>( + path.join(Global.Path.state, "model.json"), + ) + .then((x) => (Array.isArray(x.recent) ? x.recent : [])) + .catch(() => [])) as { providerID: ProviderID; modelID: ModelID }[] + for (const entry of recent) { + const provider = providers[entry.providerID] + if (!provider) continue + if (!provider.models[entry.modelID]) continue + return { providerID: entry.providerID, modelID: entry.modelID } + } -export function parseModel(model: string) { - const [providerID, ...rest] = model.split("/") - return { - providerID: ProviderID.make(providerID), - modelID: ModelID.make(rest.join("/")), + const provider = Object.values(providers).find((p) => !cfg.provider || Object.keys(cfg.provider).includes(p.id)) + if (!provider) throw new Error("no providers found") + const [model] = sort(Object.values(provider.models)) + if (!model) throw new Error("no models found") + return { + providerID: provider.id, + modelID: model.id, + } + } + + export function parseModel(model: string) { + const [providerID, ...rest] = model.split("/") + return { + providerID: ProviderID.make(providerID), + modelID: ModelID.make(rest.join("/")), + } } -} -export const ModelNotFoundError = namedSchemaError("ProviderModelNotFoundError", { - providerID: ProviderID, - modelID: ModelID, - suggestions: Schema.optional(Schema.Array(Schema.String)), -}) + export const ModelNotFoundError = NamedError.create( + "ProviderModelNotFoundError", + z.object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, + suggestions: z.array(z.string()).optional(), + }), + ) -export const InitError = namedSchemaError("ProviderInitError", { - providerID: ProviderID, -}) + export const InitError = NamedError.create( + "ProviderInitError", + z.object({ + providerID: ProviderID.zod, + }), + ) +} diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 1d84c7c93127..67b02c089602 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -5,7 +5,7 @@ import type { JSONSchema } from "zod/v4/core" import type * as Provider from "./provider" import type * as ModelsDev from "./models" import { iife } from "@/util/iife" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" type Modality = NonNullable["input"][number] @@ -175,6 +175,24 @@ function normalizeMessages( return result } + // Deepseek requires all assistant messages to have reasoning on them + if (model.api.id.includes("deepseek")) { + msgs = msgs.map((msg) => { + if (msg.role !== "assistant") return msg + if (Array.isArray(msg.content)) { + if (msg.content.some((part) => part.type === "reasoning")) return msg + return { ...msg, content: [...msg.content, { type: "reasoning", text: "" }] } + } + return { + ...msg, + content: [ + ...(msg.content ? [{ type: "text" as const, text: msg.content }] : []), + { type: "reasoning" as const, text: "" }, + ], + } + }) + } + if (typeof model.capabilities.interleaved === "object" && model.capabilities.interleaved.field) { const field = model.capabilities.interleaved.field return msgs.map((msg) => { @@ -185,24 +203,19 @@ function normalizeMessages( // Filter out reasoning parts from content const filteredContent = msg.content.filter((part: any) => part.type !== "reasoning") - // Include reasoning_content | reasoning_details directly on the message for all assistant messages - if (reasoningText) { - return { - ...msg, - content: filteredContent, - providerOptions: { - ...msg.providerOptions, - openaiCompatible: { - ...msg.providerOptions?.openaiCompatible, - [field]: reasoningText, - }, - }, - } - } - + // Include reasoning_content | reasoning_details directly on the message for all assistant messages. + // Always set the field even when empty — some providers (e.g. DeepSeek) may return empty + // reasoning_content which still needs to be sent back in subsequent requests. return { ...msg, content: filteredContent, + providerOptions: { + ...msg.providerOptions, + openaiCompatible: { + ...msg.providerOptions?.openaiCompatible, + [field]: reasoningText, + }, + }, } } @@ -405,7 +418,10 @@ export function variants(model: Provider.Model): Record [effort, { reasoningEffort: effort }])) + const efforts = [...WIDELY_SUPPORTED_EFFORTS] + if (model.api.id.includes("deepseek-v4")) { + efforts.push("max") + } + return Object.fromEntries(efforts.map((effort) => [effort, { reasoningEffort: effort }])) case "@ai-sdk/azure": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 604fa77fbb8a..918f4f86c63b 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -4,7 +4,7 @@ import { InstanceState } from "@/effect" import { Instance } from "@/project/instance" import type { Proc } from "#pty" import { Log } from "../util" -import { lazy } from "@opencode-ai/shared/util/lazy" +import { lazy } from "@opencode-ai/core/util/lazy" import { Shell } from "@/shell/shell" import { Plugin } from "@/plugin" import { PtyID } from "./schema" diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts index b67d15f55045..aceba8821f2b 100644 --- a/packages/opencode/src/server/middleware.ts +++ b/packages/opencode/src/server/middleware.ts @@ -1,12 +1,12 @@ import { Provider } from "../provider" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { NotFoundError } from "../storage" import { Session } from "../session" import type { ContentfulStatusCode } from "hono/utils/http-status" import type { ErrorHandler, MiddlewareHandler } from "hono" import { HTTPException } from "hono/http-exception" import { Log } from "../util" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { basicAuth } from "hono/basic-auth" import { cors } from "hono/cors" import { compress } from "hono/compress" diff --git a/packages/opencode/src/server/routes/global.ts b/packages/opencode/src/server/routes/global.ts index a1199a46911a..c2f8b695d2da 100644 --- a/packages/opencode/src/server/routes/global.ts +++ b/packages/opencode/src/server/routes/global.ts @@ -10,7 +10,7 @@ import { AppRuntime } from "@/effect/app-runtime" import { AsyncQueue } from "@/util/queue" import { Instance } from "../../project/instance" import { Installation } from "@/installation" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Log } from "../../util" import { lazy } from "../../util/lazy" import { Config } from "../../config" diff --git a/packages/opencode/src/server/routes/instance/experimental.ts b/packages/opencode/src/server/routes/instance/experimental.ts index f13003cb4e59..a407590f2ca4 100644 --- a/packages/opencode/src/server/routes/instance/experimental.ts +++ b/packages/opencode/src/server/routes/instance/experimental.ts @@ -230,14 +230,14 @@ export const ExperimentalRoutes = lazy(() => description: "Worktree created", content: { "application/json": { - schema: resolver(Worktree.Info), + schema: resolver(Worktree.Info.zod), }, }, }, ...errors(400), }, }), - validator("json", Worktree.CreateInput.optional()), + validator("json", Worktree.CreateInput.zod.optional()), async (c) => jsonRequest("ExperimentalRoutes.worktree.create", c, function* () { const body = c.req.valid("json") @@ -286,7 +286,7 @@ export const ExperimentalRoutes = lazy(() => ...errors(400), }, }), - validator("json", Worktree.RemoveInput), + validator("json", Worktree.RemoveInput.zod), async (c) => jsonRequest("ExperimentalRoutes.worktree.remove", c, function* () { const body = c.req.valid("json") @@ -315,7 +315,7 @@ export const ExperimentalRoutes = lazy(() => ...errors(400), }, }), - validator("json", Worktree.ResetInput), + validator("json", Worktree.ResetInput.zod), async (c) => jsonRequest("ExperimentalRoutes.worktree.reset", c, function* () { const body = c.req.valid("json") @@ -394,7 +394,7 @@ export const ExperimentalRoutes = lazy(() => description: "MCP resources", content: { "application/json": { - schema: resolver(z.record(z.string(), MCP.Resource)), + schema: resolver(z.record(z.string(), MCP.Resource.zod)), }, }, }, diff --git a/packages/opencode/src/server/routes/instance/file.ts b/packages/opencode/src/server/routes/instance/file.ts index f92fe6e7e5fd..65b3cbad3359 100644 --- a/packages/opencode/src/server/routes/instance/file.ts +++ b/packages/opencode/src/server/routes/instance/file.ts @@ -21,7 +21,7 @@ export const FileRoutes = lazy(() => description: "Matches", content: { "application/json": { - schema: resolver(Ripgrep.Match.shape.data.array()), + schema: resolver(Ripgrep.SearchMatch.zod.array()), }, }, }, @@ -117,7 +117,7 @@ export const FileRoutes = lazy(() => description: "Files and directories", content: { "application/json": { - schema: resolver(File.Node.array()), + schema: resolver(File.Node.zod.array()), }, }, }, @@ -146,7 +146,7 @@ export const FileRoutes = lazy(() => description: "File content", content: { "application/json": { - schema: resolver(File.Content), + schema: resolver(File.Content.zod), }, }, }, @@ -175,7 +175,7 @@ export const FileRoutes = lazy(() => description: "File status", content: { "application/json": { - schema: resolver(File.Info.array()), + schema: resolver(File.Info.zod.array()), }, }, }, diff --git a/packages/opencode/src/server/routes/instance/httpapi/auth.ts b/packages/opencode/src/server/routes/instance/httpapi/auth.ts new file mode 100644 index 000000000000..2fe196b5615a --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/auth.ts @@ -0,0 +1,71 @@ +import { Effect, Encoding, Layer, Redacted, Schema } from "effect" +import { HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" +import { Flag } from "@opencode-ai/core/flag/flag" + +class Unauthorized extends Schema.TaggedErrorClass()( + "Unauthorized", + { message: Schema.String }, + { httpApiStatus: 401 }, +) {} + +export class Authorization extends HttpApiMiddleware.Service()( + "@opencode/ExperimentalHttpApiAuthorization", + { + error: Unauthorized, + security: { + basic: HttpApiSecurity.basic, + authToken: HttpApiSecurity.apiKey({ in: "query", key: "auth_token" }), + }, + }, +) {} + +const emptyCredential = { + username: "", + password: Redacted.make(""), +} + +function validateCredential( + effect: Effect.Effect, + credential: { readonly username: string; readonly password: typeof emptyCredential.password }, +) { + return Effect.gen(function* () { + if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect + + if (credential.username !== (Flag.OPENCODE_SERVER_USERNAME ?? "opencode")) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) { + return yield* new Unauthorized({ message: "Unauthorized" }) + } + return yield* effect + }) +} + +function decodeCredential(input: string) { + return Encoding.decodeBase64String(input) + .asEffect() + .pipe( + Effect.match({ + onFailure: () => emptyCredential, + onSuccess: (header) => { + const parts = header.split(":") + if (parts.length !== 2) return emptyCredential + return { + username: parts[0], + password: Redacted.make(parts[1]), + } + }, + }), + ) +} + +export const authorizationLayer = Layer.succeed( + Authorization, + Authorization.of({ + basic: (effect, { credential }) => validateCredential(effect, credential), + authToken: (effect, { credential }) => + Effect.gen(function* () { + return yield* validateCredential(effect, yield* decodeCredential(Redacted.value(credential))) + }), + }), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/config.ts b/packages/opencode/src/server/routes/instance/httpapi/config.ts index 2dfdec172a51..7e0664b3d613 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/config.ts @@ -1,7 +1,10 @@ import { Config } from "@/config" import { Provider } from "@/provider" +import * as InstanceState from "@/effect/instance-state" import { Effect, Layer } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" +import { markInstanceForDisposal } from "./lifecycle" const root = "/config" @@ -18,6 +21,16 @@ export const ConfigApi = HttpApi.make("config") description: "Retrieve the current OpenCode configuration settings and preferences.", }), ), + HttpApiEndpoint.patch("update", root, { + payload: Config.Info, + success: Config.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "config.update", + summary: "Update configuration", + description: "Update OpenCode configuration settings and preferences.", + }), + ), HttpApiEndpoint.get("providers", `${root}/providers`, { success: Provider.ConfigProvidersResult, }).annotateMerge( @@ -33,7 +46,8 @@ export const ConfigApi = HttpApi.make("config") title: "config", description: "Experimental HttpApi config routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ @@ -52,6 +66,13 @@ export const configHandlers = Layer.unwrap( return yield* configSvc.get() }) + const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) { + const payload = Config.Info.zod.parse(ctx.payload) + yield* configSvc.update(payload, { dispose: false }) + yield* markInstanceForDisposal(yield* InstanceState.context) + return payload + }) + const providers = Effect.fn("ConfigHttpApi.providers")(function* () { const providers = yield* providerSvc.list() return { @@ -61,7 +82,7 @@ export const configHandlers = Layer.unwrap( }) return HttpApiBuilder.group(ConfigApi, "config", (handlers) => - handlers.handle("get", get).handle("providers", providers), + handlers.handle("get", get).handle("update", update).handle("providers", providers), ) }), ).pipe(Layer.provide(Provider.defaultLayer), Layer.provide(Config.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/httpapi/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts new file mode 100644 index 000000000000..14f54d457afb --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/experimental.ts @@ -0,0 +1,240 @@ +import { Account } from "@/account/account" +import { Config } from "@/config" +import { InstanceState } from "@/effect" +import { MCP } from "@/mcp" +import { Project } from "@/project" +import { ToolRegistry } from "@/tool" +import { Worktree } from "@/worktree" +import { Effect, Layer, Option, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" + +const ConsoleStateResponse = Schema.Struct({ + consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), + activeOrgName: Schema.optionalKey(Schema.String), + switchableOrgCount: Schema.Number, +}).annotate({ identifier: "ConsoleState" }) + +const ConsoleOrgOption = Schema.Struct({ + accountID: Schema.String, + accountEmail: Schema.String, + accountUrl: Schema.String, + orgID: Schema.String, + orgName: Schema.String, + active: Schema.Boolean, +}).annotate({ identifier: "ConsoleOrgOption" }) + +const ConsoleOrgList = Schema.Struct({ + orgs: Schema.Array(ConsoleOrgOption), +}).annotate({ identifier: "ConsoleOrgList" }) + +const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" }) + +const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" }) + +export const ExperimentalPaths = { + console: "/experimental/console", + consoleOrgs: "/experimental/console/orgs", + toolIDs: "/experimental/tool/ids", + worktree: "/experimental/worktree", + worktreeReset: "/experimental/worktree/reset", + resource: "/experimental/resource", +} as const + +export const ExperimentalApi = HttpApi.make("experimental") + .add( + HttpApiGroup.make("experimental") + .add( + HttpApiEndpoint.get("console", ExperimentalPaths.console, { + success: ConsoleStateResponse, + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.console.get", + summary: "Get active Console provider metadata", + description: "Get the active Console org name and the set of provider IDs managed by that Console org.", + }), + ), + HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, { + success: ConsoleOrgList, + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.console.listOrgs", + summary: "List switchable Console orgs", + description: "Get the available Console orgs across logged-in accounts, including the current active org.", + }), + ), + HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, { + success: ToolIDs, + }).annotateMerge( + OpenApi.annotations({ + identifier: "tool.ids", + summary: "List tool IDs", + description: + "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", + }), + ), + HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, { + success: WorktreeList, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.list", + summary: "List worktrees", + description: "List all sandbox worktrees for the current project.", + }), + ), + HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, { + payload: Schema.optional(Worktree.CreateInput), + success: Worktree.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.create", + summary: "Create worktree", + description: "Create a new git worktree for the current project and run any configured startup scripts.", + }), + ), + HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, { + payload: Worktree.RemoveInput, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.remove", + summary: "Remove worktree", + description: "Remove a git worktree and delete its branch.", + }), + ), + HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, { + payload: Worktree.ResetInput, + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "worktree.reset", + summary: "Reset worktree", + description: "Reset a worktree branch to the primary default branch.", + }), + ), + HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { + success: Schema.Record(Schema.String, MCP.Resource), + }).annotateMerge( + OpenApi.annotations({ + identifier: "experimental.resource.list", + summary: "Get MCP resources", + description: "Get all available MCP resources from connected servers. Optionally filter by name.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "experimental", + description: "Experimental HttpApi read-only routes.", + }), + ) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const experimentalHandlers = Layer.unwrap( + Effect.gen(function* () { + const account = yield* Account.Service + const config = yield* Config.Service + const mcp = yield* MCP.Service + const project = yield* Project.Service + const registry = yield* ToolRegistry.Service + const worktreeSvc = yield* Worktree.Service + + const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { + const [state, groups] = yield* Effect.all( + [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)], + { + concurrency: "unbounded", + }, + ) + return { + consoleManagedProviders: state.consoleManagedProviders, + ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}), + switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), + } + }) + + const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () { + const [groups, active] = yield* Effect.all( + [account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)], + { + concurrency: "unbounded", + }, + ) + const info = Option.getOrUndefined(active) + return { + orgs: groups.flatMap((group) => + group.orgs.map((org) => ({ + accountID: group.account.id, + accountEmail: group.account.email, + accountUrl: group.account.url, + orgID: org.id, + orgName: org.name, + active: !!info && info.id === group.account.id && info.active_org_id === org.id, + })), + ), + } + }) + + const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () { + return yield* registry.ids() + }) + + const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () { + const ctx = yield* InstanceState.context + return yield* project.sandboxes(ctx.project.id) + }) + + const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: { + payload: Worktree.CreateInput | undefined + }) { + return yield* worktreeSvc.create(ctx.payload) + }) + + const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: { + payload: Worktree.RemoveInput + }) { + const ctx = yield* InstanceState.context + yield* worktreeSvc.remove(input.payload) + yield* project.removeSandbox(ctx.project.id, input.payload.directory) + return true + }) + + const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: { + payload: Worktree.ResetInput + }) { + yield* worktreeSvc.reset(ctx.payload) + return true + }) + + const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () { + return yield* mcp.resources() + }) + + return HttpApiBuilder.group(ExperimentalApi, "experimental", (handlers) => + handlers + .handle("console", getConsole) + .handle("consoleOrgs", listConsoleOrgs) + .handle("toolIDs", toolIDs) + .handle("worktree", worktree) + .handle("worktreeCreate", worktreeCreate) + .handle("worktreeRemove", worktreeRemove) + .handle("worktreeReset", worktreeReset) + .handle("resource", resource), + ) + }), +).pipe( + Layer.provide(Account.defaultLayer), + Layer.provide(Config.defaultLayer), + Layer.provide(MCP.defaultLayer), + Layer.provide(Project.defaultLayer), + Layer.provide(ToolRegistry.defaultLayer), + Layer.provide(Worktree.defaultLayer), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/file.ts b/packages/opencode/src/server/routes/instance/httpapi/file.ts new file mode 100644 index 000000000000..5222f4ab8f19 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/file.ts @@ -0,0 +1,167 @@ +import { File } from "@/file" +import { Ripgrep } from "@/file/ripgrep" +import * as InstanceState from "@/effect/instance-state" +import { LSP } from "@/lsp" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" + +const FileQuery = Schema.Struct({ + path: Schema.String, +}) + +const FindTextQuery = Schema.Struct({ + pattern: Schema.String, +}) + +const FindFileQuery = Schema.Struct({ + query: Schema.String, + dirs: Schema.optional(Schema.Literals(["true", "false"])), + type: Schema.optional(Schema.Literals(["file", "directory"])), + limit: Schema.optional( + Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)), + ), +}) + +const FindSymbolQuery = Schema.Struct({ + query: Schema.String, +}) + +export const FilePaths = { + findText: "/find", + findFile: "/find/file", + findSymbol: "/find/symbol", + list: "/file", + content: "/file/content", + status: "/file/status", +} as const + +export const FileApi = HttpApi.make("file") + .add( + HttpApiGroup.make("file") + .add( + HttpApiEndpoint.get("findText", FilePaths.findText, { + query: FindTextQuery, + success: Schema.Array(Ripgrep.SearchMatch), + }).annotateMerge( + OpenApi.annotations({ + identifier: "find.text", + summary: "Find text", + description: "Search for text patterns across files in the project using ripgrep.", + }), + ), + HttpApiEndpoint.get("findFile", FilePaths.findFile, { + query: FindFileQuery, + success: Schema.Array(Schema.String), + }).annotateMerge( + OpenApi.annotations({ + identifier: "find.files", + summary: "Find files", + description: "Search for files or directories by name or pattern in the project directory.", + }), + ), + HttpApiEndpoint.get("findSymbol", FilePaths.findSymbol, { + query: FindSymbolQuery, + success: Schema.Array(LSP.Symbol), + }).annotateMerge( + OpenApi.annotations({ + identifier: "find.symbols", + summary: "Find symbols", + description: "Search for workspace symbols like functions, classes, and variables using LSP.", + }), + ), + HttpApiEndpoint.get("list", FilePaths.list, { + query: FileQuery, + success: Schema.Array(File.Node), + }).annotateMerge( + OpenApi.annotations({ + identifier: "file.list", + summary: "List files", + description: "List files and directories in a specified path.", + }), + ), + HttpApiEndpoint.get("content", FilePaths.content, { + query: FileQuery, + success: File.Content, + }).annotateMerge( + OpenApi.annotations({ + identifier: "file.read", + summary: "Read file", + description: "Read the content of a specified file.", + }), + ), + HttpApiEndpoint.get("status", FilePaths.status, { + success: Schema.Array(File.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "file.status", + summary: "Get file status", + description: "Get the git status of all files in the project.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "file", + description: "Experimental HttpApi file routes.", + }), + ) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const fileHandlers = Layer.unwrap( + Effect.gen(function* () { + const svc = yield* File.Service + const ripgrep = yield* Ripgrep.Service + + const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) { + return (yield* ripgrep + .search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 }) + .pipe(Effect.orDie)).items + }) + + const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: { + query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number } + }) { + return yield* svc.search({ + query: ctx.query.query, + limit: ctx.query.limit ?? 10, + dirs: ctx.query.dirs !== "false", + type: ctx.query.type, + }) + }) + + const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () { + return [] + }) + + const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) { + return yield* svc.list(ctx.query.path) + }) + + const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) { + return yield* svc.read(ctx.query.path) + }) + + const status = Effect.fn("FileHttpApi.status")(function* () { + return yield* svc.status() + }) + + return HttpApiBuilder.group(FileApi, "file", (handlers) => + handlers + .handle("findText", findText) + .handle("findFile", findFile) + .handle("findSymbol", findSymbol) + .handle("list", list) + .handle("content", content) + .handle("status", status), + ) + }), +).pipe(Layer.provide(File.defaultLayer), Layer.provide(Ripgrep.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/httpapi/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/instance.ts new file mode 100644 index 000000000000..8788dee5f8c8 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/instance.ts @@ -0,0 +1,217 @@ +import { Agent } from "@/agent/agent" +import { Command } from "@/command" +import { Format } from "@/format" +import { Global } from "@opencode-ai/core/global" +import { LSP } from "@/lsp" +import { Vcs } from "@/project" +import { Skill } from "@/skill" +import * as InstanceState from "@/effect/instance-state" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" +import { markInstanceForDisposal } from "./lifecycle" + +const PathInfo = Schema.Struct({ + home: Schema.String, + state: Schema.String, + config: Schema.String, + worktree: Schema.String, + directory: Schema.String, +}).annotate({ identifier: "Path" }) + +const VcsDiffQuery = Schema.Struct({ + mode: Vcs.Mode, +}) + +export const InstancePaths = { + dispose: "/instance/dispose", + path: "/path", + vcs: "/vcs", + vcsDiff: "/vcs/diff", + command: "/command", + agent: "/agent", + skill: "/skill", + lsp: "/lsp", + formatter: "/formatter", +} as const + +export const InstanceApi = HttpApi.make("instance") + .add( + HttpApiGroup.make("instance") + .add( + HttpApiEndpoint.post("dispose", InstancePaths.dispose, { + success: Schema.Boolean, + }).annotateMerge( + OpenApi.annotations({ + identifier: "instance.dispose", + summary: "Dispose instance", + description: "Clean up and dispose the current OpenCode instance, releasing all resources.", + }), + ), + HttpApiEndpoint.get("path", InstancePaths.path, { + success: PathInfo, + }).annotateMerge( + OpenApi.annotations({ + identifier: "path.get", + summary: "Get paths", + description: + "Retrieve the current working directory and related path information for the OpenCode instance.", + }), + ), + HttpApiEndpoint.get("vcs", InstancePaths.vcs, { + success: Vcs.Info, + }).annotateMerge( + OpenApi.annotations({ + identifier: "vcs.get", + summary: "Get VCS info", + description: + "Retrieve version control system (VCS) information for the current project, such as git branch.", + }), + ), + HttpApiEndpoint.get("vcsDiff", InstancePaths.vcsDiff, { + query: VcsDiffQuery, + success: Schema.Array(Vcs.FileDiff), + }).annotateMerge( + OpenApi.annotations({ + identifier: "vcs.diff", + summary: "Get VCS diff", + description: "Retrieve the current git diff for the working tree or against the default branch.", + }), + ), + HttpApiEndpoint.get("command", InstancePaths.command, { + success: Schema.Array(Command.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "command.list", + summary: "List commands", + description: "Get a list of all available commands in the OpenCode system.", + }), + ), + HttpApiEndpoint.get("agent", InstancePaths.agent, { + success: Schema.Array(Agent.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "app.agents", + summary: "List agents", + description: "Get a list of all available AI agents in the OpenCode system.", + }), + ), + HttpApiEndpoint.get("skill", InstancePaths.skill, { + success: Schema.Array(Skill.Info), + }).annotateMerge( + OpenApi.annotations({ + identifier: "app.skills", + summary: "List skills", + description: "Get a list of all available skills in the OpenCode system.", + }), + ), + HttpApiEndpoint.get("lsp", InstancePaths.lsp, { + success: Schema.Array(LSP.Status), + }).annotateMerge( + OpenApi.annotations({ + identifier: "lsp.status", + summary: "Get LSP status", + description: "Get LSP server status", + }), + ), + HttpApiEndpoint.get("formatter", InstancePaths.formatter, { + success: Schema.Array(Format.Status), + }).annotateMerge( + OpenApi.annotations({ + identifier: "formatter.status", + summary: "Get formatter status", + description: "Get formatter status", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "instance", + description: "Experimental HttpApi instance read routes.", + }), + ) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const instanceHandlers = Layer.unwrap( + Effect.gen(function* () { + const agent = yield* Agent.Service + const command = yield* Command.Service + const format = yield* Format.Service + const lsp = yield* LSP.Service + const skill = yield* Skill.Service + const vcs = yield* Vcs.Service + + const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () { + yield* markInstanceForDisposal(yield* InstanceState.context) + return true + }) + + const getPath = Effect.fn("InstanceHttpApi.path")(function* () { + const ctx = yield* InstanceState.context + return { + home: Global.Path.home, + state: Global.Path.state, + config: Global.Path.config, + worktree: ctx.worktree, + directory: ctx.directory, + } + }) + + const getVcs = Effect.fn("InstanceHttpApi.vcs")(function* () { + const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 }) + return { branch, default_branch } + }) + + const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) { + return yield* vcs.diff(ctx.query.mode) + }) + + const getCommand = Effect.fn("InstanceHttpApi.command")(function* () { + return yield* command.list() + }) + + const getAgent = Effect.fn("InstanceHttpApi.agent")(function* () { + return yield* agent.list() + }) + + const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () { + return yield* skill.all() + }) + + const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () { + return yield* lsp.status() + }) + + const getFormatter = Effect.fn("InstanceHttpApi.formatter")(function* () { + return yield* format.status() + }) + + return HttpApiBuilder.group(InstanceApi, "instance", (handlers) => + handlers + .handle("dispose", dispose) + .handle("path", getPath) + .handle("vcs", getVcs) + .handle("vcsDiff", getVcsDiff) + .handle("command", getCommand) + .handle("agent", getAgent) + .handle("skill", getSkill) + .handle("lsp", getLsp) + .handle("formatter", getFormatter), + ) + }), +).pipe( + Layer.provide(Agent.defaultLayer), + Layer.provide(Command.defaultLayer), + Layer.provide(Format.defaultLayer), + Layer.provide(LSP.defaultLayer), + Layer.provide(Skill.defaultLayer), + Layer.provide(Vcs.defaultLayer), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts new file mode 100644 index 000000000000..6b11dffd53c6 --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/lifecycle.ts @@ -0,0 +1,24 @@ +import { Instance, type InstanceContext } from "@/project/instance" +import { Effect } from "effect" +import { HttpEffect, HttpMiddleware, HttpServerRequest } from "effect/unstable/http" + +const disposeAfterResponse = new WeakMap() + +export const markInstanceForDisposal = (ctx: InstanceContext) => + HttpEffect.appendPreResponseHandler((request, response) => + Effect.sync(() => { + disposeAfterResponse.set(request.source, ctx) + return response + }), + ) + +export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) => + Effect.gen(function* () { + const response = yield* effect + const request = yield* HttpServerRequest.HttpServerRequest + const ctx = disposeAfterResponse.get(request.source) + if (!ctx) return response + disposeAfterResponse.delete(request.source) + yield* Effect.promise(() => Instance.restore(ctx, () => Instance.dispose())) + return response + }) diff --git a/packages/opencode/src/server/routes/instance/httpapi/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts new file mode 100644 index 000000000000..34d4e09e2dfb --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/mcp.ts @@ -0,0 +1,50 @@ +import { MCP } from "@/mcp" +import { Effect, Layer, Schema } from "effect" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" + +export const McpPaths = { + status: "/mcp", +} as const + +export const McpApi = HttpApi.make("mcp") + .add( + HttpApiGroup.make("mcp") + .add( + HttpApiEndpoint.get("status", McpPaths.status, { + success: Schema.Record(Schema.String, MCP.Status), + }).annotateMerge( + OpenApi.annotations({ + identifier: "mcp.status", + summary: "Get MCP status", + description: "Get the status of all Model Context Protocol (MCP) servers.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "mcp", + description: "Experimental HttpApi MCP routes.", + }), + ) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + +export const mcpHandlers = Layer.unwrap( + Effect.gen(function* () { + const mcp = yield* MCP.Service + + const status = Effect.fn("McpHttpApi.status")(function* () { + return yield* mcp.status() + }) + + return HttpApiBuilder.group(McpApi, "mcp", (handlers) => handlers.handle("status", status)) + }), +).pipe(Layer.provide(MCP.defaultLayer)) diff --git a/packages/opencode/src/server/routes/instance/httpapi/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/permission.ts index ed8cb4e2777b..85dbecd11615 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/permission.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/permission.ts @@ -2,6 +2,7 @@ import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/permission" @@ -35,7 +36,8 @@ export const PermissionApi = HttpApi.make("permission") title: "permission", description: "Experimental HttpApi permission routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/project.ts b/packages/opencode/src/server/routes/instance/httpapi/project.ts index 7d2d8462f075..6d3143df869b 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/project.ts @@ -1,7 +1,8 @@ -import { Instance } from "@/project/instance" +import * as InstanceState from "@/effect/instance-state" import { Project } from "@/project" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/project" @@ -33,7 +34,8 @@ export const ProjectApi = HttpApi.make("project") title: "project", description: "Experimental HttpApi project routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ @@ -52,7 +54,7 @@ export const projectHandlers = Layer.unwrap( }) const current = Effect.fn("ProjectHttpApi.current")(function* () { - return Instance.project + return (yield* InstanceState.context).project }) return HttpApiBuilder.group(ProjectApi, "project", (handlers) => diff --git a/packages/opencode/src/server/routes/instance/httpapi/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/provider.ts index 67831a1fafb2..dd1a21d2b0d3 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/provider.ts @@ -6,6 +6,7 @@ import { ProviderID } from "@/provider/schema" import { mapValues } from "remeda" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/provider" @@ -59,7 +60,8 @@ export const ProviderApi = HttpApi.make("provider") title: "provider", description: "Experimental HttpApi provider routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/question.ts b/packages/opencode/src/server/routes/instance/httpapi/question.ts index 3192b530e944..526a78ee0ac6 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/question.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/question.ts @@ -2,6 +2,7 @@ import { Question } from "@/question" import { QuestionID } from "@/question/schema" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/question" @@ -45,7 +46,8 @@ export const QuestionApi = HttpApi.make("question") title: "question", description: "Question routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 7b131d400029..be574c914b44 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -1,21 +1,26 @@ -import { Effect, Layer, Redacted, Schema } from "effect" -import { HttpApiBuilder, HttpApiMiddleware, HttpApiSecurity } from "effect/unstable/httpapi" +import { Effect, Layer, Schema } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" import { HttpRouter, HttpServer, HttpServerRequest } from "effect/unstable/http" import { AppRuntime } from "@/effect/app-runtime" import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref" import { Observability } from "@/effect" -import { Flag } from "@/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util" +import { authorizationLayer } from "./auth" import { ConfigApi, configHandlers } from "./config" +import { FileApi, fileHandlers } from "./file" +import { ExperimentalApi, experimentalHandlers } from "./experimental" +import { InstanceApi, instanceHandlers } from "./instance" +import { McpApi, mcpHandlers } from "./mcp" import { PermissionApi, permissionHandlers } from "./permission" import { ProjectApi, projectHandlers } from "./project" import { ProviderApi, providerHandlers } from "./provider" import { QuestionApi, questionHandlers } from "./question" import { WorkspaceApi, workspaceHandlers } from "./workspace" -import { memoMap } from "@/effect/memo-map" +import { disposeMiddleware } from "./lifecycle" +import { memoMap } from "@opencode-ai/core/effect/memo-map" const Query = Schema.Struct({ directory: Schema.optional(Schema.String), @@ -36,56 +41,6 @@ function decode(input: string) { } } -class Unauthorized extends Schema.TaggedErrorClass()( - "Unauthorized", - { message: Schema.String }, - { httpApiStatus: 401 }, -) {} - -class Authorization extends HttpApiMiddleware.Service()("@opencode/ExperimentalHttpApiAuthorization", { - error: Unauthorized, - security: { - basic: HttpApiSecurity.basic, - }, -}) {} - -const normalize = HttpRouter.middleware()( - Effect.gen(function* () { - return (effect) => - Effect.gen(function* () { - const query = yield* HttpServerRequest.schemaSearchParams(Query) - if (!query.auth_token) return yield* effect - const req = yield* HttpServerRequest.HttpServerRequest - const next = req.modify({ - headers: { - ...req.headers, - authorization: `Basic ${query.auth_token}`, - }, - }) - return yield* effect.pipe(Effect.provideService(HttpServerRequest.HttpServerRequest, next)) - }) - }), -).layer - -const auth = Layer.succeed( - Authorization, - Authorization.of({ - basic: (effect, { credential }) => - Effect.gen(function* () { - if (!Flag.OPENCODE_SERVER_PASSWORD) return yield* effect - - const user = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" - if (credential.username !== user) { - return yield* new Unauthorized({ message: "Unauthorized" }) - } - if (Redacted.value(credential.password) !== Flag.OPENCODE_SERVER_PASSWORD) { - return yield* new Unauthorized({ message: "Unauthorized" }) - } - return yield* effect - }), - }), -) - const instance = HttpRouter.middleware()( Effect.gen(function* () { return (effect) => @@ -108,23 +63,19 @@ const instance = HttpRouter.middleware()( }), ).layer -const QuestionSecured = QuestionApi.middleware(Authorization) -const PermissionSecured = PermissionApi.middleware(Authorization) -const ProjectSecured = ProjectApi.middleware(Authorization) -const ProviderSecured = ProviderApi.middleware(Authorization) -const ConfigSecured = ConfigApi.middleware(Authorization) -const WorkspaceSecured = WorkspaceApi.middleware(Authorization) - export const routes = Layer.mergeAll( - HttpApiBuilder.layer(ConfigSecured).pipe(Layer.provide(configHandlers)), - HttpApiBuilder.layer(ProjectSecured).pipe(Layer.provide(projectHandlers)), - HttpApiBuilder.layer(QuestionSecured).pipe(Layer.provide(questionHandlers)), - HttpApiBuilder.layer(PermissionSecured).pipe(Layer.provide(permissionHandlers)), - HttpApiBuilder.layer(ProviderSecured).pipe(Layer.provide(providerHandlers)), - HttpApiBuilder.layer(WorkspaceSecured).pipe(Layer.provide(workspaceHandlers)), + HttpApiBuilder.layer(ConfigApi).pipe(Layer.provide(configHandlers)), + HttpApiBuilder.layer(ExperimentalApi).pipe(Layer.provide(experimentalHandlers)), + HttpApiBuilder.layer(FileApi).pipe(Layer.provide(fileHandlers)), + HttpApiBuilder.layer(InstanceApi).pipe(Layer.provide(instanceHandlers)), + HttpApiBuilder.layer(McpApi).pipe(Layer.provide(mcpHandlers)), + HttpApiBuilder.layer(ProjectApi).pipe(Layer.provide(projectHandlers)), + HttpApiBuilder.layer(QuestionApi).pipe(Layer.provide(questionHandlers)), + HttpApiBuilder.layer(PermissionApi).pipe(Layer.provide(permissionHandlers)), + HttpApiBuilder.layer(ProviderApi).pipe(Layer.provide(providerHandlers)), + HttpApiBuilder.layer(WorkspaceApi).pipe(Layer.provide(workspaceHandlers)), ).pipe( - Layer.provide(auth), - Layer.provide(normalize), + Layer.provide(authorizationLayer), Layer.provide(instance), Layer.provide(HttpServer.layerServices), Layer.provideMerge(Observability.layer), @@ -133,6 +84,7 @@ export const routes = Layer.mergeAll( export const webHandler = lazy(() => HttpRouter.toWebHandler(routes, { memoMap, + middleware: disposeMiddleware, }), ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/workspace.ts index 596545073e12..2ab6b03d24e1 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/workspace.ts @@ -4,6 +4,7 @@ import { WorkspaceAdaptorEntry } from "@/control-plane/types" import * as InstanceState from "@/effect/instance-state" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "./auth" const root = "/experimental/workspace" export const WorkspacePaths = { @@ -49,7 +50,8 @@ export const WorkspaceApi = HttpApi.make("workspace") title: "workspace", description: "Experimental HttpApi workspace routes.", }), - ), + ) + .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index e8a038fabc0a..25e9e058ab25 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -9,13 +9,17 @@ import { Instance } from "@/project/instance" import { Vcs } from "@/project" import { Agent } from "@/agent/agent" import { Skill } from "@/skill" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { LSP } from "@/lsp" import { Command } from "@/command" import { QuestionRoutes } from "./question" import { PermissionRoutes } from "./permission" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { ExperimentalHttpApiServer } from "./httpapi/server" +import { ExperimentalPaths } from "./httpapi/experimental" +import { FilePaths } from "./httpapi/file" +import { InstancePaths } from "./httpapi/instance" +import { McpPaths } from "./httpapi/mcp" import { ProjectRoutes } from "./project" import { SessionRoutes } from "./session" import { PtyRoutes } from "./pty" @@ -41,13 +45,38 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { app.get("/permission", (c) => handler(c.req.raw, context)) app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context)) app.get("/config", (c) => handler(c.req.raw, context)) + app.patch("/config", (c) => handler(c.req.raw, context)) app.get("/config/providers", (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.toolIDs, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.post(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.delete(ExperimentalPaths.worktree, (c) => handler(c.req.raw, context)) + app.post(ExperimentalPaths.worktreeReset, (c) => handler(c.req.raw, context)) + app.get(ExperimentalPaths.resource, (c) => handler(c.req.raw, context)) app.get("/provider", (c) => handler(c.req.raw, context)) app.get("/provider/auth", (c) => handler(c.req.raw, context)) app.post("/provider/:providerID/oauth/authorize", (c) => handler(c.req.raw, context)) app.post("/provider/:providerID/oauth/callback", (c) => handler(c.req.raw, context)) app.get("/project", (c) => handler(c.req.raw, context)) app.get("/project/current", (c) => handler(c.req.raw, context)) + app.get(FilePaths.findText, (c) => handler(c.req.raw, context)) + app.get(FilePaths.findFile, (c) => handler(c.req.raw, context)) + app.get(FilePaths.findSymbol, (c) => handler(c.req.raw, context)) + app.get(FilePaths.list, (c) => handler(c.req.raw, context)) + app.get(FilePaths.content, (c) => handler(c.req.raw, context)) + app.get(FilePaths.status, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.path, (c) => handler(c.req.raw, context)) + app.post(InstancePaths.dispose, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcs, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.vcsDiff, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.command, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.agent, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.skill, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.lsp, (c) => handler(c.req.raw, context)) + app.get(InstancePaths.formatter, (c) => handler(c.req.raw, context)) + app.get(McpPaths.status, (c) => handler(c.req.raw, context)) } return app @@ -136,7 +165,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "VCS info", content: { "application/json": { - schema: resolver(Vcs.Info), + schema: resolver(Vcs.Info.zod), }, }, }, @@ -162,7 +191,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "VCS diff", content: { "application/json": { - schema: resolver(Vcs.FileDiff.array()), + schema: resolver(Vcs.FileDiff.zod.array()), }, }, }, @@ -171,7 +200,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { validator( "query", z.object({ - mode: Vcs.Mode, + mode: Vcs.Mode.zod, }), ), async (c) => @@ -191,7 +220,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "List of commands", content: { "application/json": { - schema: resolver(Command.Info.array()), + schema: resolver(Command.Info.zod.array()), }, }, }, @@ -214,7 +243,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "List of agents", content: { "application/json": { - schema: resolver(Agent.Info.array()), + schema: resolver(Agent.Info.zod.array()), }, }, }, @@ -237,7 +266,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "List of skills", content: { "application/json": { - schema: resolver(Skill.Info.array()), + schema: resolver(Skill.Info.zod.array()), }, }, }, @@ -283,7 +312,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { description: "Formatter status", content: { "application/json": { - schema: resolver(Format.Status.array()), + schema: resolver(Format.Status.zod.array()), }, }, }, diff --git a/packages/opencode/src/server/routes/instance/mcp.ts b/packages/opencode/src/server/routes/instance/mcp.ts index ce4722933b14..b47a6d29a961 100644 --- a/packages/opencode/src/server/routes/instance/mcp.ts +++ b/packages/opencode/src/server/routes/instance/mcp.ts @@ -21,7 +21,7 @@ export const McpRoutes = lazy(() => description: "MCP server status", content: { "application/json": { - schema: resolver(z.record(z.string(), MCP.Status)), + schema: resolver(z.record(z.string(), MCP.Status.zod)), }, }, }, @@ -44,7 +44,7 @@ export const McpRoutes = lazy(() => description: "MCP server added successfully", content: { "application/json": { - schema: resolver(z.record(z.string(), MCP.Status)), + schema: resolver(z.record(z.string(), MCP.Status.zod)), }, }, }, @@ -121,7 +121,7 @@ export const McpRoutes = lazy(() => description: "OAuth authentication completed", content: { "application/json": { - schema: resolver(MCP.Status), + schema: resolver(MCP.Status.zod), }, }, }, @@ -153,7 +153,7 @@ export const McpRoutes = lazy(() => description: "OAuth authentication completed", content: { "application/json": { - schema: resolver(MCP.Status), + schema: resolver(MCP.Status.zod), }, }, }, diff --git a/packages/opencode/src/server/routes/instance/middleware.ts b/packages/opencode/src/server/routes/instance/middleware.ts index b963268d6478..19918b8b487d 100644 --- a/packages/opencode/src/server/routes/instance/middleware.ts +++ b/packages/opencode/src/server/routes/instance/middleware.ts @@ -2,7 +2,7 @@ import type { MiddlewareHandler } from "hono" import { Instance } from "@/project/instance" import { InstanceBootstrap } from "@/project/bootstrap" import { AppRuntime } from "@/effect/app-runtime" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceID } from "@/control-plane/schema" diff --git a/packages/opencode/src/server/routes/instance/pty.ts b/packages/opencode/src/server/routes/instance/pty.ts index 51c469924168..581537221972 100644 --- a/packages/opencode/src/server/routes/instance/pty.ts +++ b/packages/opencode/src/server/routes/instance/pty.ts @@ -1,7 +1,7 @@ import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import type { UpgradeWebSocket } from "hono/ws" -import { Effect } from "effect" +import { Effect, Schema } from "effect" import z from "zod" import { AppRuntime } from "@/effect/app-runtime" import { Pty } from "@/pty" @@ -10,6 +10,8 @@ import { NotFoundError } from "@/storage" import { errors } from "../../error" import { jsonRequest, runRequest } from "./trace" +const decodePtyID = Schema.decodeUnknownSync(PtyID) + export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { return new Hono() .get( @@ -171,7 +173,7 @@ export function PtyRoutes(upgradeWebSocket: UpgradeWebSocket) { onClose: () => void } - const id = PtyID.zod.parse(c.req.param("ptyID")) + const id = decodePtyID(c.req.param("ptyID")) const cursor = (() => { const value = c.req.query("cursor") if (!value) return diff --git a/packages/opencode/src/server/routes/instance/session.ts b/packages/opencode/src/server/routes/instance/session.ts index 4f4f8ed86e7f..52a8034672e8 100644 --- a/packages/opencode/src/server/routes/instance/session.ts +++ b/packages/opencode/src/server/routes/instance/session.ts @@ -25,7 +25,7 @@ import { errors } from "../../error" import { lazy } from "@/util/lazy" import { zodObject } from "@/util/effect-zod" import { Bus } from "@/bus" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { jsonRequest, runRequest } from "./trace" const log = Log.create({ service: "server" }) diff --git a/packages/opencode/src/server/routes/ui.ts b/packages/opencode/src/server/routes/ui.ts index d449cd1c4241..5e47e6bf716b 100644 --- a/packages/opencode/src/server/routes/ui.ts +++ b/packages/opencode/src/server/routes/ui.ts @@ -1,4 +1,4 @@ -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Hono } from "hono" import { proxy } from "hono/proxy" import { getMimeType } from "hono/utils/mime" diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index d74de559dc1f..fb278f268c18 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -3,7 +3,7 @@ import { Hono } from "hono" import { adapter } from "#hono" import { lazy } from "@/util/lazy" import { Log } from "@/util" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { WorkspaceID } from "@/control-plane/schema" import { MDNS } from "./mdns" import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" diff --git a/packages/opencode/src/server/workspace.ts b/packages/opencode/src/server/workspace.ts index d30a117d6a4a..3f71bf2f71cb 100644 --- a/packages/opencode/src/server/workspace.ts +++ b/packages/opencode/src/server/workspace.ts @@ -4,7 +4,7 @@ import { getAdaptor } from "@/control-plane/adaptors" import { WorkspaceID } from "@/control-plane/schema" import { WorkspaceContext } from "@/control-plane/workspace-context" import { Workspace } from "@/control-plane/workspace" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { InstanceBootstrap } from "@/project/bootstrap" import { Instance } from "@/project/instance" import { Session } from "@/session" diff --git a/packages/opencode/src/session/instruction.ts b/packages/opencode/src/session/instruction.ts index 122644c1fd89..35de718192d4 100644 --- a/packages/opencode/src/session/instruction.ts +++ b/packages/opencode/src/session/instruction.ts @@ -4,10 +4,10 @@ import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http" import { Config } from "@/config" import { InstanceState } from "@/effect" -import { Flag } from "@/flag/flag" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { Flag } from "@opencode-ai/core/flag/flag" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { withTransientReadRetry } from "@/util/effect-http-client" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import type { MessageV2 } from "./message-v2" import type { MessageID } from "./schema" diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index b72f873de01d..d5ff4e61c9c4 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -12,7 +12,7 @@ import type { Agent } from "@/agent/agent" import type { MessageV2 } from "./message-v2" import { Plugin } from "@/plugin" import { SystemPrompt } from "./system" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { Bus } from "@/bus" @@ -20,7 +20,7 @@ import { Wildcard } from "@/util" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { Installation } from "@/installation" -import { InstallationVersion } from "@/installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { EffectBridge } from "@/effect" import * as Option from "effect/Option" import * as OtelTracer from "@effect/opentelemetry/Tracer" diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index d04645b7360c..e461c871616a 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -1,1196 +1,1009 @@ import { BusEvent } from "@/bus/bus-event" import { SessionID, MessageID, PartID } from "./schema" import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/util/error" import { APICallError, convertToModelMessages, LoadAPIKeyError, type ModelMessage, type UIMessage } from "ai" import { LSP } from "../lsp" import { Snapshot } from "@/snapshot" -import { SyncEvent } from "../sync" -import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage" +import { fn } from "@/util/fn" +import { Database, NotFoundError, and, desc, eq, inArray, lt, or } from "@/storage/db" import { MessageTable, PartTable, SessionTable } from "./session.sql" -import { ProviderError } from "@/provider" +import { ProviderTransform } from "@/provider/transform" +import { STATUS_CODES } from "http" +import { Storage } from "@/storage/storage" +import { ProviderError } from "@/provider/error" import { iife } from "@/util/iife" -import { errorMessage } from "@/util/error" -import { isMedia } from "@/util/media" import type { SystemError } from "bun" -import type { Provider } from "@/provider" +import type { Provider } from "@/provider/provider" import { ModelID, ProviderID } from "@/provider/schema" -import { Effect, Schema, Types } from "effect" -import { zod, ZodOverride } from "@/util/effect-zod" -import { NonNegativeInt, withStatics } from "@/util/schema" -import { namedSchemaError } from "@/util/named-schema-error" -import { EffectLogger } from "@/effect" - -/** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */ -interface FetchDecompressionError extends Error { - code: "ZlibError" - errno: number - path: string -} - -export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:" -export { isMedia } - -export const OutputLengthError = namedSchemaError("MessageOutputLengthError", {}) -export const AbortedError = namedSchemaError("MessageAbortedError", { message: Schema.String }) -export const StructuredOutputError = namedSchemaError("StructuredOutputError", { - message: Schema.String, - retries: Schema.Number, -}) -export const AuthError = namedSchemaError("ProviderAuthError", { - providerID: Schema.String, - message: Schema.String, -}) -export const APIError = namedSchemaError("APIError", { - message: Schema.String, - statusCode: Schema.optional(Schema.Number), - isRetryable: Schema.Boolean, - responseHeaders: Schema.optional(Schema.Record(Schema.String, Schema.String)), - responseBody: Schema.optional(Schema.String), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), -}) -export type APIError = z.infer -export const ContextOverflowError = namedSchemaError("ContextOverflowError", { - message: Schema.String, - responseBody: Schema.optional(Schema.String), -}) - -export class OutputFormatText extends Schema.Class("OutputFormatText")({ - type: Schema.Literal("text"), -}) { - static readonly zod = zod(this) -} -export class OutputFormatJsonSchema extends Schema.Class("OutputFormatJsonSchema")({ - type: Schema.Literal("json_schema"), - schema: Schema.Record(Schema.String, Schema.Any).annotate({ identifier: "JSONSchema" }), - retryCount: NonNegativeInt.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(2))), -}) { - static readonly zod = zod(this) -} - -const _Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotate({ - discriminator: "type", - identifier: "OutputFormat", -}) -export const Format = Object.assign(_Format, { zod: zod(_Format) }) -export type OutputFormat = Schema.Schema.Type - -const partBase = { - id: PartID, - sessionID: SessionID, - messageID: MessageID, -} +export namespace MessageV2 { + const NETWORK_ERROR_CODES = new Set([ + "ECONNRESET", + "ETIMEDOUT", + "ENETUNREACH", + "EHOSTUNREACH", + "ENOTFOUND", + "EPIPE", + "ECONNREFUSED", + ]) + + export function isMedia(mime: string) { + return mime.startsWith("image/") || mime === "application/pdf" + } -export const SnapshotPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("snapshot"), - snapshot: Schema.String, -}) - .annotate({ identifier: "SnapshotPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type SnapshotPart = Types.DeepMutable> - -export const PatchPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("patch"), - hash: Schema.String, - files: Schema.Array(Schema.String), -}) - .annotate({ identifier: "PatchPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type PatchPart = Types.DeepMutable> - -export const TextPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("text"), - text: Schema.String, - synthetic: Schema.optional(Schema.Boolean), - ignored: Schema.optional(Schema.Boolean), - time: Schema.optional( - Schema.Struct({ - start: Schema.Number, - end: Schema.optional(Schema.Number), + export const OutputLengthError = NamedError.create("MessageOutputLengthError", z.object({})) + export const AbortedError = NamedError.create("MessageAbortedError", z.object({ message: z.string() })) + export const StructuredOutputError = NamedError.create( + "StructuredOutputError", + z.object({ + message: z.string(), + retries: z.number(), }), - ), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}) - .annotate({ identifier: "TextPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type TextPart = Types.DeepMutable> - -export const ReasoningPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("reasoning"), - text: Schema.String, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), - time: Schema.Struct({ - start: Schema.Number, - end: Schema.optional(Schema.Number), - }), -}) - .annotate({ identifier: "ReasoningPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ReasoningPart = Types.DeepMutable> - -const filePartSourceBase = { - text: Schema.Struct({ - value: Schema.String, - start: Schema.Int, - end: Schema.Int, - }).annotate({ identifier: "FilePartSourceText" }), -} - -export const FileSource = Schema.Struct({ - ...filePartSourceBase, - type: Schema.Literal("file"), - path: Schema.String, -}) - .annotate({ identifier: "FileSource" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - -export const SymbolSource = Schema.Struct({ - ...filePartSourceBase, - type: Schema.Literal("symbol"), - path: Schema.String, - range: LSP.Range, - name: Schema.String, - kind: Schema.Int, -}) - .annotate({ identifier: "SymbolSource" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - -export const ResourceSource = Schema.Struct({ - ...filePartSourceBase, - type: Schema.Literal("resource"), - clientName: Schema.String, - uri: Schema.String, -}) - .annotate({ identifier: "ResourceSource" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - -const _FilePartSource = Schema.Union([FileSource, SymbolSource, ResourceSource]).annotate({ - discriminator: "type", - identifier: "FilePartSource", -}) -export const FilePartSource = Object.assign(_FilePartSource, { zod: zod(_FilePartSource) }) - -export const FilePart = Schema.Struct({ - ...partBase, - type: Schema.Literal("file"), - mime: Schema.String, - filename: Schema.optional(Schema.String), - url: Schema.String, - source: Schema.optional(_FilePartSource), -}) - .annotate({ identifier: "FilePart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type FilePart = Types.DeepMutable> - -export const AgentPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("agent"), - name: Schema.String, - source: Schema.optional( - Schema.Struct({ - value: Schema.String, - start: Schema.Int, - end: Schema.Int, + ) + export const AuthError = NamedError.create( + "ProviderAuthError", + z.object({ + providerID: z.string(), + message: z.string(), }), - ), -}) - .annotate({ identifier: "AgentPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type AgentPart = Types.DeepMutable> - -export const CompactionPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("compaction"), - auto: Schema.Boolean, - overflow: Schema.optional(Schema.Boolean), - tail_start_id: Schema.optional(MessageID), -}) - .annotate({ identifier: "CompactionPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type CompactionPart = Types.DeepMutable> - -export const SubtaskPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("subtask"), - prompt: Schema.String, - description: Schema.String, - agent: Schema.String, - model: Schema.optional( - Schema.Struct({ - providerID: ProviderID, - modelID: ModelID, + ) + export const APIError = NamedError.create( + "APIError", + z.object({ + message: z.string(), + statusCode: z.number().optional(), + isRetryable: z.boolean(), + responseHeaders: z.record(z.string(), z.string()).optional(), + responseBody: z.string().optional(), + metadata: z.record(z.string(), z.string()).optional(), }), - ), - command: Schema.optional(Schema.String), -}) - .annotate({ identifier: "SubtaskPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type SubtaskPart = Types.DeepMutable> - -export const RetryPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("retry"), - attempt: Schema.Number, - // APIError is still NamedError-based Zod; bridge via ZodOverride until errors migrate. - error: Schema.Any.annotate({ [ZodOverride]: APIError.Schema }), - time: Schema.Struct({ - created: Schema.Number, - }), -}) - .annotate({ identifier: "RetryPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type RetryPart = Omit>, "error"> & { - error: APIError -} + ) + export type APIError = z.infer + export const ContextOverflowError = NamedError.create( + "ContextOverflowError", + z.object({ message: z.string(), responseBody: z.string().optional() }), + ) -export const StepStartPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("step-start"), - snapshot: Schema.optional(Schema.String), -}) - .annotate({ identifier: "StepStartPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type StepStartPart = Types.DeepMutable> - -export const StepFinishPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("step-finish"), - reason: Schema.String, - snapshot: Schema.optional(Schema.String), - cost: Schema.Number, - tokens: Schema.Struct({ - total: Schema.optional(Schema.Number), - input: Schema.Number, - output: Schema.Number, - reasoning: Schema.Number, - cache: Schema.Struct({ - read: Schema.Number, - write: Schema.Number, + export const OutputFormatText = z + .object({ + type: z.literal("text"), + }) + .meta({ + ref: "OutputFormatText", + }) + + export const OutputFormatJsonSchema = z + .object({ + type: z.literal("json_schema"), + schema: z.record(z.string(), z.any()).meta({ ref: "JSONSchema" }), + retryCount: z.number().int().min(0).default(2), + }) + .meta({ + ref: "OutputFormatJsonSchema", + }) + + export const Format = z.discriminatedUnion("type", [OutputFormatText, OutputFormatJsonSchema]).meta({ + ref: "OutputFormat", + }) + export type OutputFormat = z.infer + + const PartBase = z.object({ + id: PartID.zod, + sessionID: SessionID.zod, + messageID: MessageID.zod, + }) + + export const SnapshotPart = PartBase.extend({ + type: z.literal("snapshot"), + snapshot: z.string(), + }).meta({ + ref: "SnapshotPart", + }) + export type SnapshotPart = z.infer + + export const PatchPart = PartBase.extend({ + type: z.literal("patch"), + hash: z.string(), + files: z.string().array(), + }).meta({ + ref: "PatchPart", + }) + export type PatchPart = z.infer + + export const TextPart = PartBase.extend({ + type: z.literal("text"), + text: z.string(), + synthetic: z.boolean().optional(), + ignored: z.boolean().optional(), + time: z + .object({ + start: z.number(), + end: z.number().optional(), + }) + .optional(), + metadata: z.record(z.string(), z.any()).optional(), + }).meta({ + ref: "TextPart", + }) + export type TextPart = z.infer + + export const ReasoningPart = PartBase.extend({ + type: z.literal("reasoning"), + text: z.string(), + metadata: z.record(z.string(), z.any()).optional(), + time: z.object({ + start: z.number(), + end: z.number().optional(), }), - }), -}) - .annotate({ identifier: "StepFinishPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type StepFinishPart = Types.DeepMutable> - -export const ToolStatePending = Schema.Struct({ - status: Schema.Literal("pending"), - input: Schema.Record(Schema.String, Schema.Any), - raw: Schema.String, -}) - .annotate({ identifier: "ToolStatePending" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ToolStatePending = Types.DeepMutable> - -export const ToolStateRunning = Schema.Struct({ - status: Schema.Literal("running"), - input: Schema.Record(Schema.String, Schema.Any), - title: Schema.optional(Schema.String), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), - time: Schema.Struct({ - start: Schema.Number, - }), -}) - .annotate({ identifier: "ToolStateRunning" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ToolStateRunning = Types.DeepMutable> - -export const ToolStateCompleted = Schema.Struct({ - status: Schema.Literal("completed"), - input: Schema.Record(Schema.String, Schema.Any), - output: Schema.String, - title: Schema.String, - metadata: Schema.Record(Schema.String, Schema.Any), - time: Schema.Struct({ - start: Schema.Number, - end: Schema.Number, - compacted: Schema.optional(Schema.Number), - }), - attachments: Schema.optional(Schema.Array(FilePart)), -}) - .annotate({ identifier: "ToolStateCompleted" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ToolStateCompleted = Types.DeepMutable> - -function truncateToolOutput(text: string, maxChars?: number) { - if (!maxChars || text.length <= maxChars) return text - const omitted = text.length - maxChars - return `${text.slice(0, maxChars)}\n[Tool output truncated for compaction: omitted ${omitted} chars]` -} - -export const ToolStateError = Schema.Struct({ - status: Schema.Literal("error"), - input: Schema.Record(Schema.String, Schema.Any), - error: Schema.String, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), - time: Schema.Struct({ - start: Schema.Number, - end: Schema.Number, - }), -}) - .annotate({ identifier: "ToolStateError" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ToolStateError = Types.DeepMutable> - -const _ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).annotate({ - discriminator: "status", - identifier: "ToolState", -}) -// Cast the derived zod so downstream z.infer sees the same mutable shape that -// our exported TS types expose (the pre-migration Zod inferences were mutable). -export const ToolState = Object.assign(_ToolState, { - zod: zod(_ToolState) as unknown as z.ZodType< - ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - >, -}) -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export const ToolPart = Schema.Struct({ - ...partBase, - type: Schema.Literal("tool"), - callID: Schema.String, - tool: Schema.String, - state: _ToolState, - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}) - .annotate({ identifier: "ToolPart" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type ToolPart = Omit>, "state"> & { - state: ToolState -} - -const messageBase = { - id: MessageID, - sessionID: SessionID, -} - -export const User = Schema.Struct({ - ...messageBase, - role: Schema.Literal("user"), - time: Schema.Struct({ - created: Schema.Number, - }), - format: Schema.optional(_Format), - summary: Schema.optional( - Schema.Struct({ - title: Schema.optional(Schema.String), - body: Schema.optional(Schema.String), - diffs: Schema.Array(Snapshot.FileDiff), + }).meta({ + ref: "ReasoningPart", + }) + export type ReasoningPart = z.infer + + const FilePartSourceBase = z.object({ + text: z + .object({ + value: z.string(), + start: z.number().int(), + end: z.number().int(), + }) + .meta({ + ref: "FilePartSourceText", + }), + }) + + export const FileSource = FilePartSourceBase.extend({ + type: z.literal("file"), + path: z.string(), + }).meta({ + ref: "FileSource", + }) + + export const SymbolSource = FilePartSourceBase.extend({ + type: z.literal("symbol"), + path: z.string(), + range: LSP.Range, + name: z.string(), + kind: z.number().int(), + }).meta({ + ref: "SymbolSource", + }) + + export const ResourceSource = FilePartSourceBase.extend({ + type: z.literal("resource"), + clientName: z.string(), + uri: z.string(), + }).meta({ + ref: "ResourceSource", + }) + + export const FilePartSource = z.discriminatedUnion("type", [FileSource, SymbolSource, ResourceSource]).meta({ + ref: "FilePartSource", + }) + + export const FilePart = PartBase.extend({ + type: z.literal("file"), + mime: z.string(), + filename: z.string().optional(), + url: z.string(), + source: FilePartSource.optional(), + }).meta({ + ref: "FilePart", + }) + export type FilePart = z.infer + + export const AgentPart = PartBase.extend({ + type: z.literal("agent"), + name: z.string(), + source: z + .object({ + value: z.string(), + start: z.number().int(), + end: z.number().int(), + }) + .optional(), + }).meta({ + ref: "AgentPart", + }) + export type AgentPart = z.infer + + export const CompactionPart = PartBase.extend({ + type: z.literal("compaction"), + auto: z.boolean(), + overflow: z.boolean().optional(), + }).meta({ + ref: "CompactionPart", + }) + export type CompactionPart = z.infer + + export const SubtaskPart = PartBase.extend({ + type: z.literal("subtask"), + prompt: z.string(), + description: z.string(), + agent: z.string(), + model: z + .object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, + }) + .optional(), + command: z.string().optional(), + }).meta({ + ref: "SubtaskPart", + }) + export type SubtaskPart = z.infer + + export const RetryPart = PartBase.extend({ + type: z.literal("retry"), + attempt: z.number(), + error: APIError.Schema, + time: z.object({ + created: z.number(), }), - ), - agent: Schema.String, - model: Schema.Struct({ - providerID: ProviderID, - modelID: ModelID, - variant: Schema.optional(Schema.String), - }), - system: Schema.optional(Schema.String), - tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)), -}) - .annotate({ identifier: "UserMessage" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type User = Types.DeepMutable> - -const _Part = Schema.Union([ - TextPart, - SubtaskPart, - ReasoningPart, - FilePart, - ToolPart, - StepStartPart, - StepFinishPart, - SnapshotPart, - PatchPart, - AgentPart, - RetryPart, - CompactionPart, -]).annotate({ discriminator: "type", identifier: "Part" }) -export const Part = Object.assign(_Part, { - zod: zod(_Part) as unknown as z.ZodType< - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - >, -}) -export type Part = - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -// Errors are still NamedError-based Zod; bridge via ZodOverride so the derived -// Zod + JSON Schema emit the original discriminatedUnion shape. Migrating the -// error classes to Schema.TaggedErrorClass is a separate slice. -const AssistantErrorZod = z.discriminatedUnion("name", [ - AuthError.Schema, - NamedError.Unknown.Schema, - OutputLengthError.Schema, - AbortedError.Schema, - StructuredOutputError.Schema, - ContextOverflowError.Schema, - APIError.Schema, -]) -type AssistantError = z.infer - -// ── Prompt input schemas ───────────────────────────────────────────────────── -// -// Consumers of `SessionPrompt.PromptInput.parts` send part drafts without the -// ambient IDs (`messageID`, `sessionID`) that live on stored parts, and may -// omit `id` to let the server allocate one. These Schema-Struct variants -// carry that shape, and `SessionPrompt.PromptInput` just references the -// derived `.zod` (no omit/partial gymnastics needed at the call site). - -export const TextPartInput = Schema.Struct({ - id: Schema.optional(PartID), - type: Schema.Literal("text"), - text: Schema.String, - synthetic: Schema.optional(Schema.Boolean), - ignored: Schema.optional(Schema.Boolean), - time: Schema.optional( - Schema.Struct({ - start: Schema.Number, - end: Schema.optional(Schema.Number), + }).meta({ + ref: "RetryPart", + }) + export type RetryPart = z.infer + + export const StepStartPart = PartBase.extend({ + type: z.literal("step-start"), + snapshot: z.string().optional(), + }).meta({ + ref: "StepStartPart", + }) + export type StepStartPart = z.infer + + export const StepFinishPart = PartBase.extend({ + type: z.literal("step-finish"), + reason: z.string(), + snapshot: z.string().optional(), + cost: z.number(), + tokens: z.object({ + total: z.number().optional(), + input: z.number(), + output: z.number(), + reasoning: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), }), - ), - metadata: Schema.optional(Schema.Record(Schema.String, Schema.Any)), -}) - .annotate({ identifier: "TextPartInput" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type TextPartInput = Types.DeepMutable> - -export const FilePartInput = Schema.Struct({ - id: Schema.optional(PartID), - type: Schema.Literal("file"), - mime: Schema.String, - filename: Schema.optional(Schema.String), - url: Schema.String, - source: Schema.optional(_FilePartSource), -}) - .annotate({ identifier: "FilePartInput" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type FilePartInput = Types.DeepMutable> - -export const AgentPartInput = Schema.Struct({ - id: Schema.optional(PartID), - type: Schema.Literal("agent"), - name: Schema.String, - source: Schema.optional( - Schema.Struct({ - value: Schema.String, - start: Schema.Int, - end: Schema.Int, + }).meta({ + ref: "StepFinishPart", + }) + export type StepFinishPart = z.infer + + export const ToolStatePending = z + .object({ + status: z.literal("pending"), + input: z.record(z.string(), z.any()), + raw: z.string(), + }) + .meta({ + ref: "ToolStatePending", + }) + + export type ToolStatePending = z.infer + + export const ToolStateRunning = z + .object({ + status: z.literal("running"), + input: z.record(z.string(), z.any()), + title: z.string().optional(), + metadata: z.record(z.string(), z.any()).optional(), + time: z.object({ + start: z.number(), + }), + }) + .meta({ + ref: "ToolStateRunning", + }) + export type ToolStateRunning = z.infer + + export const ToolStateCompleted = z + .object({ + status: z.literal("completed"), + input: z.record(z.string(), z.any()), + output: z.string(), + title: z.string(), + metadata: z.record(z.string(), z.any()), + time: z.object({ + start: z.number(), + end: z.number(), + compacted: z.number().optional(), + }), + attachments: FilePart.array().optional(), + }) + .meta({ + ref: "ToolStateCompleted", + }) + export type ToolStateCompleted = z.infer + + export const ToolStateError = z + .object({ + status: z.literal("error"), + input: z.record(z.string(), z.any()), + error: z.string(), + metadata: z.record(z.string(), z.any()).optional(), + time: z.object({ + start: z.number(), + end: z.number(), + }), + }) + .meta({ + ref: "ToolStateError", + }) + export type ToolStateError = z.infer + + export const ToolState = z + .discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) + .meta({ + ref: "ToolState", + }) + + export const ToolPart = PartBase.extend({ + type: z.literal("tool"), + callID: z.string(), + tool: z.string(), + state: ToolState, + metadata: z.record(z.string(), z.any()).optional(), + }).meta({ + ref: "ToolPart", + }) + export type ToolPart = z.infer + + const Base = z.object({ + id: MessageID.zod, + sessionID: SessionID.zod, + }) + + export const User = Base.extend({ + role: z.literal("user"), + time: z.object({ + created: z.number(), }), - ), -}) - .annotate({ identifier: "AgentPartInput" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type AgentPartInput = Types.DeepMutable> - -export const SubtaskPartInput = Schema.Struct({ - id: Schema.optional(PartID), - type: Schema.Literal("subtask"), - prompt: Schema.String, - description: Schema.String, - agent: Schema.String, - model: Schema.optional( - Schema.Struct({ - providerID: ProviderID, - modelID: ModelID, + format: Format.optional(), + summary: z + .object({ + title: z.string().optional(), + body: z.string().optional(), + diffs: Snapshot.FileDiff.array(), + }) + .optional(), + agent: z.string(), + model: z.object({ + providerID: ProviderID.zod, + modelID: ModelID.zod, }), - ), - command: Schema.optional(Schema.String), -}) - .annotate({ identifier: "SubtaskPartInput" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type SubtaskPartInput = Types.DeepMutable> - -export const Assistant = Schema.Struct({ - ...messageBase, - role: Schema.Literal("assistant"), - time: Schema.Struct({ - created: Schema.Number, - completed: Schema.optional(Schema.Number), - }), - error: Schema.optional(Schema.Any.annotate({ [ZodOverride]: AssistantErrorZod })), - parentID: MessageID, - modelID: ModelID, - providerID: ProviderID, - /** - * @deprecated - */ - mode: Schema.String, - agent: Schema.String, - path: Schema.Struct({ - cwd: Schema.String, - root: Schema.String, - }), - summary: Schema.optional(Schema.Boolean), - cost: Schema.Number, - tokens: Schema.Struct({ - total: Schema.optional(Schema.Number), - input: Schema.Number, - output: Schema.Number, - reasoning: Schema.Number, - cache: Schema.Struct({ - read: Schema.Number, - write: Schema.Number, + system: z.string().optional(), + tools: z.record(z.string(), z.boolean()).optional(), + variant: z.string().optional(), + }).meta({ + ref: "UserMessage", + }) + export type User = z.infer + + export const Part = z + .discriminatedUnion("type", [ + TextPart, + SubtaskPart, + ReasoningPart, + FilePart, + ToolPart, + StepStartPart, + StepFinishPart, + SnapshotPart, + PatchPart, + AgentPart, + RetryPart, + CompactionPart, + ]) + .meta({ + ref: "Part", + }) + export type Part = z.infer + + export const Assistant = Base.extend({ + role: z.literal("assistant"), + time: z.object({ + created: z.number(), + completed: z.number().optional(), }), - }), - structured: Schema.optional(Schema.Any), - variant: Schema.optional(Schema.String), - finish: Schema.optional(Schema.String), -}) - .annotate({ identifier: "AssistantMessage" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Assistant = Omit>, "error"> & { - error?: AssistantError -} - -const _Info = Schema.Union([User, Assistant]).annotate({ discriminator: "role", identifier: "Message" }) -export const Info = Object.assign(_Info, { - zod: zod(_Info) as unknown as z.ZodType, -}) -export type Info = User | Assistant - -const UpdatedEventSchema = Schema.Struct({ - sessionID: SessionID, - info: _Info, -}) - -const RemovedEventSchema = Schema.Struct({ - sessionID: SessionID, - messageID: MessageID, -}) - -const PartUpdatedEventSchema = Schema.Struct({ - sessionID: SessionID, - part: _Part, - time: Schema.Number, -}) - -const PartRemovedEventSchema = Schema.Struct({ - sessionID: SessionID, - messageID: MessageID, - partID: PartID, -}) - -export const Event = { - Updated: SyncEvent.define({ - type: "message.updated", - version: 1, - aggregate: "sessionID", - schema: UpdatedEventSchema, - }), - Removed: SyncEvent.define({ - type: "message.removed", - version: 1, - aggregate: "sessionID", - schema: RemovedEventSchema, - }), - PartUpdated: SyncEvent.define({ - type: "message.part.updated", - version: 1, - aggregate: "sessionID", - schema: PartUpdatedEventSchema, - }), - PartDelta: BusEvent.define( - "message.part.delta", - Schema.Struct({ - sessionID: SessionID, - messageID: MessageID, - partID: PartID, - field: Schema.String, - delta: Schema.String, + error: z + .discriminatedUnion("name", [ + AuthError.Schema, + NamedError.Unknown.Schema, + OutputLengthError.Schema, + AbortedError.Schema, + StructuredOutputError.Schema, + ContextOverflowError.Schema, + APIError.Schema, + ]) + .optional(), + parentID: MessageID.zod, + modelID: ModelID.zod, + providerID: ProviderID.zod, + /** + * @deprecated + */ + mode: z.string(), + agent: z.string(), + path: z.object({ + cwd: z.string(), + root: z.string(), }), - ), - PartRemoved: SyncEvent.define({ - type: "message.part.removed", - version: 1, - aggregate: "sessionID", - schema: PartRemovedEventSchema, - }), -} - -export const WithParts = Schema.Struct({ - info: _Info, - parts: Schema.Array(_Part), -}).pipe(withStatics((s) => ({ zod: zod(s) }))) -export type WithParts = { - info: Info - parts: Part[] -} - -const Cursor = Schema.Struct({ - id: MessageID, - time: Schema.Number, -}) -type Cursor = typeof Cursor.Type - -const decodeCursor = Schema.decodeUnknownSync(Cursor) - -export const cursor = { - encode(input: Cursor) { - return Buffer.from(JSON.stringify(input)).toString("base64url") - }, - decode(input: string) { - return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) - }, -} - -const info = (row: typeof MessageTable.$inferSelect) => - ({ - ...row.data, - id: row.id, - sessionID: row.session_id, - }) as Info - -const part = (row: typeof PartTable.$inferSelect) => - ({ - ...row.data, - id: row.id, - sessionID: row.session_id, - messageID: row.message_id, - }) as Part - -const older = (row: Cursor) => - or(lt(MessageTable.time_created, row.time), and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id))) - -function hydrate(rows: (typeof MessageTable.$inferSelect)[]) { - const ids = rows.map((row) => row.id) - const partByMessage = new Map() - if (ids.length > 0) { - const partRows = Database.use((db) => - db - .select() - .from(PartTable) - .where(inArray(PartTable.message_id, ids)) - .orderBy(PartTable.message_id, PartTable.id) - .all(), - ) - for (const row of partRows) { - const next = part(row) - const list = partByMessage.get(row.message_id) - if (list) list.push(next) - else partByMessage.set(row.message_id, [next]) - } + summary: z.boolean().optional(), + cost: z.number(), + tokens: z.object({ + total: z.number().optional(), + input: z.number(), + output: z.number(), + reasoning: z.number(), + cache: z.object({ + read: z.number(), + write: z.number(), + }), + }), + structured: z.any().optional(), + variant: z.string().optional(), + finish: z.string().optional(), + }).meta({ + ref: "AssistantMessage", + }) + export type Assistant = z.infer + + export const Info = z.discriminatedUnion("role", [User, Assistant]).meta({ + ref: "Message", + }) + export type Info = z.infer + + export const Event = { + Updated: BusEvent.define( + "message.updated", + z.object({ + info: Info, + }), + ), + Removed: BusEvent.define( + "message.removed", + z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, + }), + ), + PartUpdated: BusEvent.define( + "message.part.updated", + z.object({ + part: Part, + }), + ), + PartDelta: BusEvent.define( + "message.part.delta", + z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, + partID: PartID.zod, + field: z.string(), + delta: z.string(), + }), + ), + PartRemoved: BusEvent.define( + "message.part.removed", + z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, + partID: PartID.zod, + }), + ), } - return rows.map((row) => ({ - info: info(row), - parts: partByMessage.get(row.id) ?? [], - })) -} + export const WithParts = z.object({ + info: Info, + parts: z.array(Part), + }) + export type WithParts = z.infer + + const Cursor = z.object({ + id: MessageID.zod, + time: z.number(), + }) + type Cursor = z.infer + + export const cursor = { + encode(input: Cursor) { + return Buffer.from(JSON.stringify(input)).toString("base64url") + }, + decode(input: string) { + return Cursor.parse(JSON.parse(Buffer.from(input, "base64url").toString("utf8"))) + }, + } -function providerMeta(metadata: Record | undefined) { - if (!metadata) return undefined - const { providerExecuted: _, ...rest } = metadata - return Object.keys(rest).length > 0 ? rest : undefined -} + const info = (row: typeof MessageTable.$inferSelect) => + ({ + ...row.data, + id: row.id, + sessionID: row.session_id, + }) as MessageV2.Info + + const part = (row: typeof PartTable.$inferSelect) => + ({ + ...row.data, + id: row.id, + sessionID: row.session_id, + messageID: row.message_id, + }) as MessageV2.Part + + const older = (row: Cursor) => + or( + lt(MessageTable.time_created, row.time), + and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id)), + ) -export const toModelMessagesEffect = Effect.fnUntraced(function* ( - input: WithParts[], - model: Provider.Model, - options?: { stripMedia?: boolean; toolOutputMaxChars?: number }, -) { - const result: UIMessage[] = [] - const toolNames = new Set() - // Track media from tool results that need to be injected as user messages - // for providers that don't support media in tool results. - // - // OpenAI-compatible APIs only support string content in tool results, so we need - // to extract media and inject as user messages. Other SDKs (anthropic, google, - // bedrock) handle type: "content" with media parts natively. - // - // Only apply this workaround if the model actually supports image input - - // otherwise there's no point extracting images. - const supportsMediaInToolResults = (() => { - if (model.api.npm === "@ai-sdk/anthropic") return true - if (model.api.npm === "@ai-sdk/openai") return true - if (model.api.npm === "@ai-sdk/amazon-bedrock") return true - if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true - if (model.api.npm === "@ai-sdk/google") { - const id = model.api.id.toLowerCase() - return id.includes("gemini-3") && !id.includes("gemini-2") + async function hydrate(rows: (typeof MessageTable.$inferSelect)[]) { + const ids = rows.map((row) => row.id) + const partByMessage = new Map() + if (ids.length > 0) { + const partRows = Database.use((db) => + db + .select() + .from(PartTable) + .where(inArray(PartTable.message_id, ids)) + .orderBy(PartTable.message_id, PartTable.id) + .all(), + ) + for (const row of partRows) { + const next = part(row) + const list = partByMessage.get(row.message_id) + if (list) list.push(next) + else partByMessage.set(row.message_id, [next]) + } } - return false - })() - const toModelOutput = (options: { toolCallId: string; input: unknown; output: unknown }) => { - const output = options.output - if (typeof output === "string") { - return { type: "text", value: output } - } + return rows.map((row) => ({ + info: info(row), + parts: partByMessage.get(row.id) ?? [], + })) + } - if (typeof output === "object") { - const outputObject = output as { - text: string - attachments?: Array<{ mime: string; url: string }> + export function toModelMessages( + input: WithParts[], + model: Provider.Model, + options?: { stripMedia?: boolean }, + ): ModelMessage[] { + const result: UIMessage[] = [] + const toolNames = new Set() + // Track media from tool results that need to be injected as user messages + // for providers that don't support media in tool results. + // + // OpenAI-compatible APIs only support string content in tool results, so we need + // to extract media and inject as user messages. Other SDKs (anthropic, google, + // bedrock) handle type: "content" with media parts natively. + // + // Only apply this workaround if the model actually supports image input - + // otherwise there's no point extracting images. + const supportsMediaInToolResults = (() => { + if (model.api.npm === "@ai-sdk/anthropic") return true + if (model.api.npm === "@ai-sdk/openai") return true + if (model.api.npm === "@ai-sdk/amazon-bedrock") return true + if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true + if (model.api.npm === "@ai-sdk/google") { + const id = model.api.id.toLowerCase() + return id.includes("gemini-3") && !id.includes("gemini-2") } - const attachments = (outputObject.attachments ?? []).filter((attachment) => { - return attachment.url.startsWith("data:") && attachment.url.includes(",") - }) + return false + })() - return { - type: "content", - value: [ - { type: "text", text: outputObject.text }, - ...attachments.map((attachment) => ({ - type: "media", - mediaType: attachment.mime, - data: iife(() => { - const commaIndex = attachment.url.indexOf(",") - return commaIndex === -1 ? attachment.url : attachment.url.slice(commaIndex + 1) - }), - })), - ], + const toModelOutput = (output: unknown) => { + if (typeof output === "string") { + return { type: "text", value: output } } + + if (typeof output === "object") { + const outputObject = output as { + text: string + attachments?: Array<{ mime: string; url: string }> + } + const attachments = (outputObject.attachments ?? []).filter((attachment) => { + return attachment.url.startsWith("data:") && attachment.url.includes(",") + }) + + return { + type: "content", + value: [ + { type: "text", text: outputObject.text }, + ...attachments.map((attachment) => ({ + type: "media", + mediaType: attachment.mime, + data: iife(() => { + const commaIndex = attachment.url.indexOf(",") + return commaIndex === -1 ? attachment.url : attachment.url.slice(commaIndex + 1) + }), + })), + ], + } + } + + return { type: "json", value: output as never } } - return { type: "json", value: output as never } - } + for (const msg of input) { + if (msg.parts.length === 0) continue - for (const msg of input) { - if (msg.parts.length === 0) continue + if (msg.info.role === "user") { + const userMessage: UIMessage = { + id: msg.info.id, + role: "user", + parts: [], + } + result.push(userMessage) + for (const part of msg.parts) { + if (part.type === "text" && !part.ignored) + userMessage.parts.push({ + type: "text", + text: part.text, + }) + // text/plain and directory files are converted into text parts, ignore them + if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") { + if (options?.stripMedia && isMedia(part.mime)) { + userMessage.parts.push({ + type: "text", + text: `[Attached ${part.mime}: ${part.filename ?? "file"}]`, + }) + } else { + userMessage.parts.push({ + type: "file", + url: part.url, + mediaType: part.mime, + filename: part.filename, + }) + } + } - if (msg.info.role === "user") { - const userMessage: UIMessage = { - id: msg.info.id, - role: "user", - parts: [], - } - result.push(userMessage) - for (const part of msg.parts) { - if (part.type === "text" && !part.ignored) - userMessage.parts.push({ - type: "text", - text: part.text, - }) - // text/plain and directory files are converted into text parts, ignore them - if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") { - if (options?.stripMedia && isMedia(part.mime)) { + if (part.type === "compaction") { userMessage.parts.push({ type: "text", - text: `[Attached ${part.mime}: ${part.filename ?? "file"}]`, + text: "What did we do so far?", }) - } else { + } + if (part.type === "subtask") { userMessage.parts.push({ - type: "file", - url: part.url, - mediaType: part.mime, - filename: part.filename, + type: "text", + text: "The following tool was executed by the user", }) } } + } - if (part.type === "compaction") { - userMessage.parts.push({ - type: "text", - text: "What did we do so far?", - }) + if (msg.info.role === "assistant") { + const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}` + const media: Array<{ mime: string; url: string }> = [] + + if ( + msg.info.error && + !( + MessageV2.AbortedError.isInstance(msg.info.error) && + msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning") + ) + ) { + continue } - if (part.type === "subtask") { - userMessage.parts.push({ - type: "text", - text: "The following tool was executed by the user", - }) + const assistantMessage: UIMessage = { + id: msg.info.id, + role: "assistant", + parts: [], } - } - } - - if (msg.info.role === "assistant") { - const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}` - const media: Array<{ mime: string; url: string }> = [] - - if ( - msg.info.error && - !( - AbortedError.isInstance(msg.info.error) && - msg.parts.some((part) => part.type !== "step-start" && part.type !== "reasoning") - ) - ) { - continue - } - const assistantMessage: UIMessage = { - id: msg.info.id, - role: "assistant", - parts: [], - } - for (const part of msg.parts) { - if (part.type === "text") - assistantMessage.parts.push({ - type: "text", - text: part.text, - ...(differentModel ? {} : { providerMetadata: part.metadata }), - }) - if (part.type === "step-start") - assistantMessage.parts.push({ - type: "step-start", - }) - if (part.type === "tool") { - toolNames.add(part.tool) - if (part.state.status === "completed") { - const outputText = part.state.time.compacted - ? "[Old tool result content cleared]" - : truncateToolOutput(part.state.output, options?.toolOutputMaxChars) - const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? []) - - // For providers that don't support media in tool results, extract media files - // (images, PDFs) to be sent as a separate user message - const mediaAttachments = attachments.filter((a) => isMedia(a.mime)) - const nonMediaAttachments = attachments.filter((a) => !isMedia(a.mime)) - if (!supportsMediaInToolResults && mediaAttachments.length > 0) { - media.push(...mediaAttachments) - } - const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments - - const output = - finalAttachments.length > 0 - ? { - text: outputText, - attachments: finalAttachments, - } - : outputText - + for (const part of msg.parts) { + if (part.type === "text") assistantMessage.parts.push({ - type: ("tool-" + part.tool) as `tool-${string}`, - state: "output-available", - toolCallId: part.callID, - input: part.state.input, - output, - ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), - ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), + type: "text", + text: part.text, + ...(differentModel ? {} : { providerMetadata: part.metadata }), }) - } - if (part.state.status === "error") { - const output = part.state.metadata?.interrupted === true ? part.state.metadata.output : undefined - if (typeof output === "string") { + if (part.type === "step-start") + assistantMessage.parts.push({ + type: "step-start", + }) + if (part.type === "tool") { + toolNames.add(part.tool) + if (part.state.status === "completed") { + const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output + const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? []) + + // For providers that don't support media in tool results, extract media files + // (images, PDFs) to be sent as a separate user message + const mediaAttachments = attachments.filter((a) => isMedia(a.mime)) + const nonMediaAttachments = attachments.filter((a) => !isMedia(a.mime)) + if (!supportsMediaInToolResults && mediaAttachments.length > 0) { + media.push(...mediaAttachments) + } + const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments + + const output = + finalAttachments.length > 0 + ? { + text: outputText, + attachments: finalAttachments, + } + : outputText + assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-available", toolCallId: part.callID, input: part.state.input, output, - ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), - ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), + ...(differentModel ? {} : { callProviderMetadata: part.metadata }), }) - } else { + } + if (part.state.status === "error") assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-error", toolCallId: part.callID, input: part.state.input, errorText: part.state.error, - ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), - ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), + ...(differentModel ? {} : { callProviderMetadata: part.metadata }), + }) + // Handle pending/running tool calls to prevent dangling tool_use blocks + // Anthropic/Claude APIs require every tool_use to have a corresponding tool_result + if (part.state.status === "pending" || part.state.status === "running") + assistantMessage.parts.push({ + type: ("tool-" + part.tool) as `tool-${string}`, + state: "output-error", + toolCallId: part.callID, + input: part.state.input, + errorText: "[Tool execution was interrupted]", + ...(differentModel ? {} : { callProviderMetadata: part.metadata }), }) - } } - // Handle pending/running tool calls to prevent dangling tool_use blocks - // Anthropic/Claude APIs require every tool_use to have a corresponding tool_result - if (part.state.status === "pending" || part.state.status === "running") + if (part.type === "reasoning") { assistantMessage.parts.push({ - type: ("tool-" + part.tool) as `tool-${string}`, - state: "output-error", - toolCallId: part.callID, - input: part.state.input, - errorText: "[Tool execution was interrupted]", - ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), - ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }), + type: "reasoning", + text: part.text, + ...(differentModel ? {} : { providerMetadata: part.metadata }), }) + } } - if (part.type === "reasoning") { - assistantMessage.parts.push({ - type: "reasoning", - text: part.text, - ...(differentModel ? {} : { providerMetadata: part.metadata }), - }) - } - } - if (assistantMessage.parts.length > 0) { - result.push(assistantMessage) - // Inject pending media as a user message for providers that don't support - // media (images, PDFs) in tool results - if (media.length > 0) { - result.push({ - id: MessageID.ascending(), - role: "user", - parts: [ - { - type: "text" as const, - text: SYNTHETIC_ATTACHMENT_PROMPT, - }, - ...media.map((attachment) => ({ - type: "file" as const, - url: attachment.url, - mediaType: attachment.mime, - })), - ], - }) + if (assistantMessage.parts.length > 0) { + result.push(assistantMessage) + // Inject pending media as a user message for providers that don't support + // media (images, PDFs) in tool results + if (media.length > 0) { + result.push({ + id: MessageID.ascending(), + role: "user", + parts: [ + { + type: "text" as const, + text: "Attached image(s) from tool result:", + }, + ...media.map((attachment) => ({ + type: "file" as const, + url: attachment.url, + mediaType: attachment.mime, + })), + ], + }) + } } } } - } - const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }])) + const tools = Object.fromEntries(Array.from(toolNames).map((toolName) => [toolName, { toModelOutput }])) - return yield* Effect.promise(() => - convertToModelMessages( + return convertToModelMessages( result.filter((msg) => msg.parts.some((part) => part.type !== "step-start")), { //@ts-expect-error (convertToModelMessages expects a ToolSet but only actually needs tools[name]?.toModelOutput) tools, }, - ), - ) -}) - -export function toModelMessages( - input: WithParts[], - model: Provider.Model, - options?: { stripMedia?: boolean; toolOutputMaxChars?: number }, -): Promise { - return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer))) -} - -export function page(input: { sessionID: SessionID; limit: number; before?: string }) { - const before = input.before ? cursor.decode(input.before) : undefined - const where = before - ? and(eq(MessageTable.session_id, input.sessionID), older(before)) - : eq(MessageTable.session_id, input.sessionID) - const rows = Database.use((db) => - db - .select() - .from(MessageTable) - .where(where) - .orderBy(desc(MessageTable.time_created), desc(MessageTable.id)) - .limit(input.limit + 1) - .all(), - ) - if (rows.length === 0) { - const row = Database.use((db) => - db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.id, input.sessionID)).get(), ) - if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) - return { - items: [] as WithParts[], - more: false, - } } - const more = rows.length > input.limit - const slice = more ? rows.slice(0, input.limit) : rows - const items = hydrate(slice) - items.reverse() - const tail = slice.at(-1) - return { - items, - more, - cursor: more && tail ? cursor.encode({ id: tail.id, time: tail.time_created }) : undefined, - } -} + export const page = fn( + z.object({ + sessionID: SessionID.zod, + limit: z.number().int().positive(), + before: z.string().optional(), + }), + async (input) => { + const before = input.before ? cursor.decode(input.before) : undefined + const where = before + ? and(eq(MessageTable.session_id, input.sessionID), older(before)) + : eq(MessageTable.session_id, input.sessionID) + const rows = Database.use((db) => + db + .select() + .from(MessageTable) + .where(where) + .orderBy(desc(MessageTable.time_created), desc(MessageTable.id)) + .limit(input.limit + 1) + .all(), + ) + if (rows.length === 0) { + const row = Database.use((db) => + db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.id, input.sessionID)).get(), + ) + if (!row) throw new NotFoundError({ message: `Session not found: ${input.sessionID}` }) + return { + items: [] as MessageV2.WithParts[], + more: false, + } + } -export function* stream(sessionID: SessionID) { - const size = 50 - let before: string | undefined - while (true) { - const next = page({ sessionID, limit: size, before }) - if (next.items.length === 0) break - for (let i = next.items.length - 1; i >= 0; i--) { - yield next.items[i] + const more = rows.length > input.limit + const page = more ? rows.slice(0, input.limit) : rows + const items = await hydrate(page) + items.reverse() + const tail = page.at(-1) + return { + items, + more, + cursor: more && tail ? cursor.encode({ id: tail.id, time: tail.time_created }) : undefined, + } + }, + ) + + export const stream = fn(SessionID.zod, async function* (sessionID) { + const size = 50 + let before: string | undefined + while (true) { + const next = await page({ sessionID, limit: size, before }) + if (next.items.length === 0) break + for (let i = next.items.length - 1; i >= 0; i--) { + yield next.items[i] + } + if (!next.more || !next.cursor) break + before = next.cursor } - if (!next.more || !next.cursor) break - before = next.cursor - } -} + }) -export function parts(message_id: MessageID) { - const rows = Database.use((db) => - db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), - ) - return rows.map( - (row) => - ({ - ...row.data, - id: row.id, - sessionID: row.session_id, - messageID: row.message_id, - }) as Part, - ) -} + export const parts = fn(MessageID.zod, async (message_id) => { + const rows = Database.use((db) => + db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), + ) + return rows.map( + (row) => ({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id }) as MessageV2.Part, + ) + }) -export function get(input: { sessionID: SessionID; messageID: MessageID }): WithParts { - const row = Database.use((db) => - db - .select() - .from(MessageTable) - .where(and(eq(MessageTable.id, input.messageID), eq(MessageTable.session_id, input.sessionID))) - .get(), + export const get = fn( + z.object({ + sessionID: SessionID.zod, + messageID: MessageID.zod, + }), + async (input): Promise => { + const row = Database.use((db) => + db + .select() + .from(MessageTable) + .where(and(eq(MessageTable.id, input.messageID), eq(MessageTable.session_id, input.sessionID))) + .get(), + ) + if (!row) throw new NotFoundError({ message: `Message not found: ${input.messageID}` }) + return { + info: info(row), + parts: await parts(input.messageID), + } + }, ) - if (!row) throw new NotFoundError({ message: `Message not found: ${input.messageID}` }) - return { - info: info(row), - parts: parts(input.messageID), - } -} -export function filterCompacted(msgs: Iterable) { - const result = [] as WithParts[] - const completed = new Set() - let retain: MessageID | undefined - for (const msg of msgs) { - result.push(msg) - if (retain) { - if (msg.info.id === retain) break - continue - } - if (msg.info.role === "user" && completed.has(msg.info.id)) { - const part = msg.parts.find((item): item is CompactionPart => item.type === "compaction") - if (!part) continue - if (!part.tail_start_id) break - retain = part.tail_start_id - if (msg.info.id === retain) break - continue + export async function filterCompacted(stream: AsyncIterable) { + const result = [] as MessageV2.WithParts[] + const completed = new Set() + for await (const msg of stream) { + result.push(msg) + if ( + msg.info.role === "user" && + completed.has(msg.info.id) && + msg.parts.some((part) => part.type === "compaction") + ) + break + if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error) + completed.add(msg.info.parentID) } - if (msg.info.role === "user" && completed.has(msg.info.id) && msg.parts.some((part) => part.type === "compaction")) - break - if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish && !msg.info.error) - completed.add(msg.info.parentID) + result.reverse() + return result } - result.reverse() - return result -} -export const filterCompactedEffect = Effect.fnUntraced(function* (sessionID: SessionID) { - return filterCompacted(stream(sessionID)) -}) - -export function fromError( - e: unknown, - ctx: { providerID: ProviderID; aborted?: boolean }, -): NonNullable { - switch (true) { - case e instanceof DOMException && e.name === "AbortError": - return new AbortedError( - { message: e.message }, - { - cause: e, - }, - ).toObject() - case OutputLengthError.isInstance(e): - return e - case LoadAPIKeyError.isInstance(e): - return new AuthError( - { - providerID: ctx.providerID, - message: e.message, - }, - { cause: e }, - ).toObject() - case (e as SystemError)?.code === "ECONNRESET": - return new APIError( - { - message: "Connection reset by server", - isRetryable: true, - metadata: { - code: (e as SystemError).code ?? "", - syscall: (e as SystemError).syscall ?? "", - message: (e as SystemError).message ?? "", + export function fromError(e: unknown, ctx: { providerID: ProviderID }): NonNullable { + switch (true) { + case e instanceof DOMException && e.name === "AbortError": + return new MessageV2.AbortedError( + { message: e.message }, + { + cause: e, }, - }, - { cause: e }, - ).toObject() - case e instanceof Error && (e as FetchDecompressionError).code === "ZlibError": - if (ctx.aborted) { - return new AbortedError({ message: e.message }, { cause: e }).toObject() - } - return new APIError( - { - message: "Response decompression failed", - isRetryable: true, - metadata: { - code: (e as FetchDecompressionError).code, + ).toObject() + case MessageV2.OutputLengthError.isInstance(e): + return e + case LoadAPIKeyError.isInstance(e): + return new MessageV2.AuthError( + { + providerID: ctx.providerID, message: e.message, }, - }, - { cause: e }, - ).toObject() - case APICallError.isInstance(e): - const parsed = ProviderError.parseAPICallError({ - providerID: ctx.providerID, - error: e, - }) - if (parsed.type === "context_overflow") { - return new ContextOverflowError( + { cause: e }, + ).toObject() + case NETWORK_ERROR_CODES.has((e as SystemError)?.code ?? ""): + return new MessageV2.APIError( + { + message: "Network error", + isRetryable: true, + metadata: { + code: (e as SystemError).code ?? "", + syscall: (e as SystemError).syscall ?? "", + message: (e as SystemError).message ?? "", + }, + }, + { cause: e }, + ).toObject() + case e instanceof Error && e.message === "SSE read timed out": + return new MessageV2.APIError( + { + message: "SSE read timed out", + isRetryable: true, + metadata: { + message: e.message, + }, + }, + { cause: e }, + ).toObject() + case APICallError.isInstance(e): + const parsed = ProviderError.parseAPICallError({ + providerID: ctx.providerID, + error: e, + }) + if (parsed.type === "context_overflow") { + return new MessageV2.ContextOverflowError( + { + message: parsed.message, + responseBody: parsed.responseBody, + }, + { cause: e }, + ).toObject() + } + + return new MessageV2.APIError( { message: parsed.message, + statusCode: parsed.statusCode, + isRetryable: parsed.isRetryable, + responseHeaders: parsed.responseHeaders, responseBody: parsed.responseBody, + metadata: parsed.metadata, }, { cause: e }, ).toObject() - } - - return new APIError( - { - message: parsed.message, - statusCode: parsed.statusCode, - isRetryable: parsed.isRetryable, - responseHeaders: parsed.responseHeaders, - responseBody: parsed.responseBody, - metadata: parsed.metadata, - }, - { cause: e }, - ).toObject() - case e instanceof Error: - return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject() - default: - try { - const parsed = ProviderError.parseStreamError(e) - if (parsed) { - if (parsed.type === "context_overflow") { - return new ContextOverflowError( + case e instanceof Error: + return new NamedError.Unknown({ message: e instanceof Error ? e.message : String(e) }, { cause: e }).toObject() + default: + try { + const parsed = ProviderError.parseStreamError(e) + if (parsed) { + if (parsed.type === "context_overflow") { + return new MessageV2.ContextOverflowError( + { + message: parsed.message, + responseBody: parsed.responseBody, + }, + { cause: e }, + ).toObject() + } + return new MessageV2.APIError( { message: parsed.message, + isRetryable: parsed.isRetryable, responseBody: parsed.responseBody, }, - { cause: e }, + { + cause: e, + }, ).toObject() } - return new APIError( - { - message: parsed.message, - isRetryable: parsed.isRetryable, - responseBody: parsed.responseBody, - }, - { - cause: e, - }, - ).toObject() - } - } catch {} - return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject() + } catch {} + return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject() + } } } - -export * as MessageV2 from "./message-v2" diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index 21f9329c6fce..fd0a02fdcb75 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -1,619 +1,501 @@ -import { Cause, Deferred, Effect, Layer, Context, Scope } from "effect" -import * as Stream from "effect/Stream" +import { MessageV2 } from "./message-v2" +import { Log } from "@/util/log" +import { Session } from "." import { Agent } from "@/agent/agent" -import { Bus } from "@/bus" -import { Config } from "@/config" -import { Permission } from "@/permission" -import { Plugin } from "@/plugin" import { Snapshot } from "@/snapshot" -import * as Session from "./session" -import { LLM } from "./llm" -import { MessageV2 } from "./message-v2" -import { isOverflow } from "./overflow" -import { PartID } from "./schema" -import type { SessionID } from "./schema" +import { SessionSummary } from "./summary" +import { Bus } from "@/bus" import { SessionRetry } from "./retry" import { SessionStatus } from "./status" -import { SessionSummary } from "./summary" -import type { Provider } from "@/provider" +import { Plugin } from "@/plugin" +import type { Provider } from "@/provider/provider" +import { LLM } from "./llm" +import { Config } from "@/config/config" +import { SessionCompaction } from "./compaction" +import { Permission } from "@/permission" import { Question } from "@/question" -import { errorMessage } from "@/util/error" -import { Log } from "@/util" -import { isRecord } from "@/util/record" - -const DOOM_LOOP_THRESHOLD = 3 -const log = Log.create({ service: "session.processor" }) - -export type Result = "compact" | "stop" | "continue" - -export type Event = LLM.Event - -export interface Handle { - readonly message: MessageV2.Assistant - readonly updateToolCall: ( - toolCallID: string, - update: (part: MessageV2.ToolPart) => MessageV2.ToolPart, - ) => Effect.Effect - readonly completeToolCall: ( - toolCallID: string, - output: { - title: string - metadata: Record - output: string - attachments?: MessageV2.FilePart[] - }, - ) => Effect.Effect - readonly process: (streamInput: LLM.StreamInput) => Effect.Effect -} - -type Input = { - assistantMessage: MessageV2.Assistant - sessionID: SessionID - model: Provider.Model -} - -export interface Interface { - readonly create: (input: Input) => Effect.Effect -} - -type ToolCall = { - partID: MessageV2.ToolPart["id"] - messageID: MessageV2.ToolPart["messageID"] - sessionID: MessageV2.ToolPart["sessionID"] - done: Deferred.Deferred -} - -interface ProcessorContext extends Input { - toolcalls: Record - shouldBreak: boolean - snapshot: string | undefined - blocked: boolean - needsCompaction: boolean - currentText: MessageV2.TextPart | undefined - reasoningMap: Record -} - -type StreamEvent = Event - -export class Service extends Context.Service()("@opencode/SessionProcessor") {} - -export const layer: Layer.Layer< - Service, - never, - | Session.Service - | Config.Service - | Bus.Service - | Snapshot.Service - | Agent.Service - | LLM.Service - | Permission.Service - | Plugin.Service - | SessionSummary.Service - | SessionStatus.Service -> = Layer.effect( - Service, - Effect.gen(function* () { - const session = yield* Session.Service - const config = yield* Config.Service - const bus = yield* Bus.Service - const snapshot = yield* Snapshot.Service - const agents = yield* Agent.Service - const llm = yield* LLM.Service - const permission = yield* Permission.Service - const plugin = yield* Plugin.Service - const summary = yield* SessionSummary.Service - const scope = yield* Scope.Scope - const status = yield* SessionStatus.Service - - const create = Effect.fn("SessionProcessor.create")(function* (input: Input) { - // Pre-capture snapshot before the LLM stream starts. The AI SDK - // may execute tools internally before emitting start-step events, - // so capturing inside the event handler can be too late. - const initialSnapshot = yield* snapshot.track() - const ctx: ProcessorContext = { - assistantMessage: input.assistantMessage, - sessionID: input.sessionID, - model: input.model, - toolcalls: {}, - shouldBreak: false, - snapshot: initialSnapshot, - blocked: false, - needsCompaction: false, - currentText: undefined, - reasoningMap: {}, - } - let aborted = false - const slog = log.clone().tag("session.id", input.sessionID).tag("messageID", input.assistantMessage.id) - - const parse = (e: unknown) => - MessageV2.fromError(e, { - providerID: input.model.providerID, - aborted, - }) - - const settleToolCall = Effect.fn("SessionProcessor.settleToolCall")(function* (toolCallID: string) { - const done = ctx.toolcalls[toolCallID]?.done - delete ctx.toolcalls[toolCallID] - if (done) yield* Deferred.succeed(done, undefined).pipe(Effect.ignore) - }) - - const readToolCall = Effect.fn("SessionProcessor.readToolCall")(function* (toolCallID: string) { - const call = ctx.toolcalls[toolCallID] - if (!call) return - const part = yield* session.getPart({ - partID: call.partID, - messageID: call.messageID, - sessionID: call.sessionID, - }) - if (!part || part.type !== "tool") { - delete ctx.toolcalls[toolCallID] - return +import { PartID } from "./schema" +import type { SessionID, MessageID } from "./schema" + +export namespace SessionProcessor { + const DOOM_LOOP_THRESHOLD = 3 + const MAX_NETWORK_RETRIES = 5 + const log = Log.create({ service: "session.processor" }) + + export type Info = Awaited> + export type Result = Awaited> + + export function create(input: { + assistantMessage: MessageV2.Assistant + sessionID: SessionID + model: Provider.Model + abort: AbortSignal + }) { + const toolcalls: Record = {} + let snapshot: string | undefined + let blocked = false + let attempt = 0 + let networkAttempt = 0 + let receivedChunk = false + let needsCompaction = false + const cleanup = async () => { + const parts = await MessageV2.parts(input.assistantMessage.id) + for (const part of parts) { + if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") { + await Session.removePart({ + sessionID: input.sessionID, + messageID: input.assistantMessage.id, + partID: part.id, + }) + continue } - return { call, part } - }) - - const updateToolCall = Effect.fn("SessionProcessor.updateToolCall")(function* ( - toolCallID: string, - update: (part: MessageV2.ToolPart) => MessageV2.ToolPart, - ) { - const match = yield* readToolCall(toolCallID) - if (!match) return - const part = yield* session.updatePart(update(match.part)) - ctx.toolcalls[toolCallID] = { - ...match.call, - partID: part.id, - messageID: part.messageID, - sessionID: part.sessionID, + if (part.type === "text") { + await Session.updatePart({ + ...part, + text: "", + time: part.time + ? { + start: part.time.start, + } + : undefined, + }) + continue } - return part - }) - - const completeToolCall = Effect.fn("SessionProcessor.completeToolCall")(function* ( - toolCallID: string, - output: { - title: string - metadata: Record - output: string - attachments?: MessageV2.FilePart[] - }, - ) { - const match = yield* readToolCall(toolCallID) - if (!match || match.part.state.status !== "running") return - yield* session.updatePart({ - ...match.part, - state: { - status: "completed", - input: match.part.state.input, - output: output.output, - metadata: output.metadata, - title: output.title, - time: { start: match.part.state.time.start, end: Date.now() }, - attachments: output.attachments, - }, - }) - yield* settleToolCall(toolCallID) - }) - - const failToolCall = Effect.fn("SessionProcessor.failToolCall")(function* (toolCallID: string, error: unknown) { - const match = yield* readToolCall(toolCallID) - if (!match || match.part.state.status !== "running") return false - yield* session.updatePart({ - ...match.part, - state: { - status: "error", - input: match.part.state.input, - error: errorMessage(error), - time: { start: match.part.state.time.start, end: Date.now() }, - }, - }) - if (error instanceof Permission.RejectedError || error instanceof Question.RejectedError) { - ctx.blocked = ctx.shouldBreak + if (part.type === "reasoning") { + await Session.updatePart({ + ...part, + text: "", + time: { + start: part.time.start, + }, + }) } - yield* settleToolCall(toolCallID) - return true + } + Object.keys(toolcalls).forEach((id) => { + delete toolcalls[id] }) + input.assistantMessage.time.completed = undefined + await Session.updateMessage(input.assistantMessage) + } + + const result = { + get message() { + return input.assistantMessage + }, + partFromToolCall(toolCallID: string) { + return toolcalls[toolCallID] + }, + async process(streamInput: LLM.StreamInput) { + log.info("process") + needsCompaction = false + const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true + while (true) { + try { + receivedChunk = false + let currentText: MessageV2.TextPart | undefined + let reasoningMap: Record = {} + const stream = await LLM.stream(streamInput) + + for await (const value of stream.fullStream) { + receivedChunk = true + input.abort.throwIfAborted() + switch (value.type) { + case "start": + await SessionStatus.set(input.sessionID, { type: "busy" }) + break + + case "reasoning-start": + if (value.id in reasoningMap) { + continue + } + const reasoningPart = { + id: PartID.ascending(), + messageID: input.assistantMessage.id, + sessionID: input.assistantMessage.sessionID, + type: "reasoning" as const, + text: "", + time: { + start: Date.now(), + }, + metadata: value.providerMetadata, + } + reasoningMap[value.id] = reasoningPart + await Session.updatePart(reasoningPart) + break + + case "reasoning-delta": + if (value.id in reasoningMap) { + const part = reasoningMap[value.id] + part.text += value.text + if (value.providerMetadata) part.metadata = value.providerMetadata + await Session.updatePartDelta({ + sessionID: part.sessionID, + messageID: part.messageID, + partID: part.id, + field: "text", + delta: value.text, + }) + } + break + + case "reasoning-end": + if (value.id in reasoningMap) { + const part = reasoningMap[value.id] + part.text = part.text.trimEnd() + + part.time = { + ...part.time, + end: Date.now(), + } + if (value.providerMetadata) part.metadata = value.providerMetadata + await Session.updatePart(part) + delete reasoningMap[value.id] + } + break + + case "tool-input-start": + const part = await Session.updatePart({ + id: toolcalls[value.id]?.id ?? PartID.ascending(), + messageID: input.assistantMessage.id, + sessionID: input.assistantMessage.sessionID, + type: "tool", + tool: value.toolName, + callID: value.id, + state: { + status: "pending", + input: {}, + raw: "", + }, + }) + toolcalls[value.id] = part as MessageV2.ToolPart + break + + case "tool-input-delta": + break + + case "tool-input-end": + break + + case "tool-call": { + const match = toolcalls[value.toolCallId] + if (match) { + const part = await Session.updatePart({ + ...match, + tool: value.toolName, + state: { + status: "running", + input: value.input, + time: { + start: Date.now(), + }, + }, + metadata: value.providerMetadata, + }) + toolcalls[value.toolCallId] = part as MessageV2.ToolPart + + const parts = await MessageV2.parts(input.assistantMessage.id) + const lastThree = parts.slice(-DOOM_LOOP_THRESHOLD) + + if ( + lastThree.length === DOOM_LOOP_THRESHOLD && + lastThree.every( + (p) => + p.type === "tool" && + p.tool === value.toolName && + p.state.status !== "pending" && + JSON.stringify(p.state.input) === JSON.stringify(value.input), + ) + ) { + const agent = await Agent.get(input.assistantMessage.agent) + await Permission.ask({ + permission: "doom_loop", + patterns: [value.toolName], + sessionID: input.assistantMessage.sessionID, + metadata: { + tool: value.toolName, + input: value.input, + }, + always: [value.toolName], + ruleset: agent.permission, + }) + } + } + break + } + case "tool-result": { + const match = toolcalls[value.toolCallId] + if (match && match.state.status === "running") { + await Session.updatePart({ + ...match, + state: { + status: "completed", + input: value.input ?? match.state.input, + output: value.output.output, + metadata: value.output.metadata, + title: value.output.title, + time: { + start: match.state.time.start, + end: Date.now(), + }, + attachments: value.output.attachments, + }, + }) + + delete toolcalls[value.toolCallId] + } + break + } - const handleEvent = Effect.fnUntraced(function* (value: StreamEvent) { - switch (value.type) { - case "start": - yield* status.set(ctx.sessionID, { type: "busy" }) - return - - case "reasoning-start": - if (value.id in ctx.reasoningMap) return - ctx.reasoningMap[value.id] = { - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "reasoning", - text: "", - time: { start: Date.now() }, - metadata: value.providerMetadata, - } - yield* session.updatePart(ctx.reasoningMap[value.id]) - return - - case "reasoning-delta": - if (!(value.id in ctx.reasoningMap)) return - ctx.reasoningMap[value.id].text += value.text - if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata - yield* session.updatePartDelta({ - sessionID: ctx.reasoningMap[value.id].sessionID, - messageID: ctx.reasoningMap[value.id].messageID, - partID: ctx.reasoningMap[value.id].id, - field: "text", - delta: value.text, - }) - return - - case "reasoning-end": - if (!(value.id in ctx.reasoningMap)) return - // oxlint-disable-next-line no-self-assign -- reactivity trigger - ctx.reasoningMap[value.id].text = ctx.reasoningMap[value.id].text - ctx.reasoningMap[value.id].time = { ...ctx.reasoningMap[value.id].time, end: Date.now() } - if (value.providerMetadata) ctx.reasoningMap[value.id].metadata = value.providerMetadata - yield* session.updatePart(ctx.reasoningMap[value.id]) - delete ctx.reasoningMap[value.id] - return - - case "tool-input-start": - if (ctx.assistantMessage.summary) { - throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) - } - const part = yield* session.updatePart({ - id: ctx.toolcalls[value.id]?.partID ?? PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "tool", - tool: value.toolName, - callID: value.id, - state: { status: "pending", input: {}, raw: "" }, - metadata: value.providerExecuted ? { providerExecuted: true } : undefined, - } satisfies MessageV2.ToolPart) - ctx.toolcalls[value.id] = { - done: yield* Deferred.make(), - partID: part.id, - messageID: part.messageID, - sessionID: part.sessionID, - } - return - - case "tool-input-delta": - return - - case "tool-input-end": - return - - case "tool-call": { - if (ctx.assistantMessage.summary) { - throw new Error(`Tool call not allowed while generating summary: ${value.toolName}`) - } - yield* updateToolCall(value.toolCallId, (match) => ({ - ...match, - tool: value.toolName, - state: { - ...match.state, - status: "running", - input: value.input, - time: { start: Date.now() }, - }, - metadata: match.metadata?.providerExecuted - ? { ...value.providerMetadata, providerExecuted: true } - : value.providerMetadata, - })) - - const parts = MessageV2.parts(ctx.assistantMessage.id) - const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD) - - if ( - recentParts.length !== DOOM_LOOP_THRESHOLD || - !recentParts.every( - (part) => - part.type === "tool" && - part.tool === value.toolName && - part.state.status !== "pending" && - JSON.stringify(part.state.input) === JSON.stringify(value.input), - ) - ) { - return + case "tool-error": { + const match = toolcalls[value.toolCallId] + if (match && match.state.status === "running") { + await Session.updatePart({ + ...match, + state: { + status: "error", + input: value.input ?? match.state.input, + error: value.error instanceof Error ? value.error.message : String(value.error), + time: { + start: match.state.time.start, + end: Date.now(), + }, + }, + }) + + if ( + value.error instanceof Permission.RejectedError || + value.error instanceof Question.RejectedError + ) { + blocked = shouldBreak + } + delete toolcalls[value.toolCallId] + } + break + } + case "error": + throw value.error + + case "start-step": + snapshot = await Snapshot.track() + await Session.updatePart({ + id: PartID.ascending(), + messageID: input.assistantMessage.id, + sessionID: input.sessionID, + snapshot, + type: "step-start", + }) + break + + case "finish-step": + const usage = Session.getUsage({ + model: input.model, + usage: value.usage, + metadata: value.providerMetadata, + }) + input.assistantMessage.finish = value.finishReason + input.assistantMessage.cost += usage.cost + input.assistantMessage.tokens = usage.tokens + await Session.updatePart({ + id: PartID.ascending(), + reason: value.finishReason, + snapshot: await Snapshot.track(), + messageID: input.assistantMessage.id, + sessionID: input.assistantMessage.sessionID, + type: "step-finish", + tokens: usage.tokens, + cost: usage.cost, + }) + await Session.updateMessage(input.assistantMessage) + if (snapshot) { + const patch = await Snapshot.patch(snapshot) + if (patch.files.length) { + await Session.updatePart({ + id: PartID.ascending(), + messageID: input.assistantMessage.id, + sessionID: input.sessionID, + type: "patch", + hash: patch.hash, + files: patch.files, + }) + } + snapshot = undefined + } + SessionSummary.summarize({ + sessionID: input.sessionID, + messageID: input.assistantMessage.parentID, + }) + if ( + !input.assistantMessage.summary && + (await SessionCompaction.isOverflow({ tokens: usage.tokens, model: input.model })) + ) { + needsCompaction = true + } + break + + case "text-start": + currentText = { + id: PartID.ascending(), + messageID: input.assistantMessage.id, + sessionID: input.assistantMessage.sessionID, + type: "text", + text: "", + time: { + start: Date.now(), + }, + metadata: value.providerMetadata, + } + await Session.updatePart(currentText) + break + + case "text-delta": + if (currentText) { + currentText.text += value.text + if (value.providerMetadata) currentText.metadata = value.providerMetadata + await Session.updatePartDelta({ + sessionID: currentText.sessionID, + messageID: currentText.messageID, + partID: currentText.id, + field: "text", + delta: value.text, + }) + } + break + + case "text-end": + if (currentText) { + currentText.text = currentText.text.trimEnd() + const textOutput = await Plugin.trigger( + "experimental.text.complete", + { + sessionID: input.sessionID, + messageID: input.assistantMessage.id, + partID: currentText.id, + }, + { text: currentText.text }, + ) + currentText.text = textOutput.text + currentText.time = { + start: Date.now(), + end: Date.now(), + } + if (value.providerMetadata) currentText.metadata = value.providerMetadata + await Session.updatePart(currentText) + } + currentText = undefined + break + + case "finish": + break + + default: + log.info("unhandled", { + ...value, + }) + continue + } + if (needsCompaction) break } - - const agent = yield* agents.get(ctx.assistantMessage.agent) - yield* permission.ask({ - permission: "doom_loop", - patterns: [value.toolName], - sessionID: ctx.assistantMessage.sessionID, - metadata: { tool: value.toolName, input: value.input }, - always: [value.toolName], - ruleset: agent.permission, - }) - return - } - - case "tool-result": { - yield* completeToolCall(value.toolCallId, value.output) - return - } - - case "tool-error": { - yield* failToolCall(value.toolCallId, value.error) - return - } - - case "error": - throw value.error - - case "start-step": - if (!ctx.snapshot) ctx.snapshot = yield* snapshot.track() - yield* session.updatePart({ - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.sessionID, - snapshot: ctx.snapshot, - type: "step-start", - }) - return - - case "finish-step": { - const usage = Session.getUsage({ - model: ctx.model, - usage: value.usage, - metadata: value.providerMetadata, - }) - ctx.assistantMessage.finish = value.finishReason - ctx.assistantMessage.cost += usage.cost - ctx.assistantMessage.tokens = usage.tokens - yield* session.updatePart({ - id: PartID.ascending(), - reason: value.finishReason, - snapshot: yield* snapshot.track(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "step-finish", - tokens: usage.tokens, - cost: usage.cost, + } catch (e: any) { + log.error("process", { + error: e, + stack: JSON.stringify(e.stack), }) - yield* session.updateMessage(ctx.assistantMessage) - if (ctx.snapshot) { - const patch = yield* snapshot.patch(ctx.snapshot) - if (patch.files.length) { - yield* session.updatePart({ - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.sessionID, - type: "patch", - hash: patch.hash, - files: patch.files, - }) + const error = MessageV2.fromError(e, { providerID: input.model.providerID }) + if (MessageV2.ContextOverflowError.isInstance(error)) { + needsCompaction = true + Bus.publish(Session.Event.Error, { + sessionID: input.sessionID, + error, + }) + } else { + const retry = SessionRetry.retryable(error) + if (retry !== undefined) { + const network = + MessageV2.APIError.isInstance(error) && + error.data.isRetryable && + (error.data.message.includes("Network error") || + error.data.message.includes("SSE read timed out") || + error.data.message.includes("Connection reset by server")) + if (network) { + networkAttempt++ + if (networkAttempt <= MAX_NETWORK_RETRIES) { + const delay = Math.min(1000 * Math.pow(2, networkAttempt - 1), 5000) + await SessionStatus.set(input.sessionID, { + type: "reconnecting", + attempt: networkAttempt, + message: retry, + }) + if (receivedChunk) { + await cleanup() + } + await SessionRetry.sleep(delay, input.abort).catch(() => {}) + continue + } + } + if (!network) { + attempt++ + const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) + await SessionStatus.set(input.sessionID, { + type: "retry", + attempt, + message: retry, + next: Date.now() + delay, + }) + if (receivedChunk) { + await cleanup() + } + await SessionRetry.sleep(delay, input.abort).catch(() => {}) + continue + } } - ctx.snapshot = undefined - } - yield* summary - .summarize({ - sessionID: ctx.sessionID, - messageID: ctx.assistantMessage.parentID, + input.assistantMessage.error = error + Bus.publish(Session.Event.Error, { + sessionID: input.assistantMessage.sessionID, + error: input.assistantMessage.error, }) - .pipe(Effect.ignore, Effect.forkIn(scope)) - if ( - !ctx.assistantMessage.summary && - isOverflow({ cfg: yield* config.get(), tokens: usage.tokens, model: ctx.model }) - ) { - ctx.needsCompaction = true + await SessionStatus.set(input.sessionID, { type: "idle" }) } - return } - - case "text-start": - ctx.currentText = { - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.assistantMessage.sessionID, - type: "text", - text: "", - time: { start: Date.now() }, - metadata: value.providerMetadata, + if (snapshot) { + const patch = await Snapshot.patch(snapshot) + if (patch.files.length) { + await Session.updatePart({ + id: PartID.ascending(), + messageID: input.assistantMessage.id, + sessionID: input.sessionID, + type: "patch", + hash: patch.hash, + files: patch.files, + }) } - yield* session.updatePart(ctx.currentText) - return - - case "text-delta": - if (!ctx.currentText) return - ctx.currentText.text += value.text - if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata - yield* session.updatePartDelta({ - sessionID: ctx.currentText.sessionID, - messageID: ctx.currentText.messageID, - partID: ctx.currentText.id, - field: "text", - delta: value.text, - }) - return - - case "text-end": - if (!ctx.currentText) return - // oxlint-disable-next-line no-self-assign -- reactivity trigger - ctx.currentText.text = ctx.currentText.text - ctx.currentText.text = (yield* plugin.trigger( - "experimental.text.complete", - { - sessionID: ctx.sessionID, - messageID: ctx.assistantMessage.id, - partID: ctx.currentText.id, - }, - { text: ctx.currentText.text }, - )).text - { - const end = Date.now() - ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } + snapshot = undefined + } + const p = await MessageV2.parts(input.assistantMessage.id) + for (const part of p) { + if (part.type === "tool" && part.state.status !== "completed" && part.state.status !== "error") { + await Session.updatePart({ + ...part, + state: { + ...part.state, + status: "error", + error: "Tool execution aborted", + time: { + start: Date.now(), + end: Date.now(), + }, + }, + }) } - if (value.providerMetadata) ctx.currentText.metadata = value.providerMetadata - yield* session.updatePart(ctx.currentText) - ctx.currentText = undefined - return - - case "finish": - return - - default: - slog.info("unhandled", { event: value.type, value }) - return - } - }) - - const cleanup = Effect.fn("SessionProcessor.cleanup")(function* () { - if (ctx.snapshot) { - const patch = yield* snapshot.patch(ctx.snapshot) - if (patch.files.length) { - yield* session.updatePart({ - id: PartID.ascending(), - messageID: ctx.assistantMessage.id, - sessionID: ctx.sessionID, - type: "patch", - hash: patch.hash, - files: patch.files, - }) } - ctx.snapshot = undefined - } - - if (ctx.currentText) { - const end = Date.now() - ctx.currentText.time = { start: ctx.currentText.time?.start ?? end, end } - yield* session.updatePart(ctx.currentText) - ctx.currentText = undefined - } - - for (const part of Object.values(ctx.reasoningMap)) { - const end = Date.now() - yield* session.updatePart({ - ...part, - time: { start: part.time.start ?? end, end }, - }) - } - ctx.reasoningMap = {} - - yield* Effect.forEach( - Object.values(ctx.toolcalls), - (call) => Deferred.await(call.done).pipe(Effect.timeout("250 millis"), Effect.ignore), - { concurrency: "unbounded" }, - ) - - for (const toolCallID of Object.keys(ctx.toolcalls)) { - const match = yield* readToolCall(toolCallID) - if (!match) continue - const part = match.part - const end = Date.now() - const metadata = "metadata" in part.state && isRecord(part.state.metadata) ? part.state.metadata : {} - yield* session.updatePart({ - ...part, - state: { - ...part.state, - status: "error", - error: "Tool execution aborted", - metadata: { ...metadata, interrupted: true }, - time: { start: "time" in part.state ? part.state.time.start : end, end }, - }, - }) - } - ctx.toolcalls = {} - ctx.assistantMessage.time.completed = Date.now() - yield* session.updateMessage(ctx.assistantMessage) - }) - - const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) { - slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined }) - const error = parse(e) - if (MessageV2.ContextOverflowError.isInstance(error)) { - ctx.needsCompaction = true - yield* bus.publish(Session.Event.Error, { sessionID: ctx.sessionID, error }) - return - } - ctx.assistantMessage.error = error - yield* bus.publish(Session.Event.Error, { - sessionID: ctx.assistantMessage.sessionID, - error: ctx.assistantMessage.error, - }) - yield* status.set(ctx.sessionID, { type: "idle" }) - }) - - const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) { - slog.info("process") - ctx.needsCompaction = false - ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true - - return yield* Effect.gen(function* () { - yield* Effect.gen(function* () { - ctx.currentText = undefined - ctx.reasoningMap = {} - const stream = llm.stream(streamInput) - - yield* stream.pipe( - Stream.tap((event) => handleEvent(event)), - Stream.takeUntil(() => ctx.needsCompaction), - Stream.runDrain, - ) - }).pipe( - Effect.onInterrupt(() => - Effect.gen(function* () { - aborted = true - if (!ctx.assistantMessage.error) { - yield* halt(new DOMException("Aborted", "AbortError")) - } - }), - ), - Effect.catchCauseIf( - (cause) => !Cause.hasInterruptsOnly(cause), - (cause) => Effect.fail(Cause.squash(cause)), - ), - Effect.retry( - SessionRetry.policy({ - parse, - set: (info) => - status.set(ctx.sessionID, { - type: "retry", - attempt: info.attempt, - message: info.message, - next: info.next, - }), - }), - ), - Effect.catch(halt), - Effect.ensuring(cleanup()), - ) - - if (ctx.needsCompaction) return "compact" - if (ctx.blocked || ctx.assistantMessage.error) return "stop" + input.assistantMessage.time.completed = Date.now() + await Session.updateMessage(input.assistantMessage) + if (needsCompaction) return "compact" + if (blocked) return "stop" + if (input.assistantMessage.error) return "stop" return "continue" - }) - }) - - return { - get message() { - return ctx.assistantMessage - }, - updateToolCall, - completeToolCall, - process, - } satisfies Handle - }) - - return Service.of({ create }) - }), -) - -export const defaultLayer = Layer.suspend(() => - layer.pipe( - Layer.provide(Session.defaultLayer), - Layer.provide(Snapshot.defaultLayer), - Layer.provide(Agent.defaultLayer), - Layer.provide(LLM.defaultLayer), - Layer.provide(Permission.defaultLayer), - Layer.provide(Plugin.defaultLayer), - Layer.provide(SessionSummary.defaultLayer), - Layer.provide(SessionStatus.defaultLayer), - Layer.provide(Bus.layer), - Layer.provide(Config.defaultLayer), - ), -) - -export * as SessionProcessor from "./processor" + } + }, + } + return result + } +} diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5f3530bcefa7..600eb42f795e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -24,23 +24,23 @@ import MAX_STEPS from "../session/prompt/max-steps.txt" import { ToolRegistry } from "../tool" import { MCP } from "../mcp" import { LSP } from "../lsp" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { ulid } from "ulid" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import * as Stream from "effect/Stream" import { Command } from "../command" import { pathToFileURL, fileURLToPath } from "url" import { ConfigMarkdown } from "../config" import { SessionSummary } from "./summary" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { SessionProcessor } from "./processor" import { Tool } from "@/tool" import { Permission } from "@/permission" import { SessionStatus } from "./status" import { LLM } from "./llm" import { Shell } from "@/shell/shell" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "@/tool" import { decodeDataUrl } from "@/util/data-url" import { Process } from "@/util" @@ -787,6 +787,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const shellName = ( process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh) ).toLowerCase() + const cwd = ctx.directory const invocations: Record = { nu: { args: ["-c", input.command] }, fish: { args: ["-c", input.command] }, @@ -795,12 +796,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the "-l", "-c", ` - __oc_cwd=$PWD [[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true [[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true - cd "$__oc_cwd" + cd -- "$1" eval ${JSON.stringify(input.command)} `, + "opencode", + cwd, ], }, bash: { @@ -808,12 +810,13 @@ NOTE: At any point in time through this workflow you should feel free to ask the "-l", "-c", ` - __oc_cwd=$PWD shopt -s expand_aliases [[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true - cd "$__oc_cwd" + cd -- "$1" eval ${JSON.stringify(input.command)} `, + "opencode", + cwd, ], }, cmd: { args: ["/c", input.command] }, @@ -823,7 +826,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the } const args = (invocations[shellName] ?? invocations[""]).args - const cwd = ctx.directory const shellEnv = yield* plugin.trigger( "shell.env", { cwd, sessionID: input.sessionID, callID: part.callID }, diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 12fd4d345d06..e81e1973751f 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -1,4 +1,4 @@ -import type { NamedError } from "@opencode-ai/shared/util/error" +import type { NamedError } from "@opencode-ai/core/util/error" import { Cause, Clock, Duration, Effect, Schedule } from "effect" import { MessageV2 } from "./message-v2" import { iife } from "@/util/iife" diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index f4fe3bf8bd73..6c67b8517e9f 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -1,12 +1,12 @@ -import { Slug } from "@opencode-ai/shared/util/slug" +import { Slug } from "@opencode-ai/core/util/slug" import path from "path" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import { Decimal } from "decimal.js" import z from "zod" import { type ProviderMetadata, type LanguageModelUsage } from "ai" -import { Flag } from "../flag/flag" -import { InstallationVersion } from "../installation/version" +import { Flag } from "@opencode-ai/core/flag/flag" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { Database, NotFoundError, eq, and, gte, isNull, desc, like, inArray, lt } from "../storage" import { SyncEvent } from "../sync" @@ -25,7 +25,7 @@ import { SessionID, MessageID, PartID } from "./schema" import type { Provider } from "@/provider" import { Permission } from "@/permission" -import { Global } from "@/global" +import { Global } from "@opencode-ai/core/global" import { Effect, Layer, Option, Context, Schema, Types } from "effect" import { zod } from "@/util/effect-zod" import { withStatics } from "@/util/schema" diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index e5165a787945..b8e3768b1366 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -1,88 +1,104 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { InstanceState } from "@/effect" +import { InstanceState } from "@/effect/instance-state" +import { makeRunPromise } from "@/effect/run-service" import { SessionID } from "./schema" -import { zod } from "@/util/effect-zod" -import { withStatics } from "@/util/schema" -import { Effect, Layer, Context, Schema } from "effect" +import { Effect, Layer, ServiceMap } from "effect" import z from "zod" -export const Info = Schema.Union([ - Schema.Struct({ - type: Schema.Literal("idle"), - }), - Schema.Struct({ - type: Schema.Literal("retry"), - attempt: Schema.Number, - message: Schema.String, - next: Schema.Number, - }), - Schema.Struct({ - type: Schema.Literal("busy"), - }), -]) - .annotate({ identifier: "SessionStatus" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) -export type Info = Schema.Schema.Type +export namespace SessionStatus { + export const Info = z + .union([ + z.object({ + type: z.literal("idle"), + }), + z.object({ + type: z.literal("retry"), + attempt: z.number(), + message: z.string(), + next: z.number(), + }), + z.object({ + type: z.literal("reconnecting"), + attempt: z.number(), + message: z.string(), + }), + z.object({ + type: z.literal("busy"), + }), + ]) + .meta({ + ref: "SessionStatus", + }) + export type Info = z.infer -export const Event = { - Status: BusEvent.define( - "session.status", - Schema.Struct({ - sessionID: SessionID, - status: Info, - }), - ), - // deprecated - Idle: BusEvent.define( - "session.idle", - Schema.Struct({ - sessionID: SessionID, - }), - ), -} + export const Event = { + Status: BusEvent.define( + "session.status", + z.object({ + sessionID: SessionID.zod, + status: Info, + }), + ), + // deprecated + Idle: BusEvent.define( + "session.idle", + z.object({ + sessionID: SessionID.zod, + }), + ), + } -export interface Interface { - readonly get: (sessionID: SessionID) => Effect.Effect - readonly list: () => Effect.Effect> - readonly set: (sessionID: SessionID, status: Info) => Effect.Effect -} + export interface Interface { + readonly get: (sessionID: SessionID) => Effect.Effect + readonly list: () => Effect.Effect> + readonly set: (sessionID: SessionID, status: Info) => Effect.Effect + } -export class Service extends Context.Service()("@opencode/SessionStatus") {} + export class Service extends ServiceMap.Service()("@opencode/SessionStatus") {} -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service + export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const state = yield* InstanceState.make( + Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map())), + ) - const state = yield* InstanceState.make( - Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map())), - ) + const get = Effect.fn("SessionStatus.get")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + return data.get(sessionID) ?? { type: "idle" as const } + }) - const get = Effect.fn("SessionStatus.get")(function* (sessionID: SessionID) { - const data = yield* InstanceState.get(state) - return data.get(sessionID) ?? { type: "idle" as const } - }) + const list = Effect.fn("SessionStatus.list")(function* () { + return new Map(yield* InstanceState.get(state)) + }) - const list = Effect.fn("SessionStatus.list")(function* () { - return new Map(yield* InstanceState.get(state)) - }) + const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) { + const data = yield* InstanceState.get(state) + yield* Effect.promise(() => Bus.publish(Event.Status, { sessionID, status })) + if (status.type === "idle") { + yield* Effect.promise(() => Bus.publish(Event.Idle, { sessionID })) + data.delete(sessionID) + return + } + data.set(sessionID, status) + }) - const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) { - const data = yield* InstanceState.get(state) - yield* bus.publish(Event.Status, { sessionID, status }) - if (status.type === "idle") { - yield* bus.publish(Event.Idle, { sessionID }) - data.delete(sessionID) - return - } - data.set(sessionID, status) - }) + return Service.of({ get, list, set }) + }), + ) - return Service.of({ get, list, set }) - }), -) + const runPromise = makeRunPromise(Service, layer) -export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) + export async function get(sessionID: SessionID) { + return runPromise((svc) => svc.get(sessionID)) + } -export * as SessionStatus from "./status" + export async function list() { + return runPromise((svc) => svc.list()) + } + + export async function set(sessionID: SessionID, status: Info) { + return runPromise((svc) => svc.set(sessionID, status)) + } +} diff --git a/packages/opencode/src/share/session.ts b/packages/opencode/src/share/session.ts index 63b76707858d..c5394716b1d0 100644 --- a/packages/opencode/src/share/session.ts +++ b/packages/opencode/src/share/session.ts @@ -3,7 +3,7 @@ import { SessionID } from "@/session/schema" import { SyncEvent } from "@/sync" import { Effect, Layer, Scope, Context } from "effect" import { Config } from "../config" -import { Flag } from "../flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import * as ShareNext from "./share-next" export interface Interface { diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 60643c10b023..1c89961945eb 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -1,4 +1,4 @@ -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { lazy } from "@/util/lazy" import { Filesystem } from "@/util" import { which } from "@/util/which" diff --git a/packages/opencode/src/skill/discovery.ts b/packages/opencode/src/skill/discovery.ts index debd68dd3d9d..9ce56d4ce91b 100644 --- a/packages/opencode/src/skill/discovery.ts +++ b/packages/opencode/src/skill/discovery.ts @@ -2,8 +2,8 @@ import { NodePath } from "@effect/platform-node" import { Effect, Layer, Path, Schema, Context } from "effect" import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { withTransientReadRetry } from "@/util/effect-http-client" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Global } from "../global" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" const skillConcurrency = 4 diff --git a/packages/opencode/src/skill/index.ts b/packages/opencode/src/skill/index.ts index dd5cc4e5d5b3..ebb4339719c4 100644 --- a/packages/opencode/src/skill/index.ts +++ b/packages/opencode/src/skill/index.ts @@ -2,18 +2,20 @@ import os from "os" import path from "path" import { pathToFileURL } from "url" import z from "zod" -import { Effect, Layer, Context } from "effect" -import { NamedError } from "@opencode-ai/shared/util/error" +import { Effect, Layer, Context, Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" +import { NamedError } from "@opencode-ai/core/util/error" import type { Agent } from "@/agent/agent" import { Bus } from "@/bus" import { InstanceState } from "@/effect" -import { Flag } from "@/flag/flag" -import { Global } from "@/global" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Global } from "@opencode-ai/core/global" import { Permission } from "@/permission" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Config } from "../config" import { ConfigMarkdown } from "../config" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import { Log } from "../util" import { Discovery } from "./discovery" @@ -23,13 +25,13 @@ const EXTERNAL_SKILL_PATTERN = "skills/**/SKILL.md" const OPENCODE_SKILL_PATTERN = "{skill,skills}/**/SKILL.md" const SKILL_PATTERN = "**/SKILL.md" -export const Info = z.object({ - name: z.string(), - description: z.string(), - location: z.string(), - content: z.string(), -}) -export type Info = z.infer +export const Info = Schema.Struct({ + name: Schema.String, + description: Schema.String, + location: Schema.String, + content: Schema.String, +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Info = Schema.Schema.Type export const InvalidError = NamedError.create( "SkillInvalidError", @@ -91,7 +93,7 @@ const add = Effect.fnUntraced(function* (state: State, match: string, bus: Bus.I if (!md) return - const parsed = Info.pick({ name: true, description: true }).safeParse(md.data) + const parsed = z.object({ name: z.string(), description: z.string() }).safeParse(md.data) if (!parsed.success) return if (state.skills[parsed.data.name]) { diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index ddc4cb29eac1..88ea274f8bd7 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -3,12 +3,12 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { formatPatch, structuredPatch } from "diff" import path from "path" import z from "zod" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Hash } from "@opencode-ai/shared/util/hash" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Hash } from "@opencode-ai/core/util/hash" import { Config } from "../config" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { withStatics } from "@/util/schema" import { zod } from "@/util/effect-zod" diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 2c0076452e1c..e442b2a76d1e 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -4,14 +4,14 @@ import { type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { LocalContext } from "../util" import { lazy } from "../util/lazy" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" -import { Flag } from "../flag/flag" -import { InstallationChannel } from "../installation/version" +import { Flag } from "@opencode-ai/core/flag/flag" +import { InstallationChannel } from "@opencode-ai/core/installation/version" import { InstanceState } from "@/effect" import { iife } from "@/util/iife" import { init } from "#db" diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 05588db0f670..787b50117c63 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -1,6 +1,6 @@ import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { ProjectTable } from "../project/project.sql" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql" @@ -8,7 +8,7 @@ import { SessionShareTable } from "../share/share.sql" import path from "path" import { existsSync } from "fs" import { Filesystem } from "../util" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" const log = Log.create({ service: "json-migration" }) diff --git a/packages/opencode/src/storage/storage.ts b/packages/opencode/src/storage/storage.ts index b1685e689b6c..71cc37e013d9 100644 --- a/packages/opencode/src/storage/storage.ts +++ b/packages/opencode/src/storage/storage.ts @@ -1,9 +1,9 @@ import { Log } from "../util" import path from "path" -import { Global } from "../global" -import { NamedError } from "@opencode-ai/shared/util/error" +import { Global } from "@opencode-ai/core/global" +import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Effect, Exit, Layer, Option, RcMap, Schema, Context, TxReentrantLock } from "effect" import { Git } from "@/git" diff --git a/packages/opencode/src/sync/index.ts b/packages/opencode/src/sync/index.ts index 35a5abd0b103..da33b7aa96da 100644 --- a/packages/opencode/src/sync/index.ts +++ b/packages/opencode/src/sync/index.ts @@ -7,7 +7,7 @@ import { Instance } from "@/project/instance" import { EventSequenceTable, EventTable } from "./event.sql" import { WorkspaceContext } from "@/control-plane/workspace-context" import { EventID } from "./schema" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Schema as EffectSchema } from "effect" import { zodObject } from "@/util/effect-zod" import type { DeepMutable } from "@/util/schema" diff --git a/packages/opencode/src/temporary.ts b/packages/opencode/src/temporary.ts index bbb97e0f0fc2..7747eb1e9f59 100644 --- a/packages/opencode/src/temporary.ts +++ b/packages/opencode/src/temporary.ts @@ -1,6 +1,6 @@ import yargs from "yargs" import { TuiThreadCommand } from "./cli/cmd/tui/thread" -import { InstallationVersion } from "./installation/version" +import { InstallationVersion } from "@opencode-ai/core/installation/version" import { hideBin } from "yargs/helpers" import { Log } from "./node" diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 72f24a3f60a6..9a009189de51 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -9,7 +9,7 @@ import { createTwoFilesPatch, diffLines } from "diff" import { assertExternalDirectoryEffect } from "./external-directory" import { trimDiff } from "./edit" import { LSP } from "../lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import DESCRIPTION from "./apply_patch.txt" import { File } from "../file" import { Format } from "../format" diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 0a7e1a6dc2c2..a27d7c5ecb70 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -9,9 +9,9 @@ import { Instance } from "../project/instance" import { lazy } from "@/util/lazy" import { Language, type Node } from "web-tree-sitter" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { fileURLToPath } from "url" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Shell } from "@/shell/shell" import { BashArity } from "@/permission/arity" @@ -20,6 +20,7 @@ import { Plugin } from "@/plugin" import { Effect, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" +import { InstanceState } from "@/effect" const MAX_METADATA_LENGTH = 30_000 const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000 @@ -575,9 +576,10 @@ export const BashTool = Tool.define( log.info("bash tool using shell", { shell }) const limits = yield* trunc.limits() + const instance = yield* InstanceState.context return { - description: DESCRIPTION.replaceAll("${directory}", Instance.directory) + description: DESCRIPTION.replaceAll("${directory}", instance.directory) .replaceAll("${os}", process.platform) .replaceAll("${shell}", name) .replaceAll("${chaining}", chain) diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 668cea307ce4..c2fe873791fa 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -58,7 +58,7 @@ Git Safety Protocol: - NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it - NEVER run force push to main/master, warn the user if they request it - Avoid git commit --amend. ONLY use --amend when ALL conditions are met: - (1) User explicitly requested amend, OR commit SUCCEEDED but pre-commit hook auto-modified files that need including + (1) User explicitly requested amend, OR the commit succeeded and pre-commit hooks auto-modified files that need including — verify by checking `git log` that HEAD is the new commit before amending (2) HEAD commit was created by you in this conversation (verify: git log -1 --format='%an %ae') (3) Commit has NOT been pushed to remote (verify: git status shows "Your branch is ahead") - CRITICAL: If commit FAILED or was REJECTED by hook, NEVER amend - fix the issue and create a NEW commit diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index cfff5a0a30b5..04a84a388a95 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -16,7 +16,7 @@ import { Format } from "../format" import { Instance } from "../project/instance" import { Snapshot } from "@/snapshot" import { assertExternalDirectoryEffect } from "./external-directory" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import * as Bom from "@/util/bom" function normalizeLineEndings(text: string): string { diff --git a/packages/opencode/src/tool/external-directory.ts b/packages/opencode/src/tool/external-directory.ts index 88b73da50968..b8def1d75e7a 100644 --- a/packages/opencode/src/tool/external-directory.ts +++ b/packages/opencode/src/tool/external-directory.ts @@ -4,7 +4,7 @@ import { EffectLogger } from "@/effect" import { InstanceState } from "@/effect" import type * as Tool from "./tool" import { Instance } from "../project/instance" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" type Kind = "file" | "directory" diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index aeecfecb720b..984c13d41314 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -2,7 +2,7 @@ import path from "path" import { Effect, Option, Schema } from "effect" import * as Stream from "effect/Stream" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" import DESCRIPTION from "./glob.txt" diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 416005431194..844de6753f42 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -2,7 +2,7 @@ import path from "path" import { Schema } from "effect" import { Effect, Option } from "effect" import { InstanceState } from "@/effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectoryEffect } from "./external-directory" import DESCRIPTION from "./grep.txt" diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index 29c6a8d843c9..bb3b503441b2 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -6,7 +6,7 @@ import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" import { assertExternalDirectoryEffect } from "./external-directory" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" const operations = [ "goToDefinition", @@ -36,7 +36,6 @@ export const LspTool = Tool.define( Effect.gen(function* () { const lsp = yield* LSP.Service const fs = yield* AppFileSystem.Service - return { description: DESCRIPTION, parameters: Parameters, @@ -47,12 +46,29 @@ export const LspTool = Tool.define( Effect.gen(function* () { const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) yield* assertExternalDirectoryEffect(ctx, file) - yield* ctx.ask({ permission: "lsp", patterns: ["*"], always: ["*"], metadata: {} }) + const meta = + args.operation === "workspaceSymbol" + ? { operation: args.operation } + : args.operation === "documentSymbol" + ? { operation: args.operation, filePath: file } + : { operation: args.operation, filePath: file, line: args.line, character: args.character } + yield* ctx.ask({ + permission: "lsp", + patterns: ["*"], + always: ["*"], + metadata: meta, + }) const uri = pathToFileURL(file).href const position = { file, line: args.line - 1, character: args.character - 1 } const relPath = path.relative(Instance.worktree, file) - const title = `${args.operation} ${relPath}:${args.line}:${args.character}` + const detail = + args.operation === "workspaceSymbol" + ? "" + : args.operation === "documentSymbol" + ? relPath + : `${relPath}:${args.line}:${args.character}` + const title = detail ? `${args.operation} ${detail}` : args.operation const exists = yield* fs.existsSafe(file) if (!exists) throw new Error(`File not found: ${file}`) @@ -90,7 +106,7 @@ export const LspTool = Tool.define( metadata: { result }, output: result.length === 0 ? `No results found for ${args.operation}` : JSON.stringify(result, null, 2), } - }), + }).pipe(Effect.orDie), } }), ) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index d0995626c025..e89f03109097 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -3,7 +3,7 @@ import { createReadStream } from "fs" import * as path from "path" import { createInterface } from "readline" import * as Tool from "./tool" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../lsp" import DESCRIPTION from "./read.txt" import { Instance } from "../project/instance" diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 539ad632020c..4e3d3d714b96 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -23,18 +23,18 @@ import { Provider } from "../provider" import { ProviderID, type ModelID } from "../provider/schema" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" -import { Flag } from "@/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { Log } from "@/util" import { LspTool } from "./lsp" import * as Truncate from "./truncate" import { ApplyPatchTool } from "./apply_patch" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import path from "path" import { pathToFileURL } from "url" import { Effect, Layer, Context } from "effect" import { FetchHttpClient, HttpClient } from "effect/unstable/http" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../file/ripgrep" import { Format } from "../format" import { InstanceState } from "@/effect" @@ -42,7 +42,7 @@ import { Question } from "../question" import { Todo } from "../session/todo" import { LSP } from "../lsp" import { Instruction } from "../session/instruction" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Bus } from "../bus" import { Agent } from "../agent/agent" import { Skill } from "../skill" diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index e0d846858ee6..191d96795479 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -2,7 +2,7 @@ import { NodePath } from "@effect/platform-node" import { Cause, Duration, Effect, Layer, Option, Schedule, Context } from "effect" import path from "path" import type { Agent } from "../agent/agent" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { evaluate } from "@/permission/evaluate" import { Config } from "../config" import { Identifier } from "../id/id" diff --git a/packages/opencode/src/tool/truncation-dir.ts b/packages/opencode/src/tool/truncation-dir.ts index d6d5d013d783..9ed82e1f3c13 100644 --- a/packages/opencode/src/tool/truncation-dir.ts +++ b/packages/opencode/src/tool/truncation-dir.ts @@ -1,4 +1,4 @@ import path from "path" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" export const TRUNCATION_DIR = path.join(Global.Path.data, "tool-output") diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index b52f4a164c54..d977325f15e8 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -9,7 +9,7 @@ import { Bus } from "../bus" import { File } from "../file" import { FileWatcher } from "../file/watcher" import { Format } from "../format" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Instance } from "../project/instance" import { trimDiff } from "./edit" import { assertExternalDirectoryEffect } from "./external-directory" diff --git a/packages/opencode/src/util/bom.ts b/packages/opencode/src/util/bom.ts index 484228f3d415..79de915781ab 100644 --- a/packages/opencode/src/util/bom.ts +++ b/packages/opencode/src/util/bom.ts @@ -1,5 +1,5 @@ import { Effect } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" const BOM_CODE = 0xfeff const BOM = String.fromCharCode(BOM_CODE) diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts index 6c4d455224e9..6225c80d2136 100644 --- a/packages/opencode/src/util/filesystem.ts +++ b/packages/opencode/src/util/filesystem.ts @@ -4,7 +4,7 @@ import { realpathSync } from "fs" import { dirname, join, relative, resolve as pathResolve, win32 } from "path" import { Readable } from "stream" import { pipeline } from "stream/promises" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" // Fast sync version for metadata checks export async function exists(p: string): Promise { diff --git a/packages/opencode/src/util/index.ts b/packages/opencode/src/util/index.ts index f051ad964944..c67a3d140b8f 100644 --- a/packages/opencode/src/util/index.ts +++ b/packages/opencode/src/util/index.ts @@ -5,7 +5,7 @@ export * as Keybind from "./keybind" export * as LocalContext from "./local-context" export * as Locale from "./locale" export * as Lock from "./lock" -export * as Log from "./log" +export * as Log from "@opencode-ai/core/util/log" export * as Process from "./process" export * as Rpc from "./rpc" export * as Token from "./token" diff --git a/packages/opencode/src/util/which.ts b/packages/opencode/src/util/which.ts index 2e40739148a1..b9bea421c6a7 100644 --- a/packages/opencode/src/util/which.ts +++ b/packages/opencode/src/util/which.ts @@ -1,6 +1,6 @@ import whichPkg from "which" import path from "path" -import { Global } from "../global" +import { Global } from "@opencode-ai/core/global" export function which(cmd: string, env?: NodeJS.ProcessEnv) { const base = env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path ?? "" diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index e122fe453b9e..8d635e80fc37 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -1,6 +1,6 @@ import z from "zod" -import { NamedError } from "@opencode-ai/shared/util/error" -import { Global } from "../global" +import { NamedError } from "@opencode-ai/core/util/error" +import { Global } from "@opencode-ai/core/global" import { Instance } from "../project/instance" import { InstanceBootstrap } from "../project/bootstrap" import { Project } from "../project" @@ -8,7 +8,7 @@ import { Database, eq } from "../storage" import { ProjectTable } from "../project/project.sql" import type { ProjectID } from "../project/schema" import { Log } from "../util" -import { Slug } from "@opencode-ai/shared/util/slug" +import { Slug } from "@opencode-ai/core/util/slug" import { errorMessage } from "../util/error" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" @@ -16,10 +16,12 @@ import { Git } from "@/git" import { Effect, Layer, Path, Schema, Scope, Context, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { BootstrapRuntime } from "@/effect/bootstrap-runtime" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { InstanceState } from "@/effect" +import { zod as effectZod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" const log = Log.create({ service: "worktree" }) @@ -39,48 +41,38 @@ export const Event = { ), } -export const Info = z - .object({ - name: z.string(), - branch: z.string(), - directory: z.string(), - }) - .meta({ - ref: "Worktree", - }) - -export type Info = z.infer - -export const CreateInput = z - .object({ - name: z.string().optional(), - startCommand: z.string().optional().describe("Additional startup script to run after the project's start command"), - }) - .meta({ - ref: "WorktreeCreateInput", - }) - -export type CreateInput = z.infer - -export const RemoveInput = z - .object({ - directory: z.string(), - }) - .meta({ - ref: "WorktreeRemoveInput", - }) - -export type RemoveInput = z.infer - -export const ResetInput = z - .object({ - directory: z.string(), - }) - .meta({ - ref: "WorktreeResetInput", - }) - -export type ResetInput = z.infer +export const Info = Schema.Struct({ + name: Schema.String, + branch: Schema.String, + directory: Schema.String, +}) + .annotate({ identifier: "Worktree" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type Info = Schema.Schema.Type + +export const CreateInput = Schema.Struct({ + name: Schema.optional(Schema.String), + startCommand: Schema.optional( + Schema.String.annotate({ description: "Additional startup script to run after the project's start command" }), + ), +}) + .annotate({ identifier: "WorktreeCreateInput" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type CreateInput = Schema.Schema.Type + +export const RemoveInput = Schema.Struct({ + directory: Schema.String, +}) + .annotate({ identifier: "WorktreeRemoveInput" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type RemoveInput = Schema.Schema.Type + +export const ResetInput = Schema.Struct({ + directory: Schema.String, +}) + .annotate({ identifier: "WorktreeResetInput" }) + .pipe(withStatics((s) => ({ zod: effectZod(s) }))) +export type ResetInput = Schema.Schema.Type export const NotGitError = NamedError.create( "WorktreeNotGitError", @@ -210,7 +202,7 @@ export const layer: Layer.Layer< const branchCheck = yield* git(["show-ref", "--verify", "--quiet", ref], { cwd: ctx.worktree }) if (branchCheck.code === 0) continue - return Info.parse({ name, branch, directory }) + return { name, branch, directory } } throw new NameGenerationFailedError({ message: "Failed to generate a unique worktree name" }) }) diff --git a/packages/opencode/test/auth/auth.test.ts b/packages/opencode/test/auth/auth.test.ts index 864649d7aefa..55e950aab666 100644 --- a/packages/opencode/test/auth/auth.test.ts +++ b/packages/opencode/test/auth/auth.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Auth } from "../../src/auth" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/bus/bus-effect.test.ts b/packages/opencode/test/bus/bus-effect.test.ts index 3d602ae6fd79..0daf8fe6a67d 100644 --- a/packages/opencode/test/bus/bus-effect.test.ts +++ b/packages/opencode/test/bus/bus-effect.test.ts @@ -3,7 +3,7 @@ import { Deferred, Effect, Layer, Schema, Stream } from "effect" import { Bus } from "../../src/bus" import { BusEvent } from "../../src/bus/bus-event" import { Instance } from "../../src/project/instance" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideInstance, provideTmpdirInstance, tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/cli/tui/editor-context.test.ts b/packages/opencode/test/cli/tui/editor-context.test.ts new file mode 100644 index 000000000000..c605029ca3b8 --- /dev/null +++ b/packages/opencode/test/cli/tui/editor-context.test.ts @@ -0,0 +1,9 @@ +import { expect, test } from "bun:test" +import { offsetToPosition } from "../../../src/cli/cmd/tui/context/editor-zed" + +test("offsetToPosition converts Zed offsets to 1-based editor positions", () => { + expect(offsetToPosition("one\ntwo\nthree", 0)).toEqual({ line: 1, character: 1 }) + expect(offsetToPosition("one\ntwo\nthree", 4)).toEqual({ line: 2, character: 1 }) + expect(offsetToPosition("one\ntwo\nthree", 6)).toEqual({ line: 2, character: 3 }) + expect(offsetToPosition("one\ntwo\nthree", 100)).toEqual({ line: 3, character: 6 }) +}) diff --git a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts index 74236afae803..66858e2d0d98 100644 --- a/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts @@ -5,7 +5,7 @@ import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" -import { Npm } from "../../../src/npm" +import { Npm } from "@opencode-ai/core/npm" const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime") diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index f5b04ff434f5..3dd8b6015e84 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -4,7 +4,7 @@ import path from "path" import { pathToFileURL } from "url" import { tmpdir } from "../../fixture/fixture" import { createTuiPluginApi } from "../../fixture/tui-plugin" -import { Global } from "../../../src/global" +import { Global } from "@opencode-ai/core/global" import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui" import { Filesystem } from "../../../src/util/" diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 73dd46e31994..324914e6d69a 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -3,17 +3,17 @@ import { Effect, Layer, Option } from "effect" import { NodeFileSystem, NodePath } from "@effect/platform-node" import { Config, ConfigManaged } from "../../src/config" import { ConfigParse } from "../../src/config/parse" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { Instance } from "../../src/project/instance" import { Auth } from "../../src/auth" import { Account } from "../../src/account/account" import { AccessToken, AccountID, OrgID } from "../../src/account/schema" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Env } from "../../src/env" import { provideTmpdirInstance } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { testEffect } from "../lib/effect" /** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */ @@ -23,11 +23,11 @@ const infra = CrossSpawnSpawner.defaultLayer.pipe( import path from "path" import fs from "fs/promises" import { pathToFileURL } from "url" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { ProjectID } from "../../src/project/schema" import { Filesystem } from "../../src/util" import { ConfigPlugin } from "@/config/plugin" -import { Npm } from "@/npm" +import { Npm } from "@opencode-ai/core/npm" const emptyAccount = Layer.mock(Account.Service)({ active: () => Effect.succeed(Option.none()), @@ -645,6 +645,33 @@ Test agent prompt`, }) }) +test("agent markdown permission config preserves user key order", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + const agentDir = path.join(dir, ".opencode", "agent") + await fs.mkdir(agentDir, { recursive: true }) + + await Filesystem.write( + path.join(agentDir, "ordered.md"), + `--- +permission: + bash: allow + "*": deny + edit: ask +--- +Ordered permissions`, + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await load() + expect(Object.keys(config.agent?.ordered?.permission ?? {})).toEqual(["bash", "*", "edit"]) + }, + }) +}) + test("loads agents from .opencode/agents (plural)", async () => { await using tmp = await tmpdir({ init: async (dir) => { @@ -895,7 +922,7 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => { }) // Note: deduplication and serialization of npm installs is now handled by the -// shared Npm.Service (via EffectFlock). Those behaviors are tested in the shared +// core Npm.Service (via EffectFlock). Those behaviors are tested in the core // package's npm tests, not here. test("resolves scoped npm plugins in config", async () => { @@ -1495,16 +1522,9 @@ test("merges legacy tools with existing permission config", async () => { }) }) -test("permission config canonicalises known keys first, preserves rest-key insertion order", async () => { - // ConfigPermission.Info is a StructWithRest schema — the decoder reorders - // keys into declaration-order for known permission names (edit, read, - // todowrite, external_directory are declared in `config/permission.ts`), - // followed by rest keys in the user's insertion order. - // - // Rule precedence is NOT affected by this reordering: `Permission.fromConfig` - // sorts wildcards before specifics before iterating. See the - // "fromConfig - specific key beats wildcard regardless of JSON key order" - // test in test/permission/next.test.ts for the behavioural guarantee. +test("permission config preserves user key order", async () => { + // Permission precedence follows the order users write in config, so parsing + // must not canonicalise known keys ahead of wildcard or custom keys. await using tmp = await tmpdir({ init: async (dir) => { await Filesystem.write( @@ -1532,15 +1552,12 @@ test("permission config canonicalises known keys first, preserves rest-key inser fn: async () => { const config = await load() expect(Object.keys(config.permission!)).toEqual([ - // known fields that the user provided, in declaration order from - // config/permission.ts (read, edit, ..., external_directory, todowrite) - "read", + "*", "edit", + "write", "external_directory", + "read", "todowrite", - // rest keys (not in the known list), in user's insertion order - "*", - "write", "thoughts_*", "reasoning_model_*", "tools_*", @@ -1550,6 +1567,29 @@ test("permission config canonicalises known keys first, preserves rest-key inser }) }) +test("Effect config parser preserves permission order while rejecting unknown top-level keys", () => { + const config = ConfigParse.effectSchema( + Config.Info, + { + permission: { + bash: "allow", + "*": "deny", + edit: "ask", + }, + }, + "test", + ) + + expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"]) + try { + ConfigParse.effectSchema(Config.Info, { invalid_field: true }, "test") + throw new Error("expected config parse to fail") + } catch (err) { + const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } } + expect(error.data?.issues?.[0]).toMatchObject({ code: "unrecognized_keys", keys: ["invalid_field"], path: [] }) + } +}) + // MCP config merging tests test("project config can override MCP server enabled status", async () => { @@ -2232,8 +2272,8 @@ describe("OPENCODE_CONFIG_CONTENT token substitution", () => { // parseManagedPlist unit tests — pure function, no OS interaction test("parseManagedPlist strips MDM metadata keys", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2260,8 +2300,8 @@ test("parseManagedPlist strips MDM metadata keys", async () => { }) test("parseManagedPlist parses server settings", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2280,8 +2320,8 @@ test("parseManagedPlist parses server settings", async () => { }) test("parseManagedPlist parses permission rules", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2310,8 +2350,8 @@ test("parseManagedPlist parses permission rules", async () => { }) test("parseManagedPlist parses enabled_providers", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist( JSON.stringify({ @@ -2327,8 +2367,8 @@ test("parseManagedPlist parses enabled_providers", async () => { }) test("parseManagedPlist handles empty config", async () => { - const config = ConfigParse.schema( - Config.Info.zod, + const config = ConfigParse.effectSchema( + Config.Info, ConfigParse.jsonc( await ConfigManaged.parseManagedPlist(JSON.stringify({ $schema: "https://opencode.ai/config.json" })), "test:mobileconfig", diff --git a/packages/opencode/test/config/tui.test.ts b/packages/opencode/test/config/tui.test.ts index c7b6d4a50494..0dbe49cef7f4 100644 --- a/packages/opencode/test/config/tui.test.ts +++ b/packages/opencode/test/config/tui.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { TuiConfig } from "../../src/cli/cmd/tui/config/tui" import { Config } from "../../src/config" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "../../src/util" import { AppRuntime } from "../../src/effect/app-runtime" import { Effect, Layer } from "effect" diff --git a/packages/opencode/test/filesystem/filesystem.test.ts b/packages/opencode/test/filesystem/filesystem.test.ts index 0bb4ba583933..2d9271e873eb 100644 --- a/packages/opencode/test/filesystem/filesystem.test.ts +++ b/packages/opencode/test/filesystem/filesystem.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { Effect, Layer } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/opencode/test/fixture/flock-worker.ts b/packages/opencode/test/fixture/flock-worker.ts index 9954d290cce9..0b9c314c0877 100644 --- a/packages/opencode/test/fixture/flock-worker.ts +++ b/packages/opencode/test/fixture/flock-worker.ts @@ -1,5 +1,5 @@ import fs from "fs/promises" -import { Flock } from "@opencode-ai/shared/util/flock" +import { Flock } from "@opencode-ai/core/util/flock" type Msg = { key: string diff --git a/packages/opencode/test/format/format.test.ts b/packages/opencode/test/format/format.test.ts index 2f6f235aa165..674d2767cd07 100644 --- a/packages/opencode/test/format/format.test.ts +++ b/packages/opencode/test/format/format.test.ts @@ -3,7 +3,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Format } from "../../src/format" import * as Formatter from "../../src/format/formatter" diff --git a/packages/opencode/test/installation/installation.test.ts b/packages/opencode/test/installation/installation.test.ts index 0d3e92989d3e..469ebb714de3 100644 --- a/packages/opencode/test/installation/installation.test.ts +++ b/packages/opencode/test/installation/installation.test.ts @@ -3,7 +3,7 @@ import { Effect, Layer, Stream } from "effect" import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { Installation } from "../../src/installation" -import { InstallationChannel } from "../../src/installation/version" +import { InstallationChannel } from "@opencode-ai/core/installation/version" const encoder = new TextEncoder() diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index d138f56e3b25..092bfef511aa 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" import { LSPServer } from "../../src/lsp" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/lsp/lifecycle.test.ts b/packages/opencode/test/lsp/lifecycle.test.ts index 13f21c93cc7e..e39290973708 100644 --- a/packages/opencode/test/lsp/lifecycle.test.ts +++ b/packages/opencode/test/lsp/lifecycle.test.ts @@ -3,7 +3,7 @@ import path from "path" import { Effect, Layer } from "effect" import { LSP } from "../../src/lsp" import { LSPServer } from "../../src/lsp" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/npm.test.ts b/packages/opencode/test/npm.test.ts index b27d668c8c72..d5b93a83c0a3 100644 --- a/packages/opencode/test/npm.test.ts +++ b/packages/opencode/test/npm.test.ts @@ -3,11 +3,11 @@ import path from "path" import { describe, expect, test } from "bun:test" import { Effect, Layer, Stream } from "effect" import { NodeFileSystem } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import { Global } from "@opencode-ai/shared/global" -import { EffectFlock } from "@opencode-ai/shared/util/effect-flock" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { Global } from "@opencode-ai/core/global" +import { EffectFlock } from "@opencode-ai/core/util/effect-flock" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" -import { Npm } from "../src/npm" +import { Npm } from "@opencode-ai/core/npm" import { tmpdir } from "./fixture/fixture" const win = process.platform === "win32" diff --git a/packages/opencode/test/permission/next.test.ts b/packages/opencode/test/permission/next.test.ts index 372e1be7eb4e..0064185f4625 100644 --- a/packages/opencode/test/permission/next.test.ts +++ b/packages/opencode/test/permission/next.test.ts @@ -2,7 +2,7 @@ import { afterEach, test, expect } from "bun:test" import os from "os" import { Cause, Effect, Exit, Fiber, Layer } from "effect" import { Bus } from "../../src/bus" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Permission } from "../../src/permission" import { PermissionID } from "../../src/permission/schema" import { Instance } from "../../src/project/instance" @@ -128,61 +128,45 @@ test("fromConfig - does not expand tilde in middle of path", () => { expect(result).toEqual([{ permission: "external_directory", pattern: "/some/~/path", action: "allow" }]) }) -// Top-level wildcard-vs-specific precedence semantics. -// -// fromConfig sorts top-level keys so wildcard permissions (containing "*") -// come before specific permissions. Combined with `findLast` in evaluate(), -// this gives the intuitive semantic "specific tool rules override the `*` -// fallback", regardless of the order the user wrote the keys in their JSON. -// -// Sub-pattern order inside a single permission key (e.g. `bash: { "*": "allow", "rm": "deny" }`) -// still depends on insertion order — only top-level keys are sorted. - -test("fromConfig - specific key beats wildcard regardless of JSON key order", () => { +// Permission precedence follows config insertion order. `evaluate()` uses the +// last matching rule, so later config entries intentionally override earlier +// entries even when a wildcard appears after a specific permission. + +test("fromConfig - preserves top-level config key order", () => { const wildcardFirst = Permission.fromConfig({ "*": "deny", bash: "allow" }) const specificFirst = Permission.fromConfig({ bash: "allow", "*": "deny" }) - // Both orderings produce the same ruleset - expect(wildcardFirst).toEqual(specificFirst) + expect(wildcardFirst.map((r) => r.permission)).toEqual(["*", "bash"]) + expect(specificFirst.map((r) => r.permission)).toEqual(["bash", "*"]) - // And both evaluate bash → allow (bash rule wins over * fallback) expect(Permission.evaluate("bash", "ls", wildcardFirst).action).toBe("allow") - expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("allow") + expect(Permission.evaluate("bash", "ls", specificFirst).action).toBe("deny") }) -test("fromConfig - wildcard acts as fallback for permissions with no specific rule", () => { - const ruleset = Permission.fromConfig({ bash: "allow", "*": "ask" }) +test("fromConfig - wildcard acts as fallback when it appears before specifics", () => { + const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow" }) expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("ask") expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow") }) -test("fromConfig - top-level ordering: wildcards first, specifics after", () => { +test("fromConfig - top-level ordering is not sorted by wildcard specificity", () => { const ruleset = Permission.fromConfig({ bash: "allow", "*": "ask", edit: "deny", "mcp_*": "allow", }) - // wildcards (* and mcp_*) come before specifics (bash, edit) - const permissions = ruleset.map((r) => r.permission) - expect(permissions.slice(0, 2).sort()).toEqual(["*", "mcp_*"]) - expect(permissions.slice(2)).toEqual(["bash", "edit"]) + expect(ruleset.map((r) => r.permission)).toEqual(["bash", "*", "edit", "mcp_*"]) }) -test("fromConfig - sub-pattern insertion order inside a tool key is preserved (only top-level sorts)", () => { - // Sub-patterns within a single tool key use the documented "`*` first, - // specific patterns after" convention (findLast picks specifics). The - // top-level sort must not touch sub-pattern ordering. +test("fromConfig - sub-pattern insertion order inside a tool key is preserved", () => { const ruleset = Permission.fromConfig({ bash: { "*": "deny", "git *": "allow" } }) expect(ruleset.map((r) => r.pattern)).toEqual(["*", "git *"]) - // * fallback for unknown commands expect(Permission.evaluate("bash", "rm foo", ruleset).action).toBe("deny") - // specific pattern wins for git commands (it's last, findLast picks it) expect(Permission.evaluate("bash", "git status", ruleset).action).toBe("allow") }) -test("fromConfig - canonical documented example unchanged", () => { - // Regression guard for the example in docs/permissions.mdx +test("fromConfig - documented fallback-first example", () => { const ruleset = Permission.fromConfig({ "*": "ask", bash: "allow", edit: "deny" }) expect(Permission.evaluate("bash", "ls", ruleset).action).toBe("allow") expect(Permission.evaluate("edit", "foo.ts", ruleset).action).toBe("deny") @@ -448,7 +432,7 @@ test("evaluate - wildcard permission fallback for unknown tool", () => { expect(result.action).toBe("ask") }) -test("evaluate - permission patterns sorted by length regardless of object order", () => { +test("evaluate - later wildcard permission can override earlier specific permission", () => { const result = Permission.evaluate("bash", "rm", [ { permission: "bash", pattern: "*", action: "allow" }, { permission: "*", pattern: "*", action: "deny" }, diff --git a/packages/opencode/test/plugin/loader-shared.test.ts b/packages/opencode/test/plugin/loader-shared.test.ts index 83e9d71b4f9c..88b3bf343b1d 100644 --- a/packages/opencode/test/plugin/loader-shared.test.ts +++ b/packages/opencode/test/plugin/loader-shared.test.ts @@ -13,7 +13,7 @@ const { Plugin } = await import("../../src/plugin/index") const { PluginLoader } = await import("../../src/plugin/loader") const { readPackageThemes } = await import("../../src/plugin/shared") const { Instance } = await import("../../src/project/instance") -const { Npm } = await import("../../src/npm") +const { Npm } = await import("@opencode-ai/core/npm") afterAll(() => { if (disableDefault === undefined) { diff --git a/packages/opencode/test/plugin/workspace-adaptor.test.ts b/packages/opencode/test/plugin/workspace-adaptor.test.ts index e74522c8be8a..2695e9b28413 100644 --- a/packages/opencode/test/plugin/workspace-adaptor.test.ts +++ b/packages/opencode/test/plugin/workspace-adaptor.test.ts @@ -7,7 +7,7 @@ import { tmpdir } from "../fixture/fixture" const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS = "1" -const { Flag } = await import("../../src/flag/flag") +const { Flag } = await import("@opencode-ai/core/flag/flag") const { Plugin } = await import("../../src/plugin/index") const { Workspace } = await import("../../src/control-plane/workspace") const { Instance } = await import("../../src/project/instance") diff --git a/packages/opencode/test/project/project.test.ts b/packages/opencode/test/project/project.test.ts index 080519a73768..c3aba55de1c1 100644 --- a/packages/opencode/test/project/project.test.ts +++ b/packages/opencode/test/project/project.test.ts @@ -9,8 +9,8 @@ import { ProjectID } from "../../src/project/schema" import { Effect, Layer, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodePath } from "@effect/platform-node" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" void Log.init({ print: false }) diff --git a/packages/opencode/test/project/worktree-remove.test.ts b/packages/opencode/test/project/worktree-remove.test.ts index 5fb2beb28635..fa70ecb893b4 100644 --- a/packages/opencode/test/project/worktree-remove.test.ts +++ b/packages/opencode/test/project/worktree-remove.test.ts @@ -3,7 +3,7 @@ import { describe, expect } from "bun:test" import * as fs from "fs/promises" import path from "path" import { Effect, Layer } from "effect" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Worktree } from "../../src/worktree" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index c0fe635514be..44a25a8e6bae 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect } from "bun:test" import * as fs from "fs/promises" import path from "path" import { Cause, Effect, Exit, Layer } from "effect" -import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Worktree } from "../../src/worktree" import { provideInstance, provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/provider/amazon-bedrock.test.ts b/packages/opencode/test/provider/amazon-bedrock.test.ts index 03f83601ddd3..aff67494be0f 100644 --- a/packages/opencode/test/provider/amazon-bedrock.test.ts +++ b/packages/opencode/test/provider/amazon-bedrock.test.ts @@ -7,7 +7,7 @@ import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Provider } from "../../src/provider" import { Env } from "../../src/env" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "../../src/util" import { Effect } from "effect" import { AppRuntime } from "../../src/effect/app-runtime" diff --git a/packages/opencode/test/provider/gitlab-duo.test.ts b/packages/opencode/test/provider/gitlab-duo.test.ts index 907a32d61d3e..a74ef360b682 100644 --- a/packages/opencode/test/provider/gitlab-duo.test.ts +++ b/packages/opencode/test/provider/gitlab-duo.test.ts @@ -11,7 +11,7 @@ export {} // import { Instance } from "../../src/project/instance" // import { Provider } from "../../src/provider" // import { Env } from "../../src/env" -// import { Global } from "../../src/global" +// import { Global } from "@opencode-ai/core/global" // import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" // test("GitLab Duo: loads provider with API key from environment", async () => { diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 8993020820e3..612fe3e97c25 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -3,7 +3,7 @@ import { mkdir, unlink } from "fs/promises" import path from "path" import { tmpdir } from "../fixture/fixture" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Instance } from "../../src/project/instance" import { Plugin } from "../../src/plugin/index" import { ModelsDev } from "../../src/provider" diff --git a/packages/opencode/test/server/httpapi-bridge.test.ts b/packages/opencode/test/server/httpapi-bridge.test.ts new file mode 100644 index 000000000000..35f173371138 --- /dev/null +++ b/packages/opencode/test/server/httpapi-bridge.test.ts @@ -0,0 +1,134 @@ +import { afterEach, describe, expect, test } from "bun:test" +import type { UpgradeWebSocket } from "hono/ws" +import { Flag } from "@opencode-ai/core/flag/flag" +import { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { FilePaths } from "../../src/server/routes/instance/httpapi/file" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = { + OPENCODE_EXPERIMENTAL_HTTPAPI: Flag.OPENCODE_EXPERIMENTAL_HTTPAPI, + OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD, + OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME, +} + +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app(input?: { password?: string; username?: string }) { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + Flag.OPENCODE_SERVER_PASSWORD = input?.password + Flag.OPENCODE_SERVER_USERNAME = input?.username + return InstanceRoutes(websocket) +} + +function authorization(username: string, password: string) { + return `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` +} + +function fileUrl(input?: { directory?: string; token?: string }) { + const url = new URL(`http://localhost${FilePaths.content}`) + url.searchParams.set("path", "hello.txt") + if (input?.directory) url.searchParams.set("directory", input.directory) + if (input?.token) url.searchParams.set("auth_token", input.token) + return url +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original.OPENCODE_EXPERIMENTAL_HTTPAPI + Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD + Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME + await Instance.disposeAll() + await resetDatabase() +}) + +describe("HttpApi Hono bridge", () => { + test("allows requests when auth is disabled", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(`${tmp.path}/hello.txt`, "hello") + + const response = await app().request(fileUrl(), { + headers: { + "x-opencode-directory": tmp.path, + }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ content: "hello" }) + }) + + test("provides instance context to bridged handlers", async () => { + await using tmp = await tmpdir({ git: true }) + + const response = await app().request("/project/current", { + headers: { + "x-opencode-directory": tmp.path, + }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ worktree: tmp.path }) + }) + + test("requires credentials when auth is enabled", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(`${tmp.path}/hello.txt`, "hello") + + const [missing, bad, good] = await Promise.all([ + app({ password: "secret" }).request(fileUrl(), { + headers: { "x-opencode-directory": tmp.path }, + }), + app({ password: "secret" }).request(fileUrl(), { + headers: { + authorization: authorization("opencode", "wrong"), + "x-opencode-directory": tmp.path, + }, + }), + app({ password: "secret" }).request(fileUrl(), { + headers: { + authorization: authorization("opencode", "secret"), + "x-opencode-directory": tmp.path, + }, + }), + ]) + + expect(missing.status).toBe(401) + expect(bad.status).toBe(401) + expect(good.status).toBe(200) + }) + + test("accepts auth_token query credentials", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(`${tmp.path}/hello.txt`, "hello") + + const response = await app({ password: "secret" }).request( + fileUrl({ token: Buffer.from("opencode:secret").toString("base64") }), + { + headers: { + "x-opencode-directory": tmp.path, + }, + }, + ) + + expect(response.status).toBe(200) + }) + + test("selects instance from query before directory header", async () => { + await using header = await tmpdir({ git: true }) + await using query = await tmpdir({ git: true }) + await Bun.write(`${header.path}/hello.txt`, "header") + await Bun.write(`${query.path}/hello.txt`, "query") + + const response = await app().request(fileUrl({ directory: query.path }), { + headers: { + "x-opencode-directory": header.path, + }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ content: "query" }) + }) +}) diff --git a/packages/opencode/test/server/httpapi-config.test.ts b/packages/opencode/test/server/httpapi-config.test.ts new file mode 100644 index 000000000000..351ac2c5051a --- /dev/null +++ b/packages/opencode/test/server/httpapi-config.test.ts @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, test } from "bun:test" +import type { UpgradeWebSocket } from "hono/ws" +import path from "path" +import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "@/bus/global" +import { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app() { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + return InstanceRoutes(websocket) +} + +async function waitDisposed(directory: string) { + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + GlobalBus.off("event", onEvent) + reject(new Error("timed out waiting for instance disposal")) + }, 10_000) + + function onEvent(event: { directory?: string; payload: { type?: string } }) { + if (event.payload.type !== "server.instance.disposed" || event.directory !== directory) return + clearTimeout(timer) + GlobalBus.off("event", onEvent) + resolve() + } + + GlobalBus.on("event", onEvent) + }) +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original + await Instance.disposeAll() + await resetDatabase() +}) + +describe("config HttpApi", () => { + test("serves config update through Hono bridge", async () => { + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + const disposed = waitDisposed(tmp.path) + + const response = await app().request("/config", { + method: "PATCH", + headers: { + "content-type": "application/json", + "x-opencode-directory": tmp.path, + }, + body: JSON.stringify({ username: "patched-user", formatter: false, lsp: false }), + }) + + expect(response.status).toBe(200) + expect(await response.json()).toMatchObject({ username: "patched-user", formatter: false, lsp: false }) + await disposed + expect(await Bun.file(path.join(tmp.path, "config.json")).json()).toMatchObject({ + username: "patched-user", + formatter: false, + lsp: false, + }) + }) +}) diff --git a/packages/opencode/test/server/httpapi-experimental.test.ts b/packages/opencode/test/server/httpapi-experimental.test.ts new file mode 100644 index 000000000000..e355b00277a2 --- /dev/null +++ b/packages/opencode/test/server/httpapi-experimental.test.ts @@ -0,0 +1,132 @@ +import { afterEach, describe, expect, test } from "bun:test" +import type { UpgradeWebSocket } from "hono/ws" +import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "@/bus/global" +import { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { ExperimentalPaths } from "../../src/server/routes/instance/httpapi/experimental" +import { Log } from "../../src/util" +import { Worktree } from "../../src/worktree" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app() { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + return InstanceRoutes(websocket) +} + +async function waitReady(directory: string) { + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + GlobalBus.off("event", onEvent) + reject(new Error("timed out waiting for worktree.ready")) + }, 10_000) + + function onEvent(event: { directory?: string; payload: { type?: string } }) { + if (event.payload.type !== Worktree.Event.Ready.type || event.directory !== directory) return + clearTimeout(timer) + GlobalBus.off("event", onEvent) + resolve() + } + + GlobalBus.on("event", onEvent) + }) +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original + await Instance.disposeAll() + await resetDatabase() +}) + +describe("experimental HttpApi", () => { + test("serves read-only experimental endpoints through Hono bridge", async () => { + await using tmp = await tmpdir({ + config: { + formatter: false, + lsp: false, + mcp: { + demo: { + type: "local", + command: ["echo", "demo"], + enabled: false, + }, + }, + }, + }) + + const headers = { "x-opencode-directory": tmp.path } + const [consoleState, consoleOrgs, toolIDs, worktrees, resources] = await Promise.all([ + app().request(ExperimentalPaths.console, { headers }), + app().request(ExperimentalPaths.consoleOrgs, { headers }), + app().request(ExperimentalPaths.toolIDs, { headers }), + app().request(ExperimentalPaths.worktree, { headers }), + app().request(ExperimentalPaths.resource, { headers }), + ]) + + expect(consoleState.status).toBe(200) + expect(await consoleState.json()).toEqual({ + consoleManagedProviders: [], + switchableOrgCount: 0, + }) + + expect(consoleOrgs.status).toBe(200) + expect(await consoleOrgs.json()).toEqual({ orgs: [] }) + + expect(toolIDs.status).toBe(200) + expect(await toolIDs.json()).toContain("bash") + + expect(worktrees.status).toBe(200) + expect(await worktrees.json()).toEqual([]) + + expect(resources.status).toBe(200) + expect(await resources.json()).toEqual({}) + }) + + test("serves worktree mutations through Hono bridge", async () => { + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + + const headers = { "x-opencode-directory": tmp.path, "content-type": "application/json" } + const created = await app().request(ExperimentalPaths.worktree, { + method: "POST", + headers, + body: JSON.stringify({ name: "api-test" }), + }) + + expect(created.status).toBe(200) + const info = (await created.json()) as Worktree.Info + expect(info).toMatchObject({ name: "api-test", branch: "opencode/api-test" }) + await waitReady(info.directory) + + const listed = await app().request(ExperimentalPaths.worktree, { headers }) + expect(listed.status).toBe(200) + expect(await listed.json()).toContain(info.directory) + + const reset = await app().request(ExperimentalPaths.worktreeReset, { + method: "POST", + headers, + body: JSON.stringify({ directory: info.directory }), + }) + + expect(reset.status).toBe(200) + expect(await reset.json()).toBe(true) + + const removed = await app().request(ExperimentalPaths.worktree, { + method: "DELETE", + headers, + body: JSON.stringify({ directory: info.directory }), + }) + + expect(removed.status).toBe(200) + expect(await removed.json()).toBe(true) + + const afterRemove = await app().request(ExperimentalPaths.worktree, { headers }) + expect(afterRemove.status).toBe(200) + expect(await afterRemove.json()).toEqual([]) + }) +}) diff --git a/packages/opencode/test/server/httpapi-file.test.ts b/packages/opencode/test/server/httpapi-file.test.ts new file mode 100644 index 000000000000..302e0a349cb7 --- /dev/null +++ b/packages/opencode/test/server/httpapi-file.test.ts @@ -0,0 +1,77 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { Context } from "effect" +import path from "path" +import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server" +import { FilePaths } from "../../src/server/routes/instance/httpapi/file" +import { Instance } from "../../src/project/instance" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const context = Context.empty() as Context.Context + +function request(route: string, directory: string, query?: Record) { + const url = new URL(`http://localhost${route}`) + for (const [key, value] of Object.entries(query ?? {})) { + url.searchParams.set(key, value) + } + return ExperimentalHttpApiServer.webHandler().handler( + new Request(url, { + headers: { + "x-opencode-directory": directory, + }, + }), + context, + ) +} + +afterEach(async () => { + await Instance.disposeAll() + await resetDatabase() +}) + +describe("file HttpApi", () => { + test("serves read endpoints", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(path.join(tmp.path, "hello.txt"), "hello") + + const [list, content, status] = await Promise.all([ + request(FilePaths.list, tmp.path, { path: "." }), + request(FilePaths.content, tmp.path, { path: "hello.txt" }), + request(FilePaths.status, tmp.path), + ]) + + expect(list.status).toBe(200) + expect(await list.json()).toContainEqual( + expect.objectContaining({ name: "hello.txt", path: "hello.txt", type: "file" }), + ) + + expect(content.status).toBe(200) + expect(await content.json()).toMatchObject({ type: "text", content: "hello" }) + + expect(status.status).toBe(200) + expect(await status.json()).toContainEqual({ path: "hello.txt", added: 1, removed: 0, status: "added" }) + }) + + test("serves search endpoints", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(path.join(tmp.path, "hello.txt"), "needle") + + const [text, files, symbols] = await Promise.all([ + request(FilePaths.findText, tmp.path, { pattern: "needle" }), + request(FilePaths.findFile, tmp.path, { query: "hello", type: "file" }), + request(FilePaths.findSymbol, tmp.path, { query: "hello" }), + ]) + + expect(text.status).toBe(200) + expect(await text.json()).toContainEqual(expect.objectContaining({ line_number: 1 })) + + expect(files.status).toBe(200) + expect(await files.json()).toContain("hello.txt") + + expect(symbols.status).toBe(200) + expect(await symbols.json()).toEqual([]) + }) +}) diff --git a/packages/opencode/test/server/httpapi-instance.test.ts b/packages/opencode/test/server/httpapi-instance.test.ts new file mode 100644 index 000000000000..463dbaa8783d --- /dev/null +++ b/packages/opencode/test/server/httpapi-instance.test.ts @@ -0,0 +1,103 @@ +import { afterEach, describe, expect, test } from "bun:test" +import type { UpgradeWebSocket } from "hono/ws" +import path from "path" +import { Flag } from "@opencode-ai/core/flag/flag" +import { GlobalBus } from "@/bus/global" +import { Instance } from "../../src/project/instance" +import { InstanceRoutes } from "../../src/server/routes/instance" +import { InstancePaths } from "../../src/server/routes/instance/httpapi/instance" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI +const websocket = (() => () => new Response(null, { status: 501 })) as unknown as UpgradeWebSocket + +function app() { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true + return InstanceRoutes(websocket) +} + +afterEach(async () => { + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original + await Instance.disposeAll() + await resetDatabase() +}) + +describe("instance HttpApi", () => { + test("serves path and VCS read endpoints through Hono bridge", async () => { + await using tmp = await tmpdir({ git: true }) + await Bun.write(path.join(tmp.path, "changed.txt"), "hello") + + const vcsDiff = new URL(`http://localhost${InstancePaths.vcsDiff}`) + vcsDiff.searchParams.set("mode", "git") + + const [paths, vcs, diff] = await Promise.all([ + app().request(InstancePaths.path, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.vcs, { headers: { "x-opencode-directory": tmp.path } }), + app().request(vcsDiff, { headers: { "x-opencode-directory": tmp.path } }), + ]) + + expect(paths.status).toBe(200) + expect(await paths.json()).toMatchObject({ directory: tmp.path, worktree: tmp.path }) + + expect(vcs.status).toBe(200) + expect(await vcs.json()).toMatchObject({ branch: expect.any(String) }) + + expect(diff.status).toBe(200) + expect(await diff.json()).toContainEqual( + expect.objectContaining({ file: "changed.txt", additions: 1, status: "added" }), + ) + }) + + test("serves catalog read endpoints through Hono bridge", async () => { + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) + + const [commands, agents, skills, lsp, formatter] = await Promise.all([ + app().request(InstancePaths.command, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.agent, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.skill, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.lsp, { headers: { "x-opencode-directory": tmp.path } }), + app().request(InstancePaths.formatter, { headers: { "x-opencode-directory": tmp.path } }), + ]) + + expect(commands.status).toBe(200) + expect(await commands.json()).toContainEqual(expect.objectContaining({ name: "init", source: "command" })) + + expect(agents.status).toBe(200) + expect(await agents.json()).toContainEqual(expect.objectContaining({ name: "build", mode: "primary" })) + + expect(skills.status).toBe(200) + expect(await skills.json()).toBeArray() + + expect(lsp.status).toBe(200) + expect(await lsp.json()).toEqual([]) + + expect(formatter.status).toBe(200) + expect(await formatter.json()).toEqual([]) + }) + + test("serves instance dispose through Hono bridge", async () => { + await using tmp = await tmpdir() + + const disposed = new Promise((resolve) => { + const onEvent = (event: { directory?: string; payload: { type?: string } }) => { + if (event.payload.type !== "server.instance.disposed") return + GlobalBus.off("event", onEvent) + resolve(event.directory) + } + GlobalBus.on("event", onEvent) + }) + + const response = await app().request(InstancePaths.dispose, { + method: "POST", + headers: { "x-opencode-directory": tmp.path }, + }) + + expect(response.status).toBe(200) + expect(await response.json()).toBe(true) + expect(await disposed).toBe(tmp.path) + }) +}) diff --git a/packages/opencode/test/server/httpapi-mcp.test.ts b/packages/opencode/test/server/httpapi-mcp.test.ts new file mode 100644 index 000000000000..3da1dc9333d3 --- /dev/null +++ b/packages/opencode/test/server/httpapi-mcp.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { Context } from "effect" +import { ExperimentalHttpApiServer } from "../../src/server/routes/instance/httpapi/server" +import { McpPaths } from "../../src/server/routes/instance/httpapi/mcp" +import { Instance } from "../../src/project/instance" +import { Log } from "../../src/util" +import { resetDatabase } from "../fixture/db" +import { tmpdir } from "../fixture/fixture" + +void Log.init({ print: false }) + +const context = Context.empty() as Context.Context + +function request(route: string, directory: string) { + return ExperimentalHttpApiServer.webHandler().handler( + new Request(`http://localhost${route}`, { + headers: { + "x-opencode-directory": directory, + }, + }), + context, + ) +} + +afterEach(async () => { + await Instance.disposeAll() + await resetDatabase() +}) + +describe("mcp HttpApi", () => { + test("serves status endpoint", async () => { + await using tmp = await tmpdir({ + config: { + mcp: { + demo: { + type: "local", + command: ["echo", "demo"], + enabled: false, + }, + }, + }, + }) + + const response = await request(McpPaths.status, tmp.path) + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ demo: { status: "disabled" } }) + }) +}) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 4fe9c1551136..1b2b120b6164 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -25,7 +25,7 @@ import * as SessionProcessorModule from "../../src/session/processor" import { Snapshot } from "../../src/snapshot" import { ProviderTest } from "../fake/provider" import { testEffect } from "../lib/effect" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" void Log.init({ print: false }) diff --git a/packages/opencode/test/session/instruction.test.ts b/packages/opencode/test/session/instruction.test.ts index c46bbd20bdf2..60882d2b318c 100644 --- a/packages/opencode/test/session/instruction.test.ts +++ b/packages/opencode/test/session/instruction.test.ts @@ -6,7 +6,7 @@ import { Instruction } from "../../src/session/instruction" import type { MessageV2 } from "../../src/session/message-v2" import { Instance } from "../../src/project/instance" import { MessageID, PartID, SessionID } from "../../src/session/schema" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { tmpdir } from "../fixture/fixture" const run = (effect: Effect.Effect) => diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 74ce913077d3..fee42a9397a1 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -19,7 +19,7 @@ import { SessionStatus } from "../../src/session/status" import { SessionSummary } from "../../src/session/summary" import { Snapshot } from "../../src/snapshot" import { Log } from "../../src/util" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirServer } from "../fixture/fixture" import { testEffect } from "../lib/effect" import { raw, reply, TestLLMServer } from "../lib/llm-server" diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 8ffb20f15419..7e33777463d8 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -4,7 +4,7 @@ import { expect } from "bun:test" import { Cause, Effect, Exit, Fiber, Layer } from "effect" import path from "path" import { fileURLToPath } from "url" -import { NamedError } from "@opencode-ai/shared/util/error" +import { NamedError } from "@opencode-ai/core/util/error" import { Agent as AgentSvc } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { Command } from "../../src/command" @@ -21,7 +21,7 @@ import { Todo } from "../../src/session/todo" import { Session } from "../../src/session" import { LLM } from "../../src/session/llm" import { MessageV2 } from "../../src/session/message-v2" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { SessionCompaction } from "../../src/session/compaction" import { SessionSummary } from "../../src/session/summary" import { Instruction } from "../../src/session/instruction" @@ -38,7 +38,7 @@ import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool" import { Truncate } from "../../src/tool" import { Log } from "../../src/util" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" import { provideTmpdirInstance, provideTmpdirServer } from "../fixture/fixture" @@ -1078,6 +1078,30 @@ unix("shell completes a fast command on the preferred shell", () => ), ) +unix("shell commands can change directory after startup", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const { prompt, run, chat } = yield* boot() + const parent = path.dirname(dir) + const result = yield* prompt.shell({ + sessionID: chat.id, + agent: "build", + command: "cd .. && pwd", + }) + + expect(result.info.role).toBe("assistant") + const tool = completedTool(result.parts) + if (!tool) return + + expect(tool.state.output).toContain(parent) + expect(tool.state.metadata.output).toContain(parent) + yield* run.assertNotBusy(chat.id) + }), + { git: true, config: cfg }, + ), +) + unix("shell lists files from the project directory", () => provideTmpdirInstance( (dir) => diff --git a/packages/opencode/test/session/reconnection.test.ts b/packages/opencode/test/session/reconnection.test.ts new file mode 100644 index 000000000000..cf4e9155089f --- /dev/null +++ b/packages/opencode/test/session/reconnection.test.ts @@ -0,0 +1,186 @@ +import { describe, test, expect } from "bun:test" +import path from "path" +import { Log } from "../../src/util/log" +import { Instance } from "../../src/project/instance" +import { Session } from "../../src/session" +import { SessionProcessor } from "../../src/session/processor" +import { SessionStatus } from "../../src/session/status" +import { SessionRetry } from "../../src/session/retry" +import { Bus } from "../../src/bus" +import { MessageV2 } from "../../src/session/message-v2" +import { LLM } from "../../src/session/llm" +import type { Provider } from "../../src/provider/provider" +import { MessageID, PartID } from "../../src/session/schema" +import { ProviderID, ModelID } from "../../src/provider/schema" + +const projectRoot = path.join(__dirname, "../..") +Log.init({ print: false }) + +const model: Provider.Model = { + id: ModelID.make("test-model"), + providerID: ProviderID.make("test"), + api: { id: "test", url: "http://localhost:9999", npm: "@ai-sdk/openai" }, + name: "Test Model", + capabilities: { + temperature: false, + reasoning: false, + attachment: false, + toolcall: false, + input: { text: true, audio: false, image: false, video: false, pdf: false }, + output: { text: true, audio: false, image: false, video: false, pdf: false }, + interleaved: false, + }, + cost: { input: 0, output: 0, cache: { read: 0, write: 0 } }, + limit: { context: 100000, output: 4096 }, + status: "active", + options: {}, + headers: {}, + release_date: "2024-01-01", +} + +async function makeMsg() { + const session = await Session.create({}) + const userID = MessageID.ascending() + await Session.updateMessage({ + id: userID, + sessionID: session.id, + role: "user", + time: { created: Date.now() }, + agent: "build", + model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") }, + } as unknown as MessageV2.Info) + const msg: MessageV2.Assistant = { + id: MessageID.ascending(), + sessionID: session.id, + role: "assistant", + time: { created: Date.now() }, + parentID: userID, + modelID: ModelID.make("test"), + providerID: ProviderID.make("test"), + mode: "primary", + agent: "build", + path: { cwd: projectRoot, root: projectRoot }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + } + await Session.updateMessage(msg) + return { session, msg } +} + +async function* sseTimeout() { + yield { type: "start" } + throw new Error("SSE read timed out") +} + +async function* ok() { + yield { type: "start" } +} + +type Reconnecting = Extract + +describe("session.processor.reconnection", () => { + test("busy → reconnecting(1) → busy → success with partial part cleanup", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const { session, msg } = await makeMsg() + + await Session.updatePart({ + id: PartID.ascending(), + sessionID: session.id, + messageID: msg.id, + type: "text", + text: "pre-existing partial", + time: { start: Date.now() }, + }) + + const statuses: SessionStatus.Info[] = [] + const unsub = Bus.subscribe(SessionStatus.Event.Status, (e) => { + statuses.push(e.properties.status) + }) + + const [prevStream, prevSleep] = [LLM.stream, SessionRetry.sleep] + ;(SessionRetry as any).sleep = async () => {} + + let call = 0 + ;(LLM as any).stream = async () => { + call++ + return { fullStream: call === 1 ? sseTimeout() : ok() } + } + + const ctrl = new AbortController() + const proc = SessionProcessor.create({ + assistantMessage: msg, + sessionID: session.id, + model, + abort: ctrl.signal, + }) + + const result = await proc.process({} as unknown as LLM.StreamInput) + + ;(LLM as any).stream = prevStream + ;(SessionRetry as any).sleep = prevSleep + unsub() + + expect(call).toBe(2) + expect(result).toBe("continue") + + const reconnecting = statuses.filter((s): s is Reconnecting => s.type === "reconnecting") + expect(reconnecting.length).toBe(1) + expect(reconnecting[0].attempt).toBe(1) + expect(reconnecting[0].message).toBe("SSE read timed out") + + expect(statuses.filter((s) => s.type === "busy").length).toBeGreaterThanOrEqual(2) + + const parts = await MessageV2.parts(msg.id) + const text = parts.find((p): p is MessageV2.TextPart => p.type === "text") + expect(text).toBeDefined() + expect(text?.text).toBe("") + + await Session.remove(session.id) + }, + }) + }, 30_000) + + test("max network retries exhausted: 5 reconnecting states → idle with error → stop", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const { session, msg } = await makeMsg() + + const statuses: SessionStatus.Info[] = [] + const unsub = Bus.subscribe(SessionStatus.Event.Status, (e) => { + statuses.push(e.properties.status) + }) + + const [prevStream, prevSleep] = [LLM.stream, SessionRetry.sleep] + ;(SessionRetry as any).sleep = async () => {} + ;(LLM as any).stream = async () => ({ fullStream: sseTimeout() }) + + const ctrl = new AbortController() + const proc = SessionProcessor.create({ + assistantMessage: msg, + sessionID: session.id, + model, + abort: ctrl.signal, + }) + + const result = await proc.process({} as unknown as LLM.StreamInput) + + ;(LLM as any).stream = prevStream + ;(SessionRetry as any).sleep = prevSleep + unsub() + + expect(result).toBe("stop") + + const reconnecting = statuses.filter((s): s is Reconnecting => s.type === "reconnecting") + expect(reconnecting.length).toBe(5) + expect(reconnecting.map((s) => s.attempt)).toStrictEqual([1, 2, 3, 4, 5]) + + expect(statuses.at(-1)?.type).toBe("idle") + + await Session.remove(session.id) + }, + }) + }, 30_000) +}) diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 6ca8775f30dd..f18ef9fce894 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { NamedError } from "@opencode-ai/shared/util/error" +import type { NamedError } from "@opencode-ai/core/util/error" import { APICallError } from "ai" import { setTimeout as sleep } from "node:timers/promises" import { Effect, Schedule } from "effect" @@ -262,7 +262,7 @@ describe("session.message-v2.fromError", () => { expect(MessageV2.APIError.isInstance(result)).toBe(true) expect((result as MessageV2.APIError).data.isRetryable).toBe(true) - expect((result as MessageV2.APIError).data.message).toBe("Connection reset by server") + expect((result as MessageV2.APIError).data.message).toBe("Network error") expect((result as MessageV2.APIError).data.metadata?.code).toBe("ECONNRESET") expect((result as MessageV2.APIError).data.metadata?.message).toInclude("socket connection") }, @@ -271,14 +271,14 @@ describe("session.message-v2.fromError", () => { test("ECONNRESET socket error is retryable", () => { const error = new MessageV2.APIError({ - message: "Connection reset by server", + message: "Network error", isRetryable: true, metadata: { code: "ECONNRESET", message: "The socket connection was closed unexpectedly" }, }).toObject() as MessageV2.APIError const retryable = SessionRetry.retryable(error) expect(retryable).toBeDefined() - expect(retryable).toBe("Connection reset by server") + expect(retryable).toBe("Network error") }) test("marks OpenAI 404 status codes as retryable", () => { diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index f28fb94c0be5..d77ef3e051b8 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -9,7 +9,7 @@ import { MessageV2 } from "../../src/session/message-v2" import { Snapshot } from "../../src/snapshot" import { Log } from "../../src/util" import { MessageID, PartID, SessionID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 651754733909..269c23148b8a 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -51,8 +51,8 @@ import { SessionStatus } from "../../src/session/status" import { Snapshot } from "../../src/snapshot" import { ToolRegistry } from "../../src/tool" import { Truncate } from "../../src/tool" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" import { Format } from "../../src/format" diff --git a/packages/opencode/test/share/share-next.test.ts b/packages/opencode/test/share/share-next.test.ts index e217300d0946..5470d654ade1 100644 --- a/packages/opencode/test/share/share-next.test.ts +++ b/packages/opencode/test/share/share-next.test.ts @@ -6,7 +6,7 @@ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstab import { AccessToken, AccountID, OrgID, RefreshToken } from "../../src/account/schema" import { Account } from "../../src/account/account" import { AccountRepo } from "../../src/account/repo" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Bus } from "../../src/bus" import { Config } from "../../src/config" import { Provider } from "../../src/provider" diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index 3f82103293af..230a9e03e40a 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect, beforeAll, afterAll } from "bun:test" import { Effect } from "effect" import { Discovery } from "../../src/skill/discovery" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Filesystem } from "../../src/util" import { rm } from "fs/promises" import path from "path" diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 21c6c7e65137..bfcb0dcd67bd 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Skill } from "../../src/skill" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideInstance, provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { testEffect } from "../lib/effect" import path from "path" diff --git a/packages/opencode/test/storage/db.test.ts b/packages/opencode/test/storage/db.test.ts index 6beb95ac5f15..2cd9f817c7c6 100644 --- a/packages/opencode/test/storage/db.test.ts +++ b/packages/opencode/test/storage/db.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Global } from "../../src/global" -import { InstallationChannel } from "../../src/installation/version" +import { Global } from "@opencode-ai/core/global" +import { InstallationChannel } from "@opencode-ai/core/installation/version" import { Database } from "../../src/storage" describe("Database.Path", () => { diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index 019faf061ca4..2635737941b2 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -6,7 +6,7 @@ import path from "path" import fs from "fs/promises" import { readFileSync, readdirSync } from "fs" import { JsonMigration } from "../../src/storage" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { ProjectTable } from "../../src/project/project.sql" import { ProjectID } from "../../src/project/schema" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" diff --git a/packages/opencode/test/storage/storage.test.ts b/packages/opencode/test/storage/storage.test.ts index c35244bb7a9d..a3d5a8ac5dd9 100644 --- a/packages/opencode/test/storage/storage.test.ts +++ b/packages/opencode/test/storage/storage.test.ts @@ -1,10 +1,10 @@ import { describe, expect } from "bun:test" import path from "path" import { Effect, Exit, Layer } from "effect" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Git } from "../../src/git" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Storage } from "../../src/storage" import { tmpdirScoped } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/sync/index.test.ts b/packages/opencode/test/sync/index.test.ts index d50f0d7c94ef..c9f6812ca7cb 100644 --- a/packages/opencode/test/sync/index.test.ts +++ b/packages/opencode/test/sync/index.test.ts @@ -7,7 +7,7 @@ import { SyncEvent } from "../../src/sync" import { Database } from "../../src/storage" import { EventTable } from "../../src/sync/event.sql" import { Identifier } from "../../src/id/id" -import { Flag } from "../../src/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { initProjectors } from "../../src/server/projectors" const original = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index fa88432136a5..f311b3d9b310 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -5,7 +5,7 @@ import { Effect, ManagedRuntime, Layer } from "effect" import { ApplyPatchTool } from "../../src/tool/apply_patch" import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Format } from "../../src/format" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index d66cfc3e370a..32cd43100145 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -11,8 +11,8 @@ import type { Permission } from "../../src/permission" import { Agent } from "../../src/agent/agent" import { Truncate } from "../../src/tool" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Plugin } from "../../src/plugin" const runtime = ManagedRuntime.make( diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 82e1b4a7fd4b..fb20805918d5 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -6,7 +6,7 @@ import { EditTool } from "../../src/tool/edit" import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Format } from "../../src/format" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" diff --git a/packages/opencode/test/tool/glob.test.ts b/packages/opencode/test/tool/glob.test.ts index 87d35715dd6f..a8637ea9c56b 100644 --- a/packages/opencode/test/tool/glob.test.ts +++ b/packages/opencode/test/tool/glob.test.ts @@ -3,9 +3,9 @@ import path from "path" import { Cause, Effect, Exit, Layer } from "effect" import { GlobTool } from "../../src/tool/glob" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Ripgrep } from "../../src/file/ripgrep" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Truncate } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { provideTmpdirInstance } from "../fixture/fixture" diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index 388828f6eb8e..acdaff03acf2 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -4,11 +4,11 @@ import { Effect, Layer } from "effect" import { GrepTool } from "../../src/tool/grep" import { provideInstance, provideTmpdirInstance } from "../fixture/fixture" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Truncate } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { Ripgrep } from "../../src/file/ripgrep" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { testEffect } from "../lib/effect" const it = testEffect( diff --git a/packages/opencode/test/tool/lsp.test.ts b/packages/opencode/test/tool/lsp.test.ts new file mode 100644 index 000000000000..b7a52da19c6c --- /dev/null +++ b/packages/opencode/test/tool/lsp.test.ts @@ -0,0 +1,162 @@ +import { afterEach, describe, expect } from "bun:test" +import { Effect, Layer } from "effect" +import path from "path" +import { Agent } from "../../src/agent/agent" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { LSP } from "../../src/lsp" +import { Permission } from "../../src/permission" +import { Instance } from "../../src/project/instance" +import { MessageID, SessionID } from "../../src/session/schema" +import { Tool, Truncate } from "../../src/tool" +import { LspTool } from "../../src/tool/lsp" +import { provideTmpdirInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" + +afterEach(async () => { + await Instance.disposeAll() +}) + +const ctx = { + sessionID: SessionID.make("ses_test"), + messageID: MessageID.make(""), + callID: "", + agent: "build", + abort: AbortSignal.any([]), + messages: [], + metadata: () => Effect.void, + ask: () => Effect.void, +} + +const lsp = Layer.succeed( + LSP.Service, + LSP.Service.of({ + init: () => Effect.void, + status: () => Effect.succeed([]), + hasClients: () => Effect.succeed(true), + touchFile: () => Effect.void, + diagnostics: () => Effect.succeed({}), + hover: () => Effect.succeed([]), + definition: () => Effect.succeed([]), + references: () => Effect.succeed([]), + implementation: () => Effect.succeed([]), + documentSymbol: () => Effect.succeed([]), + workspaceSymbol: () => Effect.succeed([]), + prepareCallHierarchy: () => Effect.succeed([]), + incomingCalls: () => Effect.succeed([]), + outgoingCalls: () => Effect.succeed([]), + }), +) + +const it = testEffect( + Layer.mergeAll( + Agent.defaultLayer, + AppFileSystem.defaultLayer, + CrossSpawnSpawner.defaultLayer, + Truncate.defaultLayer, + lsp, + ), +) + +const init = Effect.fn("LspToolTest.init")(function* () { + const info = yield* LspTool + return yield* info.init() +}) + +const run = Effect.fn("LspToolTest.run")(function* ( + args: Tool.InferParameters, + next: Tool.Context = ctx, +) { + const tool = yield* init() + return yield* tool.execute(args, next) +}) + +const put = Effect.fn("LspToolTest.put")(function* (file: string) { + const fs = yield* AppFileSystem.Service + yield* fs.writeWithDirs(file, "export const x = 1\n") +}) + +const asks = () => { + const items: Array> = [] + return { + items, + next: { + ...ctx, + ask: (req: Omit) => + Effect.sync(() => { + items.push(req) + }), + }, + } +} + +describe("tool.lsp", () => { + describe("permission metadata", () => { + it.live("keeps cursor details for position-based operations", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const file = path.join(dir, "test.ts") + yield* put(file) + + const { items, next } = asks() + const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next) + const req = items.find((item) => item.permission === "lsp") + + expect(req).toBeDefined() + expect(req!.metadata).toEqual({ + operation: "goToDefinition", + filePath: file, + line: 3, + character: 7, + }) + expect(result.title).toBe("goToDefinition test.ts:3:7") + }), + { git: true }, + ), + ) + + it.live("omits cursor details for documentSymbol", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const file = path.join(dir, "test.ts") + yield* put(file) + + const { items, next } = asks() + const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next) + const req = items.find((item) => item.permission === "lsp") + + expect(req).toBeDefined() + expect(req!.metadata).toEqual({ + operation: "documentSymbol", + filePath: file, + }) + expect(result.title).toBe("documentSymbol test.ts") + }), + { git: true }, + ), + ) + + it.live("omits file and cursor details for workspaceSymbol", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const file = path.join(dir, "test.ts") + yield* put(file) + + const { items, next } = asks() + const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next) + const req = items.find((item) => item.permission === "lsp") + + expect(req).toBeDefined() + expect(req!.metadata).toEqual({ + operation: "workspaceSymbol", + }) + expect(result.title).toBe("workspaceSymbol") + }), + { git: true }, + ), + ) + }) +}) diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index 17718b2b3a13..537d1f950126 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -4,7 +4,7 @@ import { QuestionTool } from "../../src/tool/question" import { Question } from "../../src/question" import { SessionID, MessageID } from "../../src/session/schema" import { Agent } from "../../src/agent/agent" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Truncate } from "../../src/tool" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 42817d15dfa4..b9c313bdcb87 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -2,8 +2,8 @@ import { afterEach, describe, expect } from "bun:test" import { Cause, Effect, Exit, Layer } from "effect" import path from "path" import { Agent } from "../../src/agent/agent" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { LSP } from "../../src/lsp" import { Permission } from "../../src/permission" import { Instance } from "../../src/project/instance" diff --git a/packages/opencode/test/tool/registry.test.ts b/packages/opencode/test/tool/registry.test.ts index dbb89e09a932..523352d41bc4 100644 --- a/packages/opencode/test/tool/registry.test.ts +++ b/packages/opencode/test/tool/registry.test.ts @@ -3,7 +3,7 @@ import path from "path" import fs from "fs/promises" import { Effect, Layer } from "effect" import { Instance } from "../../src/project/instance" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { ToolRegistry } from "../../src/tool" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index b12940e4dc2f..43659187f3d0 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -1,4 +1,4 @@ -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Effect, Layer } from "effect" import { afterEach, describe, expect } from "bun:test" import path from "path" diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index b94dd5208655..490b3f200eb8 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Agent } from "../../src/agent/agent" import { Config } from "../../src/config" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Instance } from "../../src/project/instance" import { Session } from "../../src/session" import { MessageV2 } from "../../src/session/message-v2" diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 36131f9596a3..e6e3831899ce 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -5,14 +5,14 @@ import fs from "fs/promises" import { WriteTool } from "../../src/tool/write" import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" -import { AppFileSystem } from "@opencode-ai/shared/filesystem" +import { AppFileSystem } from "@opencode-ai/core/filesystem" import { Bus } from "../../src/bus" import { Format } from "../../src/format" import { Truncate } from "../../src/tool" import { Tool } from "../../src/tool" import { Agent } from "../../src/agent/agent" import { SessionID, MessageID } from "../../src/session/schema" -import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" diff --git a/packages/opencode/test/util/glob.test.ts b/packages/opencode/test/util/glob.test.ts index e982d5194cd5..4ed2f71f3928 100644 --- a/packages/opencode/test/util/glob.test.ts +++ b/packages/opencode/test/util/glob.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import path from "path" import fs from "fs/promises" -import { Glob } from "@opencode-ai/shared/util/glob" +import { Glob } from "@opencode-ai/core/util/glob" import { tmpdir } from "../fixture/fixture" describe("Glob", () => { diff --git a/packages/opencode/test/util/log.test.ts b/packages/opencode/test/util/log.test.ts index 336b16a17bbe..9a3b61732d23 100644 --- a/packages/opencode/test/util/log.test.ts +++ b/packages/opencode/test/util/log.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" -import { Global } from "../../src/global" +import { Global } from "@opencode-ai/core/global" import { Log } from "../../src/util" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/util/module.test.ts b/packages/opencode/test/util/module.test.ts index 6725149c74be..19c7958fc3a6 100644 --- a/packages/opencode/test/util/module.test.ts +++ b/packages/opencode/test/util/module.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Module } from "@opencode-ai/shared/util/module" +import { Module } from "@opencode-ai/core/util/module" import { Filesystem } from "../../src/util" import { tmpdir } from "../fixture/fixture" diff --git a/packages/opencode/test/workspace/workspace-restore.test.ts b/packages/opencode/test/workspace/workspace-restore.test.ts index ad6ac2c5fd85..2f8b236b5ff9 100644 --- a/packages/opencode/test/workspace/workspace-restore.test.ts +++ b/packages/opencode/test/workspace/workspace-restore.test.ts @@ -6,7 +6,7 @@ import { registerAdaptor } from "../../src/control-plane/adaptors" import type { WorkspaceAdaptor } from "../../src/control-plane/types" import { Workspace } from "../../src/control-plane/workspace" import { AppRuntime } from "../../src/effect/app-runtime" -import { Flag } from "../../src/flag/flag" +import { Flag } from "@opencode-ai/core/flag/flag" import { ModelID, ProviderID } from "../../src/provider/schema" import { Instance } from "../../src/project/instance" import { Session as SessionNs } from "../../src/session" diff --git a/packages/plugin/package.json b/packages/plugin/package.json index ee8210ec1441..88f7a65f62ea 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.14.22", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { @@ -22,8 +22,8 @@ "zod": "catalog:" }, "peerDependencies": { - "@opentui/core": ">=0.1.99", - "@opentui/solid": ">=0.1.99" + "@opentui/core": ">=0.1.103", + "@opentui/solid": ">=0.1.103" }, "peerDependenciesMeta": { "@opentui/core": { @@ -34,8 +34,8 @@ } }, "devDependencies": { - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", + "@opentui/core": "catalog:", + "@opentui/solid": "catalog:", "@tsconfig/node22": "catalog:", "@types/node": "catalog:", "typescript": "catalog:", diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 62a50b7434b7..3f6ffa7a5a68 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.14.22", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 40e661b46a2d..766b830995cc 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4,6 +4,20 @@ export type ClientOptions = { baseUrl: `${string}://${string}` | (string & {}) } +export type EventInstallationUpdated = { + type: "installation.updated" + properties: { + version: string + } +} + +export type EventInstallationUpdateAvailable = { + type: "installation.update-available" + properties: { + version: string + } +} + export type Project = { id: string worktree: string @@ -54,21 +68,6 @@ export type EventGlobalDisposed = { } } -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - export type EventLspClientDiagnostics = { type: "lsp.client.diagnostics" properties: { @@ -84,74 +83,61 @@ export type EventLspUpdated = { } } -export type EventInstallationUpdated = { - type: "installation.updated" - properties: { - version: string - } -} - -export type EventInstallationUpdateAvailable = { - type: "installation.update-available" +export type EventFileEdited = { + type: "file.edited" properties: { - version: string + file: string } } -export type EventMessagePartDelta = { - type: "message.part.delta" - properties: { - sessionID: string - messageID: string - partID: string - field: string - delta: string - } +export type OutputFormatText = { + type: "text" } -export type PermissionRequest = { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } +export type JsonSchema = { + [key: string]: unknown } -export type EventPermissionAsked = { - type: "permission.asked" - properties: PermissionRequest +export type OutputFormatJsonSchema = { + type: "json_schema" + schema: JsonSchema + retryCount?: number } -export type EventPermissionReplied = { - type: "permission.replied" - properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} +export type OutputFormat = OutputFormatText | OutputFormatJsonSchema -export type SnapshotFileDiff = { +export type FileDiff = { file: string - patch: string + before: string + after: string additions: number deletions: number status?: "added" | "deleted" | "modified" } -export type EventSessionDiff = { - type: "session.diff" - properties: { - sessionID: string - diff: Array +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + format?: OutputFormat + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string } + system?: string + tools?: { + [key: string]: boolean + } + variant?: string } export type ProviderAuthError = { @@ -215,475 +201,110 @@ export type ApiError = { } } -export type EventSessionError = { - type: "session.error" - properties: { - sessionID?: string - error?: - | ProviderAuthError - | UnknownError - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ApiError +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number } + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + structured?: unknown + variant?: string + finish?: string } -export type QuestionOption = { - /** - * Display text (1-5 words, concise) - */ - label: string - /** - * Explanation of choice - */ - description: string -} +export type Message = UserMessage | AssistantMessage -export type QuestionInfo = { - /** - * Complete question - */ - question: string - /** - * Very short label (max 30 chars) - */ - header: string - /** - * Available choices - */ - options: Array - /** - * Allow selecting multiple choices - */ - multiple?: boolean - /** - * Allow typing a custom answer (default: true) - */ - custom?: boolean +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message + } } -export type QuestionTool = { - messageID: string - callID: string +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string + } } -export type QuestionRequest = { +export type TextPart = { id: string sessionID: string - /** - * Questions to ask - */ - questions: Array - tool?: QuestionTool + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } } -export type EventQuestionAsked = { - type: "question.asked" - properties: QuestionRequest +export type SubtaskPart = { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + model?: { + providerID: string + modelID: string + } + command?: string } -export type QuestionAnswer = Array - -export type QuestionReplied = { +export type ReasoningPart = { + id: string sessionID: string - requestID: string - answers: Array -} - -export type EventQuestionReplied = { - type: "question.replied" - properties: QuestionReplied -} - -export type QuestionRejected = { - sessionID: string - requestID: string -} - -export type EventQuestionRejected = { - type: "question.rejected" - properties: QuestionRejected -} - -export type Todo = { - /** - * Brief description of the task - */ - content: string - /** - * Current status of the task: pending, in_progress, completed, cancelled - */ - status: string - /** - * Priority level of the task: high, medium, low - */ - priority: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } - -export type EventSessionStatus = { - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string - } -} - -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } -} - -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.line.up" - | "session.line.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} - -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type EventMcpToolsChanged = { - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventMcpBrowserOpenFailed = { - type: "mcp.browser.open.failed" - properties: { - mcpName: string - url: string - } -} - -export type EventCommandExecuted = { - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type EventWorktreeReady = { - type: "worktree.ready" - properties: { - name: string - branch: string - } -} - -export type EventWorktreeFailed = { - type: "worktree.failed" - properties: { - message: string - } -} - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type EventPtyCreated = { - type: "pty.created" - properties: { - info: Pty - } -} - -export type EventPtyUpdated = { - type: "pty.updated" - properties: { - info: Pty - } -} - -export type EventPtyExited = { - type: "pty.exited" - properties: { - id: string - exitCode: number - } -} - -export type EventPtyDeleted = { - type: "pty.deleted" - properties: { - id: string - } -} - -export type EventWorkspaceReady = { - type: "workspace.ready" - properties: { - name: string - } -} - -export type EventWorkspaceFailed = { - type: "workspace.failed" - properties: { - message: string - } -} - -export type EventWorkspaceRestore = { - type: "workspace.restore" - properties: { - workspaceID: string - sessionID: string - total: number - step: number - } -} - -export type EventWorkspaceStatus = { - type: "workspace.status" - properties: { - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - } -} - -export type OutputFormatText = { - type: "text" -} - -export type JsonSchema = { - [key: string]: unknown -} - -export type OutputFormatJsonSchema = { - type: "json_schema" - schema: JsonSchema - retryCount?: number -} - -export type OutputFormat = OutputFormatText | OutputFormatJsonSchema - -export type UserMessage = { - id: string - sessionID: string - role: "user" - time: { - created: number - } - format?: OutputFormat - summary?: { - title?: string - body?: string - diffs: Array - } - agent: string - model: { - providerID: string - modelID: string - variant?: string - } - system?: string - tools?: { - [key: string]: boolean - } -} - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: - | ProviderAuthError - | UnknownError - | MessageOutputLengthError - | MessageAbortedError - | StructuredOutputError - | ContextOverflowError - | ApiError - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - total?: number - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - structured?: unknown - variant?: string - finish?: string -} - -export type Message = UserMessage | AssistantMessage - -export type EventMessageUpdated = { - type: "message.updated" - properties: { - sessionID: string - info: Message - } -} - -export type EventMessageRemoved = { - type: "message.removed" - properties: { - sessionID: string - messageID: string - } -} - -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type SubtaskPart = { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - model?: { - providerID: string - modelID: string - } - command?: string -} - -export type ReasoningPart = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number - } + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } } export type FilePartSourceText = { @@ -808,115 +429,374 @@ export type ToolPart = { } } -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + total?: number + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } +} + +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError + time: { + created: number + } +} + +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean + overflow?: boolean +} + +export type Part = + | TextPart + | SubtaskPart + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type EventMessagePartUpdated = { + type: "message.part.updated" + properties: { + part: Part + } +} + +export type EventMessagePartDelta = { + type: "message.part.delta" + properties: { + sessionID: string + messageID: string + partID: string + field: string + delta: string + } +} + +export type EventMessagePartRemoved = { + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } +} + +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest +} + +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} + +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "reconnecting" + attempt: number + message: string + } + | { + type: "busy" + } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string + } +} + +export type QuestionOption = { + /** + * Display text (1-5 words, concise) + */ + label: string + /** + * Explanation of choice + */ + description: string +} + +export type QuestionInfo = { + /** + * Complete question + */ + question: string + /** + * Very short label (max 30 chars) + */ + header: string + /** + * Available choices + */ + options: Array + /** + * Allow selecting multiple choices + */ + multiple?: boolean + /** + * Allow typing a custom answer (default: true) + */ + custom?: boolean +} + +export type QuestionRequest = { + id: string + sessionID: string + /** + * Questions to ask + */ + questions: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventQuestionAsked = { + type: "question.asked" + properties: QuestionRequest +} + +export type QuestionAnswer = Array + +export type EventQuestionReplied = { + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionRejected = { + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } } -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string - cost: number - tokens: { - total?: number - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string } -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } } -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } } -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.line.up" + | "session.line.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string } } -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiError - time: { - created: number +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number } } -export type CompactionPart = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean - overflow?: boolean - tail_start_id?: string +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } } -export type Part = - | TextPart - | SubtaskPart - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } +} -export type EventMessagePartUpdated = { - type: "message.part.updated" +export type EventMcpBrowserOpenFailed = { + type: "mcp.browser.open.failed" properties: { - sessionID: string - part: Part - time: number + mcpName: string + url: string } } -export type EventMessagePartRemoved = { - type: "message.part.removed" +export type EventCommandExecuted = { + type: "command.executed" properties: { + name: string sessionID: string + arguments: string messageID: string - partID: string } } @@ -941,7 +821,7 @@ export type Session = { additions: number deletions: number files: number - diffs?: Array + diffs?: Array } share?: { url: string @@ -966,7 +846,6 @@ export type Session = { export type EventSessionCreated = { type: "session.created" properties: { - sessionID: string info: Session } } @@ -974,7 +853,6 @@ export type EventSessionCreated = { export type EventSessionUpdated = { type: "session.updated" properties: { - sessionID: string info: Session } } @@ -982,187 +860,158 @@ export type EventSessionUpdated = { export type EventSessionDeleted = { type: "session.deleted" properties: { - sessionID: string info: Session } } -export type SyncEventMessageUpdated = { - type: "sync" - name: "message.updated.1" - id: string - seq: number - aggregateID: "sessionID" - data: { +export type EventSessionDiff = { + type: "session.diff" + properties: { sessionID: string - info: Message + diff: Array } } -export type SyncEventMessageRemoved = { - type: "sync" - name: "message.removed.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - messageID: string +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: + | ProviderAuthError + | UnknownError + | MessageOutputLengthError + | MessageAbortedError + | StructuredOutputError + | ContextOverflowError + | ApiError } } -export type SyncEventMessagePartUpdated = { - type: "sync" - name: "message.part.updated.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - part: Part - time: number +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string } } -export type SyncEventMessagePartRemoved = { - type: "sync" - name: "message.part.removed.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - messageID: string - partID: string +export type EventWorkspaceReady = { + type: "workspace.ready" + properties: { + name: string + } +} + +export type EventWorkspaceFailed = { + type: "workspace.failed" + properties: { + message: string } } -export type SyncEventSessionCreated = { - type: "sync" - name: "session.created.1" +export type Pty = { id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - info: Session + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number +} + +export type EventPtyCreated = { + type: "pty.created" + properties: { + info: Pty + } +} + +export type EventPtyUpdated = { + type: "pty.updated" + properties: { + info: Pty + } +} + +export type EventPtyExited = { + type: "pty.exited" + properties: { + id: string + exitCode: number + } +} + +export type EventPtyDeleted = { + type: "pty.deleted" + properties: { + id: string } } -export type SyncEventSessionUpdated = { - type: "sync" - name: "session.updated.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - info: { - id?: string | null - slug?: string | null - projectID?: string | null - workspaceID?: string | null - directory?: string | null - parentID?: string | null - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } | null - share?: { - url?: string | null - } - title?: string | null - version?: string | null - time?: { - created?: number | null - updated?: number | null - compacting?: number | null - archived?: number | null - } - permission?: PermissionRuleset | null - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } | null - } +export type EventWorktreeReady = { + type: "worktree.ready" + properties: { + name: string + branch: string } } -export type SyncEventSessionDeleted = { - type: "sync" - name: "session.deleted.1" - id: string - seq: number - aggregateID: "sessionID" - data: { - sessionID: string - info: Session +export type EventWorktreeFailed = { + type: "worktree.failed" + properties: { + message: string } } +export type Event = + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventProjectUpdated + | EventServerInstanceDisposed + | EventServerConnected + | EventGlobalDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventFileEdited + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartDelta + | EventMessagePartRemoved + | EventPermissionAsked + | EventPermissionReplied + | EventSessionStatus + | EventSessionIdle + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventSessionCompacted + | EventFileWatcherUpdated + | EventTodoUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventMcpBrowserOpenFailed + | EventCommandExecuted + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionDiff + | EventSessionError + | EventVcsBranchUpdated + | EventWorkspaceReady + | EventWorkspaceFailed + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventWorktreeReady + | EventWorktreeFailed + export type GlobalEvent = { directory: string - project?: string - workspace?: string - payload: - | EventProjectUpdated - | EventServerInstanceDisposed - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated - | EventLspClientDiagnostics - | EventLspUpdated - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventVcsBranchUpdated - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | SyncEventMessageUpdated - | SyncEventMessageRemoved - | SyncEventMessagePartUpdated - | SyncEventMessagePartRemoved - | SyncEventSessionCreated - | SyncEventSessionUpdated - | SyncEventSessionDeleted + payload: Event } /** @@ -1205,8 +1054,8 @@ export type PermissionObjectConfig = { export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig export type PermissionConfig = - | PermissionActionConfig | { + __originalKeys?: Array read?: PermissionRuleConfig edit?: PermissionRuleConfig glob?: PermissionRuleConfig @@ -1216,6 +1065,7 @@ export type PermissionConfig = task?: PermissionRuleConfig external_directory?: PermissionRuleConfig todowrite?: PermissionActionConfig + todoread?: PermissionActionConfig question?: PermissionActionConfig webfetch?: PermissionActionConfig websearch?: PermissionActionConfig @@ -1223,8 +1073,9 @@ export type PermissionConfig = lsp?: PermissionRuleConfig doom_loop?: PermissionActionConfig skill?: PermissionRuleConfig - [key: string]: PermissionRuleConfig | PermissionActionConfig | undefined + [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined } + | PermissionActionConfig export type AgentConfig = { model?: string @@ -1300,29 +1151,6 @@ export type ProviderConfig = { env?: Array id?: string npm?: string - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - /** - * GitHub Enterprise URL for copilot authentication - */ - enterpriseUrl?: string - /** - * Enable promptCacheKey for this provider (default false) - */ - setCacheKey?: boolean - /** - * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. - */ - timeout?: number | false - /** - * Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted. - */ - chunkTimeout?: number - [key: string]: unknown | string | boolean | number | false | number | undefined - } models?: { [key: string]: { id?: string @@ -1361,16 +1189,16 @@ export type ProviderConfig = { } experimental?: boolean status?: "alpha" | "beta" | "deprecated" - provider?: { - npm?: string - api?: string - } options?: { [key: string]: unknown } headers?: { [key: string]: string } + provider?: { + npm?: string + api?: string + } /** * Variant-specific configuration */ @@ -1385,6 +1213,29 @@ export type ProviderConfig = { } } } + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + /** + * GitHub Enterprise URL for copilot authentication + */ + enterpriseUrl?: string + /** + * Enable promptCacheKey for this provider (default false) + */ + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + /** + * Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted. + */ + chunkTimeout?: number + [key: string]: unknown | string | boolean | number | false | number | undefined + } } export type McpLocalConfig = { @@ -1425,10 +1276,6 @@ export type McpOAuthConfig = { * OAuth scopes to request during authorization */ scope?: string - /** - * OAuth redirect URI (default: http://127.0.0.1:19876/mcp/oauth/callback). - */ - redirectUri?: string } export type McpRemoteConfig = { @@ -1500,19 +1347,11 @@ export type Config = { watcher?: { ignore?: Array } + plugin?: Array /** * Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true. */ snapshot?: boolean - plugin?: Array< - | string - | [ - string, - { - [key: string]: unknown - }, - ] - > /** * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing */ @@ -1588,7 +1427,7 @@ export type Config = { } } formatter?: - | boolean + | false | { [key: string]: { disabled?: boolean @@ -1600,7 +1439,7 @@ export type Config = { } } lsp?: - | boolean + | false | { [key: string]: | { @@ -1633,19 +1472,6 @@ export type Config = { */ url?: string } - /** - * Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned. - */ - tool_output?: { - /** - * Maximum lines of tool output before it is truncated and saved to disk (default: 2000) - */ - max_lines?: number - /** - * Maximum bytes of tool output before it is truncated and saved to disk (default: 51200) - */ - max_bytes?: number - } compaction?: { /** * Enable automatic compaction when context is full (default: true) @@ -1655,14 +1481,6 @@ export type Config = { * Enable pruning of old tool outputs (default: true) */ prune?: boolean - /** - * Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2) - */ - tail_turns?: number - /** - * Maximum number of tokens from recent turns to preserve verbatim after compaction - */ - preserve_recent_tokens?: number /** * Token buffer for compaction. Leaves enough window to avoid overflow during compaction. */ @@ -1713,9 +1531,6 @@ export type OAuth = { export type ApiAuth = { type: "api" key: string - metadata?: { - [key: string]: string - } } export type WellKnownAuth = { @@ -1726,16 +1541,6 @@ export type WellKnownAuth = { export type Auth = OAuth | ApiAuth | WellKnownAuth -export type Workspace = { - id: string - type: string - name: string - branch: string | null - directory: string | null - extra: unknown | null - projectID: string -} - export type NotFoundError = { name: "NotFoundError" data: { @@ -1828,12 +1633,6 @@ export type Provider = { } } -export type ConsoleState = { - consoleManagedProviders: Array - activeOrgName?: string - switchableOrgCount: number -} - export type ToolIds = Array export type ToolListItem = { @@ -1844,6 +1643,16 @@ export type ToolListItem = { export type ToolList = Array +export type Workspace = { + id: string + type: string + branch: string | null + name: string | null + directory: string | null + extra: unknown | null + projectID: string +} + export type Worktree = { name: string branch: string @@ -1883,7 +1692,7 @@ export type GlobalSession = { additions: number deletions: number files: number - diffs?: Array + diffs?: Array } share?: { url: string @@ -2001,100 +1810,51 @@ export type ProviderAuthAuthorization = { instructions: string } -export type Symbol = { - name: string - kind: number - location: { - uri: string - range: Range - } -} - -export type FileNode = { - name: string - path: string - absolute: string - type: "file" | "directory" - ignored: boolean -} - -export type FileContent = { - type: "text" | "binary" - content: string - diff?: string - patch?: { - oldFileName: string - newFileName: string - oldHeader?: string - newHeader?: string - hunks: Array<{ - oldStart: number - oldLines: number - newStart: number - newLines: number - lines: Array - }> - index?: string - } - encoding?: "base64" - mimeType?: string -} - -export type File = { - path: string - added: number - removed: number - status: "added" | "deleted" | "modified" -} - -export type Event = - | EventProjectUpdated - | EventServerInstanceDisposed - | EventServerConnected - | EventGlobalDisposed - | EventFileEdited - | EventFileWatcherUpdated - | EventLspClientDiagnostics - | EventLspUpdated - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventMessagePartDelta - | EventPermissionAsked - | EventPermissionReplied - | EventSessionDiff - | EventSessionError - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventTodoUpdated - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventMcpBrowserOpenFailed - | EventCommandExecuted - | EventVcsBranchUpdated - | EventWorktreeReady - | EventWorktreeFailed - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventWorkspaceReady - | EventWorkspaceFailed - | EventWorkspaceRestore - | EventWorkspaceStatus - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted +export type Symbol = { + name: string + kind: number + location: { + uri: string + range: Range + } +} + +export type FileNode = { + name: string + path: string + absolute: string + type: "file" | "directory" + ignored: boolean +} + +export type FileContent = { + type: "text" | "binary" + content: string + diff?: string + patch?: { + oldFileName: string + newFileName: string + oldHeader?: string + newHeader?: string + hunks: Array<{ + oldStart: number + oldLines: number + newStart: number + newLines: number + lines: Array + }> + index?: string + } + encoding?: "base64" + mimeType?: string +} + +export type File = { + path: string + added: number + removed: number + status: "added" | "deleted" | "modified" +} export type McpStatusConnected = { status: "connected" @@ -2134,16 +1894,7 @@ export type Path = { } export type VcsInfo = { - branch?: string - default_branch?: string -} - -export type VcsFileDiff = { - file: string - patch: string - additions: number - deletions: number - status?: "added" | "deleted" | "modified" + branch: string } export type Command = { @@ -2345,250 +2096,33 @@ export type AuthRemoveResponses = { } export type AuthRemoveResponse = AuthRemoveResponses[keyof AuthRemoveResponses] - -export type AuthSetData = { - body?: Auth - path: { - providerID: string - } - query?: never - url: "/auth/{providerID}" -} - -export type AuthSetErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] - -export type AuthSetResponses = { - /** - * Successfully set authentication credentials - */ - 200: boolean -} - -export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] - -export type AppLogData = { - body?: { - /** - * Service name for the log entry - */ - service: string - /** - * Log level - */ - level: "debug" | "info" | "error" | "warn" - /** - * Log message - */ - message: string - /** - * Additional metadata for the log entry - */ - extra?: { - [key: string]: unknown - } - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/log" -} - -export type AppLogErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type AppLogError = AppLogErrors[keyof AppLogErrors] - -export type AppLogResponses = { - /** - * Log entry written successfully - */ - 200: boolean -} - -export type AppLogResponse = AppLogResponses[keyof AppLogResponses] - -export type ExperimentalWorkspaceAdaptorListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/adaptor" -} - -export type ExperimentalWorkspaceAdaptorListResponses = { - /** - * Workspace adaptors - */ - 200: Array<{ - type: string - name: string - description: string - }> -} - -export type ExperimentalWorkspaceAdaptorListResponse = - ExperimentalWorkspaceAdaptorListResponses[keyof ExperimentalWorkspaceAdaptorListResponses] - -export type ExperimentalWorkspaceListData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace" -} - -export type ExperimentalWorkspaceListResponses = { - /** - * Workspaces - */ - 200: Array -} - -export type ExperimentalWorkspaceListResponse = - ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] - -export type ExperimentalWorkspaceCreateData = { - body?: { - id?: string - type: string - branch: string | null - extra: unknown | null - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace" -} - -export type ExperimentalWorkspaceCreateErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceCreateError = - ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] - -export type ExperimentalWorkspaceCreateResponses = { - /** - * Workspace created - */ - 200: Workspace -} - -export type ExperimentalWorkspaceCreateResponse = - ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] - -export type ExperimentalWorkspaceStatusData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/status" -} - -export type ExperimentalWorkspaceStatusResponses = { - /** - * Workspace status - */ - 200: Array<{ - workspaceID: string - status: "connected" | "connecting" | "disconnected" | "error" - }> -} - -export type ExperimentalWorkspaceStatusResponse = - ExperimentalWorkspaceStatusResponses[keyof ExperimentalWorkspaceStatusResponses] - -export type ExperimentalWorkspaceRemoveData = { - body?: never - path: { - id: string - } - query?: { - directory?: string - workspace?: string - } - url: "/experimental/workspace/{id}" -} - -export type ExperimentalWorkspaceRemoveErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type ExperimentalWorkspaceRemoveError = - ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] - -export type ExperimentalWorkspaceRemoveResponses = { - /** - * Workspace removed - */ - 200: Workspace -} - -export type ExperimentalWorkspaceRemoveResponse = - ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] - -export type ExperimentalWorkspaceSessionRestoreData = { - body?: { - sessionID: string - } - path: { - id: string - } - query?: { - directory?: string - workspace?: string + +export type AuthSetData = { + body?: Auth + path: { + providerID: string } - url: "/experimental/workspace/{id}/session-restore" + query?: never + url: "/auth/{providerID}" } -export type ExperimentalWorkspaceSessionRestoreErrors = { +export type AuthSetErrors = { /** * Bad request */ 400: BadRequestError } -export type ExperimentalWorkspaceSessionRestoreError = - ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors] +export type AuthSetError = AuthSetErrors[keyof AuthSetErrors] -export type ExperimentalWorkspaceSessionRestoreResponses = { +export type AuthSetResponses = { /** - * Session replay started + * Successfully set authentication credentials */ - 200: { - total: number - } + 200: boolean } -export type ExperimentalWorkspaceSessionRestoreResponse = - ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses] +export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses] export type ProjectListData = { body?: never @@ -2946,134 +2480,150 @@ export type ConfigProvidersResponses = { export type ConfigProvidersResponse = ConfigProvidersResponses[keyof ConfigProvidersResponses] -export type ExperimentalConsoleGetData = { +export type ToolIdsData = { body?: never path?: never query?: { directory?: string workspace?: string } - url: "/experimental/console" + url: "/experimental/tool/ids" +} + +export type ToolIdsErrors = { + /** + * Bad request + */ + 400: BadRequestError } -export type ExperimentalConsoleGetResponses = { +export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] + +export type ToolIdsResponses = { /** - * Active Console provider metadata + * Tool IDs */ - 200: ConsoleState + 200: ToolIds } -export type ExperimentalConsoleGetResponse = ExperimentalConsoleGetResponses[keyof ExperimentalConsoleGetResponses] +export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] -export type ExperimentalConsoleListOrgsData = { +export type ToolListData = { body?: never path?: never - query?: { + query: { directory?: string workspace?: string + provider: string + model: string } - url: "/experimental/console/orgs" + url: "/experimental/tool" } -export type ExperimentalConsoleListOrgsResponses = { +export type ToolListErrors = { /** - * Switchable Console orgs + * Bad request */ - 200: { - orgs: Array<{ - accountID: string - accountEmail: string - accountUrl: string - orgID: string - orgName: string - active: boolean - }> - } + 400: BadRequestError } -export type ExperimentalConsoleListOrgsResponse = - ExperimentalConsoleListOrgsResponses[keyof ExperimentalConsoleListOrgsResponses] +export type ToolListError = ToolListErrors[keyof ToolListErrors] -export type ExperimentalConsoleSwitchOrgData = { - body?: { - accountID: string - orgID: string - } +export type ToolListResponses = { + /** + * Tools + */ + 200: ToolList +} + +export type ToolListResponse = ToolListResponses[keyof ToolListResponses] + +export type ExperimentalWorkspaceListData = { + body?: never path?: never query?: { directory?: string workspace?: string } - url: "/experimental/console/switch" + url: "/experimental/workspace" } -export type ExperimentalConsoleSwitchOrgResponses = { +export type ExperimentalWorkspaceListResponses = { /** - * Switch success + * Workspaces */ - 200: boolean + 200: Array } -export type ExperimentalConsoleSwitchOrgResponse = - ExperimentalConsoleSwitchOrgResponses[keyof ExperimentalConsoleSwitchOrgResponses] +export type ExperimentalWorkspaceListResponse = + ExperimentalWorkspaceListResponses[keyof ExperimentalWorkspaceListResponses] -export type ToolIdsData = { - body?: never +export type ExperimentalWorkspaceCreateData = { + body?: { + id?: string + type: string + branch: string | null + extra: unknown | null + } path?: never query?: { directory?: string workspace?: string } - url: "/experimental/tool/ids" + url: "/experimental/workspace" } -export type ToolIdsErrors = { +export type ExperimentalWorkspaceCreateErrors = { /** * Bad request */ 400: BadRequestError } -export type ToolIdsError = ToolIdsErrors[keyof ToolIdsErrors] +export type ExperimentalWorkspaceCreateError = + ExperimentalWorkspaceCreateErrors[keyof ExperimentalWorkspaceCreateErrors] -export type ToolIdsResponses = { +export type ExperimentalWorkspaceCreateResponses = { /** - * Tool IDs + * Workspace created */ - 200: ToolIds + 200: Workspace } -export type ToolIdsResponse = ToolIdsResponses[keyof ToolIdsResponses] +export type ExperimentalWorkspaceCreateResponse = + ExperimentalWorkspaceCreateResponses[keyof ExperimentalWorkspaceCreateResponses] -export type ToolListData = { +export type ExperimentalWorkspaceRemoveData = { body?: never - path?: never - query: { + path: { + id: string + } + query?: { directory?: string workspace?: string - provider: string - model: string } - url: "/experimental/tool" + url: "/experimental/workspace/{id}" } -export type ToolListErrors = { +export type ExperimentalWorkspaceRemoveErrors = { /** * Bad request */ 400: BadRequestError } -export type ToolListError = ToolListErrors[keyof ToolListErrors] +export type ExperimentalWorkspaceRemoveError = + ExperimentalWorkspaceRemoveErrors[keyof ExperimentalWorkspaceRemoveErrors] -export type ToolListResponses = { +export type ExperimentalWorkspaceRemoveResponses = { /** - * Tools + * Workspace removed */ - 200: ToolList + 200: Workspace } -export type ToolListResponse = ToolListResponses[keyof ToolListResponses] +export type ExperimentalWorkspaceRemoveResponse = + ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses] export type WorktreeRemoveData = { body?: WorktreeRemoveInput @@ -3418,7 +2968,6 @@ export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses] export type SessionUpdateData = { body?: { title?: string - permission?: PermissionRuleset time?: { archived?: number } @@ -3703,7 +3252,7 @@ export type SessionDiffResponses = { /** * Successfully retrieved diff */ - 200: Array + 200: Array } export type SessionDiffResponse = SessionDiffResponses[keyof SessionDiffResponses] @@ -4094,7 +3643,6 @@ export type SessionCommandResponse = SessionCommandResponses[keyof SessionComman export type SessionShellData = { body?: { - messageID?: string agent: string model?: { providerID: string @@ -4129,10 +3677,7 @@ export type SessionShellResponses = { /** * Created message */ - 200: { - info: Message - parts: Array - } + 200: AssistantMessage } export type SessionShellResponse = SessionShellResponses[keyof SessionShellResponses] @@ -4408,7 +3953,68 @@ export type ProviderListResponses = { * List of providers */ 200: { - all: Array + all: Array<{ + api?: string + name: string + env: Array + id: string + npm?: string + models: { + [key: string]: { + id: string + name: string + family?: string + release_date: string + attachment: boolean + reasoning: boolean + temperature: boolean + tool_call: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm?: string + api?: string + } + variants?: { + [key: string]: { + [key: string]: unknown + } + } + } + } + }> default: { [key: string]: string } @@ -4525,104 +4131,6 @@ export type ProviderOauthCallbackResponses = { export type ProviderOauthCallbackResponse = ProviderOauthCallbackResponses[keyof ProviderOauthCallbackResponses] -export type SyncStartData = { - body?: never - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/sync/start" -} - -export type SyncStartResponses = { - /** - * Workspace sync started - */ - 200: boolean -} - -export type SyncStartResponse = SyncStartResponses[keyof SyncStartResponses] - -export type SyncReplayData = { - body?: { - directory: string - events: Array<{ - id: string - aggregateID: string - seq: number - type: string - data: { - [key: string]: unknown - } - }> - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/sync/replay" -} - -export type SyncReplayErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SyncReplayError = SyncReplayErrors[keyof SyncReplayErrors] - -export type SyncReplayResponses = { - /** - * Replayed sync events - */ - 200: { - sessionID: string - } -} - -export type SyncReplayResponse = SyncReplayResponses[keyof SyncReplayResponses] - -export type SyncHistoryListData = { - body?: { - [key: string]: number - } - path?: never - query?: { - directory?: string - workspace?: string - } - url: "/sync/history" -} - -export type SyncHistoryListErrors = { - /** - * Bad request - */ - 400: BadRequestError -} - -export type SyncHistoryListError = SyncHistoryListErrors[keyof SyncHistoryListErrors] - -export type SyncHistoryListResponses = { - /** - * Sync events - */ - 200: Array<{ - id: string - aggregate_id: string - seq: number - type: string - data: { - [key: string]: unknown - } - }> -} - -export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] - export type FindTextData = { body?: never path?: never @@ -5384,44 +4892,71 @@ export type VcsGetResponses = { export type VcsGetResponse = VcsGetResponses[keyof VcsGetResponses] -export type VcsDiffData = { +export type CommandListData = { body?: never path?: never - query: { + query?: { directory?: string workspace?: string - mode: "git" | "branch" } - url: "/vcs/diff" + url: "/command" } -export type VcsDiffResponses = { +export type CommandListResponses = { /** - * VCS diff + * List of commands */ - 200: Array + 200: Array } -export type VcsDiffResponse = VcsDiffResponses[keyof VcsDiffResponses] +export type CommandListResponse = CommandListResponses[keyof CommandListResponses] -export type CommandListData = { - body?: never +export type AppLogData = { + body?: { + /** + * Service name for the log entry + */ + service: string + /** + * Log level + */ + level: "debug" | "info" | "error" | "warn" + /** + * Log message + */ + message: string + /** + * Additional metadata for the log entry + */ + extra?: { + [key: string]: unknown + } + } path?: never query?: { directory?: string workspace?: string } - url: "/command" + url: "/log" } -export type CommandListResponses = { +export type AppLogErrors = { /** - * List of commands + * Bad request */ - 200: Array + 400: BadRequestError } -export type CommandListResponse = CommandListResponses[keyof CommandListResponses] +export type AppLogError = AppLogErrors[keyof AppLogErrors] + +export type AppLogResponses = { + /** + * Log entry written successfully + */ + 200: boolean +} + +export type AppLogResponse = AppLogResponses[keyof AppLogResponses] export type AppAgentsData = { body?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index cd7b381d838e..9fb2a3e6d729 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -11679,8 +11679,7 @@ "type": "boolean" } }, - "required": ["enabled"], - "additionalProperties": false + "required": ["enabled"] } ] } @@ -13345,9 +13344,7 @@ "additionalProperties": {} }, "steps": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 + "type": "number" } }, "required": ["name", "mode", "permission", "options"] diff --git a/packages/shared/src/global.ts b/packages/shared/src/global.ts deleted file mode 100644 index 538cc091b534..000000000000 --- a/packages/shared/src/global.ts +++ /dev/null @@ -1,42 +0,0 @@ -import path from "path" -import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir" -import os from "os" -import { Context, Effect, Layer } from "effect" - -export namespace Global { - export class Service extends Context.Service()("@opencode/Global") {} - - export interface Interface { - readonly home: string - readonly data: string - readonly cache: string - readonly config: string - readonly state: string - readonly bin: string - readonly log: string - } - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const app = "opencode" - const home = process.env.OPENCODE_TEST_HOME ?? os.homedir() - const data = path.join(xdgData!, app) - const cache = path.join(xdgCache!, app) - const cfg = path.join(xdgConfig!, app) - const state = path.join(xdgState!, app) - const bin = path.join(cache, "bin") - const log = path.join(data, "log") - - return Service.of({ - home, - data, - cache, - config: cfg, - state, - bin, - log, - }) - }), - ) -} diff --git a/packages/shared/src/types.d.ts b/packages/shared/src/types.d.ts deleted file mode 100644 index 60e1639adb24..000000000000 --- a/packages/shared/src/types.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -declare module "@npmcli/arborist" { - export interface ArboristOptions { - path: string - binLinks?: boolean - progress?: boolean - savePrefix?: string - ignoreScripts?: boolean - [key: string]: unknown - } - - export interface ArboristNode { - name: string - path: string - } - - export interface ArboristEdge { - to?: ArboristNode - } - - export interface ArboristTree { - edgesOut: Map - } - - export interface ReifyOptions { - add?: string[] - save?: boolean - saveType?: "prod" | "dev" | "optional" | "peer" - [key: string]: unknown - } - - export class Arborist { - constructor(options: ArboristOptions) - loadVirtual(): Promise - reify(options?: ReifyOptions): Promise - } -} - -declare var Bun: - | { - file(path: string): { - text(): Promise - json(): Promise - } - write(path: string, content: string | Uint8Array): Promise - } - | undefined diff --git a/packages/slack/package.json b/packages/slack/package.json index a3516932ab63..9ab39fad7dbc 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.14.22", + "version": "1.14.25", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/ui/package.json b/packages/ui/package.json index ba0fb0149497..da7a0f6732a4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.14.22", + "version": "1.14.25", "type": "module", "license": "MIT", "exports": { @@ -44,7 +44,7 @@ "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/sdk": "workspace:*", - "@opencode-ai/shared": "workspace:*", + "@opencode-ai/core": "workspace:*", "@pierre/diffs": "catalog:", "@shikijs/transformers": "3.9.2", "@solid-primitives/bounds": "0.1.3", diff --git a/packages/ui/src/components/file.tsx b/packages/ui/src/components/file.tsx index 633b23b70632..97d4d69f7891 100644 --- a/packages/ui/src/components/file.tsx +++ b/packages/ui/src/components/file.tsx @@ -1,4 +1,4 @@ -import { sampledChecksum } from "@opencode-ai/shared/util/encode" +import { sampledChecksum } from "@opencode-ai/core/util/encode" import { DEFAULT_VIRTUAL_FILE_METRICS, type DiffLineAnnotation, diff --git a/packages/ui/src/components/line-comment.tsx b/packages/ui/src/components/line-comment.tsx index e20da5a8d30f..e5a7af9cbda6 100644 --- a/packages/ui/src/components/line-comment.tsx +++ b/packages/ui/src/components/line-comment.tsx @@ -1,5 +1,5 @@ import { useFilteredList } from "@opencode-ai/ui/hooks" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { createSignal, For, onMount, Show, splitProps, type JSX } from "solid-js" import { Button } from "./button" import { FileIcon } from "./file-icon" diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 28653512e5fc..56e2d9d7094f 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -2,7 +2,7 @@ import { useMarked } from "../context/marked" import { useI18n } from "../context/i18n" import DOMPurify from "dompurify" import morphdom from "morphdom" -import { checksum } from "@opencode-ai/shared/util/encode" +import { checksum } from "@opencode-ai/core/util/encode" import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js" import { isServer } from "solid-js/web" import { stream } from "./markdown-stream" diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 9c0c90c00076..013272205085 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -45,8 +45,8 @@ import { Checkbox } from "./checkbox" import { DiffChanges } from "./diff-changes" import { Markdown } from "./markdown" import { ImagePreview } from "./image-preview" -import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/shared/util/path" -import { checksum } from "@opencode-ai/shared/util/encode" +import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { checksum } from "@opencode-ai/core/util/encode" import { Tooltip } from "./tooltip" import { IconButton } from "./icon-button" import { Spinner } from "./spinner" diff --git a/packages/ui/src/components/session-review.tsx b/packages/ui/src/components/session-review.tsx index 94bca6727d05..949402f4392a 100644 --- a/packages/ui/src/components/session-review.tsx +++ b/packages/ui/src/components/session-review.tsx @@ -11,8 +11,8 @@ import { Tooltip } from "./tooltip" import { ScrollView } from "./scroll-view" import { useFileComponent } from "../context/file" import { useI18n } from "../context/i18n" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" -import { checksum } from "@opencode-ai/shared/util/encode" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" +import { checksum } from "@opencode-ai/core/util/encode" import { createEffect, createMemo, For, Match, onCleanup, Show, Switch, untrack, type JSX } from "solid-js" import { createStore } from "solid-js/store" import { type FileContent, type SnapshotFileDiff, type VcsFileDiff } from "@opencode-ai/sdk/v2" diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 61123b180e26..b35f718ef047 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -8,8 +8,8 @@ import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" import { useFileComponent } from "../context/file" -import { Binary } from "@opencode-ai/shared/util/binary" -import { getDirectory, getFilename } from "@opencode-ai/shared/util/path" +import { Binary } from "@opencode-ai/core/util/binary" +import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" diff --git a/packages/web/package.json b/packages/web/package.json index 818cdf721eba..ce4a80436bad 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.14.22", + "version": "1.14.25", "scripts": { "dev": "astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", diff --git a/packages/web/src/content/docs/ar/go.mdx b/packages/web/src/content/docs/ar/go.mdx index 785ea35b6610..42ba87ffab9f 100644 --- a/packages/web/src/content/docs/ar/go.mdx +++ b/packages/web/src/content/docs/ar/go.mdx @@ -65,6 +65,8 @@ OpenCode Go حاليًا في المرحلة التجريبية. - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** قد تتغير قائمة النماذج مع استمرارنا في اختبار نماذج جديدة وإضافتها. @@ -82,25 +84,28 @@ OpenCode Go حاليًا في المرحلة التجريبية. يوضح الجدول أدناه عددًا تقديريًا للطلبات بناءً على أنماط استخدام Go المعتادة: -| Model | الطلبات لكل 5 ساعات | الطلبات في الأسبوع | الطلبات في الشهر | -| ------------- | ------------------- | ------------------ | ---------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | الطلبات لكل 5 ساعات | الطلبات في الأسبوع | الطلبات في الشهر | +| ----------------- | ------------------- | ------------------ | ---------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | تستند التقديرات إلى متوسطات أنماط الطلبات المرصودة: - GLM-5/5.1 — ‏700 input، و52,000 cached، و150 output tokens لكل طلب - Kimi K2.5/K2.6 — ‏870 input، و55,000 cached، و200 output tokens لكل طلب +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — ‏300 input، و55,000 cached، و125 output tokens لكل طلب - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -129,20 +134,22 @@ OpenCode Go حاليًا في المرحلة التجريبية. يمكنك أيضًا الوصول إلى نماذج Go عبر نقاط نهاية API التالية. -| Model | Model ID | Endpoint | AI SDK Package | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | Model ID | Endpoint | AI SDK Package | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | يستخدم [model id](/docs/config/#models) في إعدادات OpenCode لديك التنسيق `opencode-go/`. على سبيل المثال، بالنسبة إلى Kimi K2.6، ستستخدم `opencode-go/kimi-k2.6` في إعداداتك. diff --git a/packages/web/src/content/docs/ar/zen.mdx b/packages/web/src/content/docs/ar/zen.mdx index f85c15ea9659..9f7ed302ed59 100644 --- a/packages/web/src/content/docs/ar/zen.mdx +++ b/packages/web/src/content/docs/ar/zen.mdx @@ -59,6 +59,8 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن | النموذج | معرّف النموذج | نقطة النهاية | حزمة AI SDK | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -99,7 +101,7 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -يستخدم [معرّف النموذج](/docs/config/#models) في إعدادات OpenCode الصيغة `opencode/`. على سبيل المثال، بالنسبة إلى GPT 5.4، ستستخدم `opencode/gpt-5.4` في إعداداتك. +يستخدم [معرّف النموذج](/docs/config/#models) في إعدادات OpenCode الصيغة `opencode/`. على سبيل المثال، بالنسبة إلى GPT 5.5، ستستخدم `opencode/gpt-5.5` في إعداداتك. --- @@ -146,7 +148,11 @@ https://opencode.ai/zen/v1/models | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/bs/go.mdx b/packages/web/src/content/docs/bs/go.mdx index 523f1ef8ed8b..0d183ba28e3d 100644 --- a/packages/web/src/content/docs/bs/go.mdx +++ b/packages/web/src/content/docs/bs/go.mdx @@ -75,6 +75,8 @@ Trenutna lista modela uključuje: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** Lista modela se može mijenjati dok testiramo i dodajemo nove. @@ -92,25 +94,28 @@ Ograničenja su definisana u dolarskoj vrijednosti. To znači da vaš stvarni br Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca korištenja Go pretplate: -| Model | zahtjeva na 5 sati | zahtjeva sedmično | zahtjeva mjesečno | -| ------------- | ------------------ | ----------------- | ----------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | zahtjeva na 5 sati | zahtjeva sedmično | zahtjeva mjesečno | +| ----------------- | ------------------ | ----------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Procjene se zasnivaju na zapaženim prosječnim obrascima zahtjeva: - GLM-5/5.1 — 700 ulaznih (input), 52,000 keširanih, 150 izlaznih (output) tokena po zahtjevu - Kimi K2.5/K2.6 — 870 ulaznih, 55,000 keširanih, 200 izlaznih tokena po zahtjevu +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 ulaznih, 55,000 keširanih, 125 izlaznih tokena po zahtjevu - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -141,20 +146,22 @@ nakon što dostignete ograničenja upotrebe umjesto blokiranja zahtjeva. Također možete pristupiti Go modelima putem sljedećih API endpointa. -| Model | Model ID | Endpoint | AI SDK Paket | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | Model ID | Endpoint | AI SDK Paket | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | [Model id](/docs/config/#models) u vašoj OpenCode konfiguraciji koristi format `opencode-go/`. Na primjer, za Kimi K2.6, koristili biste diff --git a/packages/web/src/content/docs/bs/zen.mdx b/packages/web/src/content/docs/bs/zen.mdx index 026e145adc64..d10e4f87f64a 100644 --- a/packages/web/src/content/docs/bs/zen.mdx +++ b/packages/web/src/content/docs/bs/zen.mdx @@ -64,6 +64,8 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa. | Model | Model ID | Endpoint | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa. | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | [model id](/docs/config/#models) u vašoj OpenCode konfiguraciji koristi format -`opencode/`. Na primjer, za GPT 5.4 u konfiguraciji biste -koristili `opencode/gpt-5.4`. +`opencode/`. Na primjer, za GPT 5.5 u konfiguraciji biste +koristili `opencode/gpt-5.5`. --- @@ -153,7 +155,11 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/da/go.mdx b/packages/web/src/content/docs/da/go.mdx index 86a834b984db..fcdbaf5f9ba4 100644 --- a/packages/web/src/content/docs/da/go.mdx +++ b/packages/web/src/content/docs/da/go.mdx @@ -75,6 +75,8 @@ Den nuværende liste over modeller inkluderer: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** Listen over modeller kan ændre sig, efterhånden som vi tester og tilføjer nye. @@ -92,25 +94,28 @@ Grænserne er defineret i dollarværdi. Det betyder, at dit faktiske antal anmod Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-forbrugsmønstre: -| Model | anmodninger pr. 5 timer | anmodninger pr. uge | anmodninger pr. måned | -| ------------- | ----------------------- | ------------------- | --------------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | anmodninger pr. 5 timer | anmodninger pr. uge | anmodninger pr. måned | +| ----------------- | ----------------------- | ------------------- | --------------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Estimaterne er baseret på observerede gennemsnitlige anmodningsmønstre: - GLM-5/5.1 — 700 input, 52.000 cachelagrede, 150 output-tokens pr. anmodning - Kimi K2.5/K2.6 — 870 input, 55.000 cachelagrede, 200 output-tokens pr. anmodning +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 input, 55.000 cachelagrede, 125 output-tokens pr. anmodning - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -141,20 +146,22 @@ når du har nået dine forbrugsgrænser, i stedet for at blokere anmodninger. Du kan også få adgang til Go-modeller gennem følgende API-endpoints. -| Model | Model ID | Endpoint | AI SDK Package | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | Model ID | Endpoint | AI SDK Package | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | Dit [model id](/docs/config/#models) i din OpenCode config bruger formatet `opencode-go/`. For eksempel for Kimi K2.6, vil du diff --git a/packages/web/src/content/docs/da/zen.mdx b/packages/web/src/content/docs/da/zen.mdx index b5c9f469f3f4..44df1602ab39 100644 --- a/packages/web/src/content/docs/da/zen.mdx +++ b/packages/web/src/content/docs/da/zen.mdx @@ -64,6 +64,8 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints. | Model | Model ID | Endpoint | AI SDK-pakke | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints. | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | [model id](/docs/config/#models) i din OpenCode-konfiguration -bruger formatet `opencode/`. For eksempel ville du for GPT 5.4 -bruge `opencode/gpt-5.4` i din konfiguration. +bruger formatet `opencode/`. For eksempel ville du for GPT 5.5 +bruge `opencode/gpt-5.5` i din konfiguration. --- @@ -153,7 +155,11 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/de/go.mdx b/packages/web/src/content/docs/de/go.mdx index 49c0efda58b4..09341efa4bfb 100644 --- a/packages/web/src/content/docs/de/go.mdx +++ b/packages/web/src/content/docs/de/go.mdx @@ -67,6 +67,8 @@ Die aktuelle Liste der Modelle umfasst: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** Die Liste der Modelle kann sich ändern, während wir neue testen und hinzufügen. @@ -84,25 +86,28 @@ Limits sind in Dollarwerten definiert. Das bedeutet, dass die tatsächliche Anza Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf typischen Go-Nutzungsmustern: -| Model | Anfragen pro 5 Stunden | Anfragen pro Woche | Anfragen pro Monat | -| ------------- | ---------------------- | ------------------ | ------------------ | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | Anfragen pro 5 Stunden | Anfragen pro Woche | Anfragen pro Monat | +| ----------------- | ---------------------- | ------------------ | ------------------ | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Die Schätzungen basieren auf beobachteten durchschnittlichen Anfragemustern: - GLM-5/5.1 — 700 Input-, 52.000 Cached-, 150 Output-Tokens pro Anfrage - Kimi K2.5/K2.6 — 870 Input-, 55.000 Cached-, 200 Output-Tokens pro Anfrage +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 Input-, 55.000 Cached-, 125 Output-Tokens pro Anfrage - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -131,20 +136,22 @@ Wenn du auch Guthaben auf deinem Zen-Konto hast, kannst du in der Console die Op Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen. -| Modell | Modell-ID | Endpunkt | AI SDK Package | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Modell | Modell-ID | Endpunkt | AI SDK Package | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | Die [Modell-ID](/docs/config/#models) in deiner OpenCode Config verwendet das Format `opencode-go/`. Für Kimi K2.6 würdest du beispielsweise `opencode-go/kimi-k2.6` in deiner Config verwenden. diff --git a/packages/web/src/content/docs/de/zen.mdx b/packages/web/src/content/docs/de/zen.mdx index 2fd012dd1b7d..61a03e7c2493 100644 --- a/packages/web/src/content/docs/de/zen.mdx +++ b/packages/web/src/content/docs/de/zen.mdx @@ -55,6 +55,8 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. | Model | Model ID | Endpoint | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -95,7 +97,7 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -Die [Model-ID](/docs/config/#models) in deiner OpenCode-Konfiguration verwendet das Format `opencode/`. Für GPT 5.4 würdest du zum Beispiel `opencode/gpt-5.4` in deiner Konfiguration verwenden. +Die [Model-ID](/docs/config/#models) in deiner OpenCode-Konfiguration verwendet das Format `opencode/`. Für GPT 5.5 würdest du zum Beispiel `opencode/gpt-5.5` in deiner Konfiguration verwenden. --- @@ -142,7 +144,11 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/es/go.mdx b/packages/web/src/content/docs/es/go.mdx index a541171cafdd..fd1edcf9ce4d 100644 --- a/packages/web/src/content/docs/es/go.mdx +++ b/packages/web/src/content/docs/es/go.mdx @@ -75,6 +75,8 @@ La lista actual de modelos incluye: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** La lista de modelos puede cambiar a medida que probamos y agregamos otros nuevos. @@ -92,25 +94,28 @@ Los límites se definen en valor en dólares. Esto significa que tu cantidad rea La siguiente tabla proporciona una cantidad estimada de peticiones basada en los patrones típicos de uso de Go: -| Model | peticiones por 5 horas | peticiones por semana | peticiones por mes | -| ------------- | ---------------------- | --------------------- | ------------------ | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | peticiones por 5 horas | peticiones por semana | peticiones por mes | +| ----------------- | ---------------------- | --------------------- | ------------------ | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Las estimaciones se basan en los patrones de peticiones promedio observados: - GLM-5/5.1 — 700 tokens de entrada, 52,000 en caché, 150 tokens de salida por petición - Kimi K2.5/K2.6 — 870 tokens de entrada, 55,000 en caché, 200 tokens de salida por petición +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 tokens de entrada, 55,000 en caché, 125 tokens de salida por petición - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -141,20 +146,22 @@ después de que hayas alcanzado tus límites de uso en lugar de bloquear las pet También puedes acceder a los modelos de Go a través de los siguientes endpoints de la API. -| Modelo | ID del modelo | Endpoint | Paquete de AI SDK | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Modelo | ID del modelo | Endpoint | Paquete de AI SDK | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | El [ID del modelo](/docs/config/#models) en tu configuración de OpenCode usa el formato `opencode-go/`. Por ejemplo, para Kimi K2.6, usarías diff --git a/packages/web/src/content/docs/es/zen.mdx b/packages/web/src/content/docs/es/zen.mdx index 82ed07a23b6b..f0df78118c0a 100644 --- a/packages/web/src/content/docs/es/zen.mdx +++ b/packages/web/src/content/docs/es/zen.mdx @@ -64,6 +64,8 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints | Modelo | Model ID | Endpoint | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | El [identificador del modelo](/docs/config/#models) en tu configuración de OpenCode -usa el formato `opencode/`. Por ejemplo, para GPT 5.4, usarías -`opencode/gpt-5.4` en tu configuración. +usa el formato `opencode/`. Por ejemplo, para GPT 5.5, usarías +`opencode/gpt-5.5` en tu configuración. --- @@ -153,7 +155,11 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/fr/go.mdx b/packages/web/src/content/docs/fr/go.mdx index 5f55128ed499..80cd5d2fc293 100644 --- a/packages/web/src/content/docs/fr/go.mdx +++ b/packages/web/src/content/docs/fr/go.mdx @@ -65,6 +65,8 @@ La liste actuelle des modèles comprend : - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** La liste des modèles peut changer au fur et à mesure que nous en testons et en ajoutons de nouveaux. @@ -82,25 +84,28 @@ Les limites sont définies en valeur monétaire (dollars). Cela signifie que vot Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur des modèles d'utilisation typiques de Go : -| Model | requêtes par 5 heures | requêtes par semaine | requêtes par mois | -| ------------- | --------------------- | -------------------- | ----------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | requêtes par 5 heures | requêtes par semaine | requêtes par mois | +| ----------------- | --------------------- | -------------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Les estimations sont basées sur les modèles de requêtes moyens observés : - GLM-5/5.1 — 700 tokens en entrée, 52,000 en cache, 150 tokens en sortie par requête - Kimi K2.5/K2.6 — 870 tokens en entrée, 55,000 en cache, 200 tokens en sortie par requête +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 tokens en entrée, 55,000 en cache, 125 tokens en sortie par requête - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -129,20 +134,22 @@ Si vous avez également des crédits sur votre solde Zen, vous pouvez activer l' Vous pouvez également accéder aux modèles Go via les points de terminaison d'API suivants. -| Modèle | ID de modèle | Point de terminaison | Package AI SDK | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Modèle | ID de modèle | Point de terminaison | Package AI SDK | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | L'[ID de modèle](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode-go/`. Par exemple, pour Kimi K2.6, vous utiliseriez `opencode-go/kimi-k2.6` dans votre configuration. diff --git a/packages/web/src/content/docs/fr/zen.mdx b/packages/web/src/content/docs/fr/zen.mdx index 0b2bcf94aa9f..09fa4699a84b 100644 --- a/packages/web/src/content/docs/fr/zen.mdx +++ b/packages/web/src/content/docs/fr/zen.mdx @@ -55,6 +55,8 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP | Modèle | ID du modèle | Point de terminaison | Package AI SDK | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -95,7 +97,7 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -Le [model id](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode/`. Par exemple, pour GPT 5.4, vous utiliseriez `opencode/gpt-5.4` dans votre configuration. +Le [model id](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode/`. Par exemple, pour GPT 5.5, vous utiliseriez `opencode/gpt-5.5` dans votre configuration. --- @@ -142,7 +144,11 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/go.mdx b/packages/web/src/content/docs/go.mdx index 946c70de30f3..9db12a644c2e 100644 --- a/packages/web/src/content/docs/go.mdx +++ b/packages/web/src/content/docs/go.mdx @@ -75,6 +75,8 @@ The current list of models includes: - **MiniMax M2.7** - **Qwen3.5 Plus** - **Qwen3.6 Plus** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** The list of models may change as we test and add new ones. @@ -92,25 +94,28 @@ Limits are defined in dollar value. This means your actual request count depends The table below provides an estimated request count based on typical Go usage patterns: -| Model | requests per 5 hour | requests per week | requests per month | -| ------------- | ------------------- | ----------------- | ------------------ | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | requests per 5 hour | requests per week | requests per month | +| ----------------- | ------------------- | ----------------- | ------------------ | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Estimates are based on observed average request patterns: - GLM-5/5.1 — 700 input, 52,000 cached, 150 output tokens per request - Kimi K2.5/K2.6 — 870 input, 55,000 cached, 200 output tokens per request +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 input, 55,000 cached, 125 output tokens per request - MiMo-V2-Pro — 350 input, 41,000 cached, 250 output tokens per request - MiMo-V2-Omni — 1000 input, 60,000 cached, 140 output tokens per request @@ -141,20 +146,22 @@ after you've reached your usage limits instead of blocking requests. You can also access Go models through the following API endpoints. -| Model | Model ID | Endpoint | AI SDK Package | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | Model ID | Endpoint | AI SDK Package | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | The [model id](/docs/config/#models) in your OpenCode config uses the format `opencode-go/`. For example, for Kimi K2.6, you would diff --git a/packages/web/src/content/docs/it/go.mdx b/packages/web/src/content/docs/it/go.mdx index 341a22c4cb27..775e567f8e4f 100644 --- a/packages/web/src/content/docs/it/go.mdx +++ b/packages/web/src/content/docs/it/go.mdx @@ -73,6 +73,8 @@ L'elenco attuale dei modelli include: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** L'elenco dei modelli potrebbe cambiare man mano che ne testiamo e aggiungiamo di nuovi. @@ -90,25 +92,28 @@ I limiti sono definiti in valore in dollari. Questo significa che il conteggio e La tabella seguente fornisce una stima del conteggio delle richieste in base a pattern di utilizzo tipici di Go: -| Model | richieste ogni 5 ore | richieste a settimana | richieste al mese | -| ------------- | -------------------- | --------------------- | ----------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | richieste ogni 5 ore | richieste a settimana | richieste al mese | +| ----------------- | -------------------- | --------------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Le stime si basano sui pattern medi di richieste osservati: - GLM-5/5.1 — 700 di input, 52.000 in cache, 150 token di output per richiesta - Kimi K2.5/K2.6 — 870 di input, 55.000 in cache, 200 token di output per richiesta +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 di input, 55.000 in cache, 125 token di output per richiesta - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -139,20 +144,22 @@ dopo che avrai raggiunto i limiti di utilizzo invece di bloccare le richieste. Puoi anche accedere ai modelli Go tramite i seguenti endpoint API. -| Modello | ID Modello | Endpoint | Pacchetto AI SDK | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Modello | ID Modello | Endpoint | Pacchetto AI SDK | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | Il [model id](/docs/config/#models) nella tua OpenCode config utilizza il formato `opencode-go/`. Ad esempio, per Kimi K2.6, useresti diff --git a/packages/web/src/content/docs/it/zen.mdx b/packages/web/src/content/docs/it/zen.mdx index 14ddb891d36e..36c4fd3e8411 100644 --- a/packages/web/src/content/docs/it/zen.mdx +++ b/packages/web/src/content/docs/it/zen.mdx @@ -64,6 +64,8 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. | Modello | Model ID | Endpoint | Pacchetto AI SDK | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | Il [model id](/docs/config/#models) nella config di OpenCode -usa il formato `opencode/`. Per esempio, per GPT 5.4, useresti -`opencode/gpt-5.4` nella tua config. +usa il formato `opencode/`. Per esempio, per GPT 5.5, useresti +`opencode/gpt-5.5` nella tua config. --- @@ -153,7 +155,11 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/ja/go.mdx b/packages/web/src/content/docs/ja/go.mdx index ddd5a66803a4..6ab9c3235c08 100644 --- a/packages/web/src/content/docs/ja/go.mdx +++ b/packages/web/src/content/docs/ja/go.mdx @@ -65,6 +65,8 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** 新しいモデルをテストして追加するにつれて、モデルのリストは変更される場合があります。 @@ -82,25 +84,28 @@ OpenCode Goには以下の制限が含まれています: 以下の表は、一般的なGoの利用パターンに基づいた推定リクエスト数を示しています: -| Model | 5時間あたりのリクエスト数 | 週間リクエスト数 | 月間リクエスト数 | -| ------------- | ------------------------- | ---------------- | ---------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | 5時間あたりのリクエスト数 | 週間リクエスト数 | 月間リクエスト数 | +| ----------------- | ------------------------- | ---------------- | ---------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | 推定値は、観測された平均的なリクエストパターンに基づいています: - GLM-5/5.1 — リクエストあたり 入力 700トークン、キャッシュ 52,000トークン、出力 150トークン - Kimi K2.5/K2.6 — リクエストあたり 入力 870トークン、キャッシュ 55,000トークン、出力 200トークン +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — リクエストあたり 入力 300トークン、キャッシュ 55,000トークン、出力 125トークン - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -129,20 +134,22 @@ Zen残高にクレジットがある場合は、コンソールで**Use balance* 以下のAPIエンドポイントを通じて、Goモデルにアクセスすることもできます。 -| Model | Model ID | Endpoint | AI SDK Package | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | Model ID | Endpoint | AI SDK Package | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | OpenCode設定の[model id](/docs/config/#models)は、`opencode-go/`という形式を使用します。たとえば、Kimi K2.6の場合は、設定で`opencode-go/kimi-k2.6`を使用します。 diff --git a/packages/web/src/content/docs/ja/zen.mdx b/packages/web/src/content/docs/ja/zen.mdx index ee84dc917233..504f1fd7867b 100644 --- a/packages/web/src/content/docs/ja/zen.mdx +++ b/packages/web/src/content/docs/ja/zen.mdx @@ -55,6 +55,8 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 | Model | Model ID | Endpoint | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -95,7 +97,7 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -OpenCode 設定で使う [model id](/docs/config/#models) は `opencode/` 形式です。たとえば、GPT 5.4 では設定に `opencode/gpt-5.4` を使用します。 +OpenCode 設定で使う [model id](/docs/config/#models) は `opencode/` 形式です。たとえば、GPT 5.5 では設定に `opencode/gpt-5.5` を使用します。 --- @@ -142,7 +144,11 @@ https://opencode.ai/zen/v1/models | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/ko/go.mdx b/packages/web/src/content/docs/ko/go.mdx index da787040fb5b..83c7c8e6a667 100644 --- a/packages/web/src/content/docs/ko/go.mdx +++ b/packages/web/src/content/docs/ko/go.mdx @@ -65,6 +65,8 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다. - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** 새로운 모델을 테스트하고 추가함에 따라 이 목록은 변경될 수 있습니다. @@ -82,25 +84,28 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. 아래 표는 일반적인 Go 사용 패턴을 기준으로 한 예상 요청 횟수를 보여줍니다. -| Model | 5시간당 요청 횟수 | 주간 요청 횟수 | 월간 요청 횟수 | -| ------------- | ----------------- | -------------- | -------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | 5시간당 요청 횟수 | 주간 요청 횟수 | 월간 요청 횟수 | +| ----------------- | ----------------- | -------------- | -------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | 예상치는 관찰된 평균 요청 패턴을 기준으로 합니다. - GLM-5/5.1 — 요청당 입력 700, 캐시 52,000, 출력 토큰 150 - Kimi K2.5/K2.6 — 요청당 입력 870, 캐시 55,000, 출력 토큰 200 +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 요청당 입력 300, 캐시 55,000, 출력 토큰 125 - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -129,20 +134,22 @@ Zen 잔액에 크레딧도 있다면, console에서 **Use balance** 옵션을 다음 API 엔드포인트를 통해서도 Go 모델에 액세스할 수 있습니다. -| 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | OpenCode config의 [model id](/docs/config/#models)는 `opencode-go/` 형식을 사용합니다. 예를 들어 Kimi K2.6의 경우 config에서 `opencode-go/kimi-k2.6`를 사용하면 됩니다. diff --git a/packages/web/src/content/docs/ko/zen.mdx b/packages/web/src/content/docs/ko/zen.mdx index 416ec9f20b4c..222f494bcf2e 100644 --- a/packages/web/src/content/docs/ko/zen.mdx +++ b/packages/web/src/content/docs/ko/zen.mdx @@ -55,6 +55,8 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. | 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -95,7 +97,7 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -OpenCode config에서 사용하는 [모델 ID](/docs/config/#models)는 `opencode/` 형식입니다. 예를 들어 GPT 5.4를 사용하려면 config에서 `opencode/gpt-5.4`를 사용하면 됩니다. +OpenCode config에서 사용하는 [모델 ID](/docs/config/#models)는 `opencode/` 형식입니다. 예를 들어 GPT 5.5를 사용하려면 config에서 `opencode/gpt-5.5`를 사용하면 됩니다. --- @@ -142,7 +144,11 @@ https://opencode.ai/zen/v1/models | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/lsp.mdx b/packages/web/src/content/docs/lsp.mdx index f242f4c5e4d3..ad6a4644df7a 100644 --- a/packages/web/src/content/docs/lsp.mdx +++ b/packages/web/src/content/docs/lsp.mdx @@ -16,7 +16,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | astro | .astro | Auto-installs for Astro projects | | bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server | | clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects | -| csharp | .cs | `.NET SDK` installed | +| csharp | .cs, .csx | `.NET SDK` installed | | clojure-lsp | .clj, .cljs, .cljc, .edn | `clojure-lsp` command available | | dart | .dart | `dart` command available | | deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) | @@ -36,6 +36,7 @@ OpenCode comes with several built-in LSP servers for popular languages: | php intelephense | .php | Auto-installs for PHP projects | | prisma | .prisma | `prisma` command available | | pyright | .py, .pyi | `pyright` dependency installed | +| razor | .razor, .cshtml | `.NET SDK` and VS Code C# extension installed | | ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available | | rust | .rs | `rust-analyzer` command available | | sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) | diff --git a/packages/web/src/content/docs/nb/go.mdx b/packages/web/src/content/docs/nb/go.mdx index 95c05417cf12..53ad74f3765e 100644 --- a/packages/web/src/content/docs/nb/go.mdx +++ b/packages/web/src/content/docs/nb/go.mdx @@ -75,6 +75,8 @@ Den nåværende listen over modeller inkluderer: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** Listen over modeller kan endres etter hvert som vi tester og legger til nye. @@ -92,25 +94,28 @@ Grensene er definert i dollarverdi. Dette betyr at ditt faktiske antall forespø Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksmønstre for Go: -| Model | forespørsler per 5 timer | forespørsler per uke | forespørsler per måned | -| ------------- | ------------------------ | -------------------- | ---------------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | forespørsler per 5 timer | forespørsler per uke | forespørsler per måned | +| ----------------- | ------------------------ | -------------------- | ---------------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Estimatene er basert på observerte gjennomsnittlige forespørselsmønstre: - GLM-5/5.1 — 700 input, 52 000 bufret, 150 output-tokens per forespørsel - Kimi K2.5/K2.6 — 870 input, 55 000 bufret, 200 output-tokens per forespørsel +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 input, 55 000 bufret, 125 output-tokens per forespørsel - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -141,20 +146,22 @@ etter at du har nådd bruksgrensene dine, i stedet for å blokkere forespørsler Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter. -| Modell | Modell-ID | Endepunkt | AI SDK Package | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Modell | Modell-ID | Endepunkt | AI SDK Package | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | [Modell-ID-en](/docs/config/#models) i din OpenCode-konfigurasjon bruker formatet `opencode-go/`. For eksempel, for Kimi K2.6, vil du diff --git a/packages/web/src/content/docs/nb/zen.mdx b/packages/web/src/content/docs/nb/zen.mdx index 25689ef8c428..c86c282e7d71 100644 --- a/packages/web/src/content/docs/nb/zen.mdx +++ b/packages/web/src/content/docs/nb/zen.mdx @@ -64,6 +64,8 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. | Modell | Modell-ID | Endepunkt | AI SDK-pakke | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | [modell-id](/docs/config/#models) i OpenCode-konfigurasjonen din -bruker formatet `opencode/`. For eksempel, for GPT 5.4, ville du -brukt `opencode/gpt-5.4` i konfigurasjonen din. +bruker formatet `opencode/`. For eksempel, for GPT 5.5, ville du +brukt `opencode/gpt-5.5` i konfigurasjonen din. --- @@ -153,7 +155,11 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/pl/go.mdx b/packages/web/src/content/docs/pl/go.mdx index 9ae3ea34b8bf..87fcf1cc3526 100644 --- a/packages/web/src/content/docs/pl/go.mdx +++ b/packages/web/src/content/docs/pl/go.mdx @@ -69,6 +69,8 @@ Obecna lista modeli obejmuje: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** Lista modeli może ulec zmianie w miarę testowania i dodawania nowych. @@ -86,25 +88,28 @@ Limity są zdefiniowane w wartości w dolarach. Oznacza to, że rzeczywista licz Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych wzorców korzystania z Go: -| Model | żądania na 5 godzin | żądania na tydzień | żądania na miesiąc | -| ------------- | ------------------- | ------------------ | ------------------ | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | żądania na 5 godzin | żądania na tydzień | żądania na miesiąc | +| ----------------- | ------------------- | ------------------ | ------------------ | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Szacunki opierają się na zaobserwowanych średnich wzorcach żądań: - GLM-5/5.1 — 700 tokenów wejściowych, 52 000 w pamięci podręcznej, 150 tokenów wyjściowych na żądanie - Kimi K2.5/K2.6 — 870 tokenów wejściowych, 55 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 tokenów wejściowych, 55 000 w pamięci podręcznej, 125 tokenów wyjściowych na żądanie - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -133,20 +138,22 @@ Jeśli masz również środki na swoim saldzie Zen, możesz włączyć opcję ** Możesz również uzyskać dostęp do modeli Go za pośrednictwem następujących punktów końcowych API. -| Model | ID modelu | Punkt końcowy | Pakiet AI SDK | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | ID modelu | Punkt końcowy | Pakiet AI SDK | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | [ID modelu](/docs/config/#models) w Twojej konfiguracji OpenCode używa formatu `opencode-go/`. Na przykład dla Kimi K2.6 należy użyć diff --git a/packages/web/src/content/docs/pl/zen.mdx b/packages/web/src/content/docs/pl/zen.mdx index 023f22da293e..078888965661 100644 --- a/packages/web/src/content/docs/pl/zen.mdx +++ b/packages/web/src/content/docs/pl/zen.mdx @@ -64,6 +64,8 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. | Model | ID modelu | Endpoint | Pakiet AI SDK | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | [ID modelu](/docs/config/#models) w Twojej konfiguracji OpenCode używa formatu -`opencode/`. Na przykład dla GPT 5.4 użyjesz w konfiguracji -`opencode/gpt-5.4`. +`opencode/`. Na przykład dla GPT 5.5 użyjesz w konfiguracji +`opencode/gpt-5.5`. --- @@ -153,7 +155,11 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/pt-br/go.mdx b/packages/web/src/content/docs/pt-br/go.mdx index 7d4d90ed51b2..4a220893391b 100644 --- a/packages/web/src/content/docs/pt-br/go.mdx +++ b/packages/web/src/content/docs/pt-br/go.mdx @@ -75,6 +75,8 @@ A lista atual de modelos inclui: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** A lista de modelos pode mudar conforme testamos e adicionamos novos. @@ -92,25 +94,28 @@ Os limites são definidos em valor em dólares. Isso significa que a sua contage A tabela abaixo fornece uma contagem estimada de requisições com base nos padrões típicos de uso do Go: -| Model | requisições por 5 horas | requisições por semana | requisições por mês | -| ------------- | ----------------------- | ---------------------- | ------------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | requisições por 5 horas | requisições por semana | requisições por mês | +| ----------------- | ----------------------- | ---------------------- | ------------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | As estimativas baseiam-se nos padrões médios de requisições observados: - GLM-5/5.1 — 700 tokens de entrada, 52.000 em cache, 150 tokens de saída por requisição - Kimi K2.5/K2.6 — 870 tokens de entrada, 55.000 em cache, 200 tokens de saída por requisição +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 tokens de entrada, 55.000 em cache, 125 tokens de saída por requisição - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -141,20 +146,22 @@ após você atingir os seus limites de uso em vez de bloquear as requisições. Você também pode acessar os modelos do Go através dos seguintes endpoints de API. -| Modelo | ID do Modelo | Endpoint | Pacote do AI SDK | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Modelo | ID do Modelo | Endpoint | Pacote do AI SDK | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | O [ID do modelo](/docs/config/#models) na sua configuração do OpenCode usa o formato `opencode-go/`. Por exemplo, para o Kimi K2.6, você usaria diff --git a/packages/web/src/content/docs/pt-br/zen.mdx b/packages/web/src/content/docs/pt-br/zen.mdx index 05fabd40ce2f..a72a9a90bfb8 100644 --- a/packages/web/src/content/docs/pt-br/zen.mdx +++ b/packages/web/src/content/docs/pt-br/zen.mdx @@ -55,6 +55,8 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API. | Modelo | ID do modelo | Endpoint | Pacote AI SDK | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -95,7 +97,7 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API. | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -O [model id](/docs/config/#models) na sua configuração do OpenCode usa o formato `opencode/`. Por exemplo, para GPT 5.4, você usaria `opencode/gpt-5.4` na sua configuração. +O [model id](/docs/config/#models) na sua configuração do OpenCode usa o formato `opencode/`. Por exemplo, para GPT 5.5, você usaria `opencode/gpt-5.5` na sua configuração. --- @@ -142,7 +144,11 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/ru/go.mdx b/packages/web/src/content/docs/ru/go.mdx index a8d33f296ddf..5a07418a7326 100644 --- a/packages/web/src/content/docs/ru/go.mdx +++ b/packages/web/src/content/docs/ru/go.mdx @@ -75,6 +75,8 @@ OpenCode Go работает так же, как и любой другой пр - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** Список моделей может меняться по мере того, как мы тестируем и добавляем новые. @@ -92,25 +94,28 @@ OpenCode Go включает следующие лимиты: В таблице ниже приведено примерное количество запросов на основе типичных сценариев использования Go: -| Model | запросов за 5 часов | запросов в неделю | запросов в месяц | -| ------------- | ------------------- | ----------------- | ---------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | запросов за 5 часов | запросов в неделю | запросов в месяц | +| ----------------- | ------------------- | ----------------- | ---------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Оценки основаны на наблюдаемых средних показателях запросов: - GLM-5/5.1 — 700 входных, 52,000 кешированных, 150 выходных токенов на запрос - Kimi K2.5/K2.6 — 870 входных, 55,000 кешированных, 200 выходных токенов на запрос +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 входных, 55,000 кешированных, 125 выходных токенов на запрос - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -141,20 +146,22 @@ OpenCode Go включает следующие лимиты: Вы также можете получить доступ к моделям Go через следующие API-эндпоинты. -| Модель | ID модели | Эндпоинт | Пакет AI SDK | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Модель | ID модели | Эндпоинт | Пакет AI SDK | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | [ID модели](/docs/config/#models) в вашем конфиге OpenCode использует формат `opencode-go/`. Например, для Kimi K2.6 вам нужно diff --git a/packages/web/src/content/docs/ru/zen.mdx b/packages/web/src/content/docs/ru/zen.mdx index 13184ecf779f..84fedeb107d0 100644 --- a/packages/web/src/content/docs/ru/zen.mdx +++ b/packages/web/src/content/docs/ru/zen.mdx @@ -64,6 +64,8 @@ OpenCode Zen работает как любой другой провайдер | Модель | Идентификатор модели | Конечная точка | Пакет AI SDK | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ OpenCode Zen работает как любой другой провайдер | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | [идентификатор модели](/docs/config/#models) в вашей конфигурации OpenCode -использует формат `opencode/`. Например, для GPT 5.4 вам нужно -использовать `opencode/gpt-5.4` в своей конфигурации. +использует формат `opencode/`. Например, для GPT 5.5 вам нужно +использовать `opencode/gpt-5.5` в своей конфигурации. --- @@ -153,7 +155,11 @@ https://opencode.ai/zen/v1/models | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/th/go.mdx b/packages/web/src/content/docs/th/go.mdx index fb0262c9582a..17cd9feb84a3 100644 --- a/packages/web/src/content/docs/th/go.mdx +++ b/packages/web/src/content/docs/th/go.mdx @@ -65,6 +65,8 @@ OpenCode Go ทำงานเหมือนกับผู้ให้บร - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** รายชื่อโมเดลอาจมีการเปลี่ยนแปลงเมื่อเราทำการทดสอบและเพิ่มโมเดลใหม่ๆ @@ -82,25 +84,28 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: ตารางด้านล่างแสดงจำนวน request โดยประมาณตามรูปแบบการใช้งานปกติของ Go: -| Model | requests ต่อ 5 ชั่วโมง | requests ต่อสัปดาห์ | requests ต่อเดือน | -| ------------- | ---------------------- | ------------------- | ----------------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | requests ต่อ 5 ชั่วโมง | requests ต่อสัปดาห์ | requests ต่อเดือน | +| ----------------- | ---------------------- | ------------------- | ----------------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | การประมาณการอ้างอิงจากรูปแบบการใช้งาน request โดยเฉลี่ยที่สังเกตพบ: - GLM-5/5.1 — 700 input, 52,000 cached, 150 output tokens ต่อ request - Kimi K2.5/K2.6 — 870 input, 55,000 cached, 200 output tokens ต่อ request +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 300 input, 55,000 cached, 125 output tokens ต่อ request - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -129,20 +134,22 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: คุณสามารถเข้าถึงโมเดลของ Go ผ่าน API endpoints ต่อไปนี้ได้เช่นกัน -| Model | Model ID | Endpoint | AI SDK Package | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | Model ID | Endpoint | AI SDK Package | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | [model id](/docs/config/#models) ใน OpenCode config ของคุณจะใช้รูปแบบ `opencode-go/` ตัวอย่างเช่น สำหรับ Kimi K2.6 คุณจะใช้ `opencode-go/kimi-k2.6` ใน config ของคุณ diff --git a/packages/web/src/content/docs/th/zen.mdx b/packages/web/src/content/docs/th/zen.mdx index b75e6d5fd66c..efb896601c85 100644 --- a/packages/web/src/content/docs/th/zen.mdx +++ b/packages/web/src/content/docs/th/zen.mdx @@ -57,6 +57,8 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน | Model | Model ID | Endpoint | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -97,7 +99,7 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -[model id](/docs/config/#models) ใน OpenCode config ของคุณใช้รูปแบบ `opencode/` ตัวอย่างเช่น สำหรับ GPT 5.4 คุณจะใช้ `opencode/gpt-5.4` ใน config ของคุณ +[model id](/docs/config/#models) ใน OpenCode config ของคุณใช้รูปแบบ `opencode/` ตัวอย่างเช่น สำหรับ GPT 5.5 คุณจะใช้ `opencode/gpt-5.5` ใน config ของคุณ --- @@ -144,7 +146,11 @@ https://opencode.ai/zen/v1/models | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/tr/go.mdx b/packages/web/src/content/docs/tr/go.mdx index 96a1ca3e2fd1..884c0e00cf67 100644 --- a/packages/web/src/content/docs/tr/go.mdx +++ b/packages/web/src/content/docs/tr/go.mdx @@ -65,6 +65,8 @@ Mevcut model listesi şunları içerir: - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** Test edip yenilerini ekledikçe model listesi değişebilir. @@ -82,25 +84,28 @@ Limitler dolar değeri üzerinden belirlenmiştir. Bu, gerçek istek sayınızı Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek sayısı sunmaktadır: -| Model | 5 saatte bir istek | haftalık istek | aylık istek | -| ------------- | ------------------ | -------------- | ----------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | 5 saatte bir istek | haftalık istek | aylık istek | +| ----------------- | ------------------ | -------------- | ----------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | Tahminler, gözlemlenen ortalama istek modellerine dayanmaktadır: - GLM-5/5.1 — İstek başına 700 girdi, 52.000 önbelleğe alınmış, 150 çıktı token'ı - Kimi K2.5/K2.6 — İstek başına 870 girdi, 55.000 önbelleğe alınmış, 200 çıktı token'ı +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — İstek başına 300 girdi, 55.000 önbelleğe alınmış, 125 çıktı token'ı - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -129,20 +134,22 @@ Eğer Zen bakiyenizde kredileriniz varsa, konsoldan **Bakiye kullan (Use balance Go modellerine aşağıdaki API uç noktaları aracılığıyla da erişebilirsiniz. -| Model | Model ID | Uç Nokta | AI SDK Paketi | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Model | Model ID | Uç Nokta | AI SDK Paketi | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | OpenCode yapılandırmanızdaki [model id](/docs/config/#models) formatı `opencode-go/` şeklindedir. Örneğin, Kimi K2.6 için yapılandırmanızda `opencode-go/kimi-k2.6` kullanmalısınız. diff --git a/packages/web/src/content/docs/tr/zen.mdx b/packages/web/src/content/docs/tr/zen.mdx index a3a1781f0b57..c505662cbedf 100644 --- a/packages/web/src/content/docs/tr/zen.mdx +++ b/packages/web/src/content/docs/tr/zen.mdx @@ -55,6 +55,8 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin | Model | Model ID | Endpoint | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -95,7 +97,7 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -OpenCode yapılandırmanızdaki [model id](/docs/config/#models) `opencode/` biçimini kullanır. Örneğin, GPT 5.4 için yapılandırmanızda `opencode/gpt-5.4` kullanırsınız. +OpenCode yapılandırmanızdaki [model id](/docs/config/#models) `opencode/` biçimini kullanır. Örneğin, GPT 5.5 için yapılandırmanızda `opencode/gpt-5.5` kullanırsınız. --- @@ -142,7 +144,11 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index d208d3ae5cf3..6ebf78336ae6 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -64,6 +64,8 @@ You can also access our models through the following API endpoints. | Model | Model ID | Endpoint | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -105,8 +107,8 @@ You can also access our models through the following API endpoints. | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | The [model id](/docs/config/#models) in your OpenCode config -uses the format `opencode/`. For example, for GPT 5.4, you would -use `opencode/gpt-5.4` in your config. +uses the format `opencode/`. For example, for GPT 5.5, you would +use `opencode/gpt-5.5` in your config. --- @@ -153,7 +155,11 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/zh-cn/go.mdx b/packages/web/src/content/docs/zh-cn/go.mdx index f52f5b572e0b..828da590a3ac 100644 --- a/packages/web/src/content/docs/zh-cn/go.mdx +++ b/packages/web/src/content/docs/zh-cn/go.mdx @@ -65,6 +65,8 @@ OpenCode Go 的工作方式与 OpenCode 中的其他提供商一样。 - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** 随着我们进行测试和添加新模型,该列表可能会发生变化。 @@ -82,25 +84,28 @@ OpenCode Go 包含以下限制: 下表提供了基于典型 Go 使用模式的预估请求数: -| Model | 每 5 小时请求数 | 每周请求数 | 每月请求数 | -| ------------- | --------------- | ---------- | ---------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | 每 5 小时请求数 | 每周请求数 | 每月请求数 | +| ----------------- | --------------- | ---------- | ---------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | 预估值基于观察到的平均请求模式: - GLM-5/5.1 — 每次请求 700 个输入 token,52,000 个缓存 token,150 个输出 token - Kimi K2.5/K2.6 — 每次请求 870 个输入 token,55,000 个缓存 token,200 个输出 token +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiMo-V2-Pro — 每次请求 350 个输入 token,41,000 个缓存 token,250 个输出 token - MiMo-V2-Omni — 每次请求 1000 个输入 token,60,000 个缓存 token,140 个输出 token - MiMo-V2.5-Pro — 每次请求 350 个输入 token,41,000 个缓存 token,250 个输出 token @@ -129,20 +134,22 @@ OpenCode Go 包含以下限制: 你也可以通过以下 API 端点访问 Go 模型。 -| 模型 | 模型 ID | 端点 | AI SDK 包 | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| 模型 | 模型 ID | 端点 | AI SDK 包 | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | 你 OpenCode 配置中的 [模型 ID](/docs/config/#models) 使用 `opencode-go/` 格式。例如,对于 Kimi K2.6,你将在配置中使用 `opencode-go/kimi-k2.6`。 diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index 510b93666795..9248ff174cb0 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -55,6 +55,8 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | 模型 | 模型 ID | 端点 | AI SDK 包 | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -95,7 +97,7 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 | Hy3 Preview Free | hy3-preview-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -在你的 OpenCode 配置中,[模型 ID](/docs/config/#models) 使用 `opencode/` 格式。例如,对于 GPT 5.4,你需要在配置中使用 `opencode/gpt-5.4`。 +在你的 OpenCode 配置中,[模型 ID](/docs/config/#models) 使用 `opencode/` 格式。例如,对于 GPT 5.5,你需要在配置中使用 `opencode/gpt-5.5`。 --- @@ -142,7 +144,11 @@ https://opencode.ai/zen/v1/models | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/packages/web/src/content/docs/zh-tw/go.mdx b/packages/web/src/content/docs/zh-tw/go.mdx index 481c08cec57f..1df68a6c4e55 100644 --- a/packages/web/src/content/docs/zh-tw/go.mdx +++ b/packages/web/src/content/docs/zh-tw/go.mdx @@ -65,6 +65,8 @@ OpenCode Go 的運作方式與 OpenCode 中的任何其他供應商相同。 - **Qwen3.5 Plus** - **Qwen3.6 Plus** - **MiniMax M2.7** +- **DeepSeek V4 Pro** +- **DeepSeek V4 Flash** 隨著我們測試並加入新模型,模型清單可能會有所變動。 @@ -82,25 +84,28 @@ OpenCode Go 包含以下限制: 下表提供了基於典型 Go 使用模式的預估請求次數: -| Model | 每 5 小時請求數 | 每週請求數 | 每月請求數 | -| ------------- | --------------- | ---------- | ---------- | -| GLM-5.1 | 880 | 2,150 | 4,300 | -| GLM-5 | 1,150 | 2,880 | 5,750 | -| Kimi K2.5 | 1,850 | 4,630 | 9,250 | -| Kimi K2.6 | 1,150 | 2,880 | 5,750 | -| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | -| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | -| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | -| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | -| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | -| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| Model | 每 5 小時請求數 | 每週請求數 | 每月請求數 | +| ----------------- | --------------- | ---------- | ---------- | +| GLM-5.1 | 880 | 2,150 | 4,300 | +| GLM-5 | 1,150 | 2,880 | 5,750 | +| Kimi K2.5 | 1,850 | 4,630 | 9,250 | +| Kimi K2.6 | 1,150 | 2,880 | 5,750 | +| MiMo-V2-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2-Omni | 2,150 | 5,450 | 10,900 | +| MiMo-V2.5-Pro | 1,290 | 3,225 | 6,450 | +| MiMo-V2.5 | 2,150 | 5,450 | 10,900 | +| Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | +| MiniMax M2.7 | 3,400 | 8,500 | 17,000 | +| MiniMax M2.5 | 6,300 | 15,900 | 31,800 | +| Qwen3.5 Plus | 10,200 | 25,200 | 50,500 | +| DeepSeek V4 Pro | 1,300 | 3,250 | 6,500 | +| DeepSeek V4 Flash | 7,450 | 18,600 | 37,300 | 預估值是基於觀察到的平均請求模式: - GLM-5/5.1 — 每次請求 700 個輸入 token、52,000 個快取 token、150 個輸出 token - Kimi K2.5/K2.6 — 每次請求 870 個輸入 token、55,000 個快取 token、200 個輸出 token +- DeepSeek V4 Pro/Flash — 700 input, 52,000 cached, 150 output tokens per request - MiniMax M2.7/M2.5 — 每次請求 300 個輸入 token、55,000 個快取 token、125 個輸出 token - Qwen3.5 Plus — 410 input, 47,000 cached, 140 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request @@ -129,20 +134,22 @@ OpenCode Go 包含以下限制: 您也可以透過以下 API 端點存取 Go 模型。 -| 模型 | 模型 ID | 端點 | AI SDK 套件 | -| ------------- | ------------- | ------------------------------------------------ | --------------------------- | -| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| 模型 | 模型 ID | 端點 | AI SDK 套件 | +| ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | +| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM-5 | glm-5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiMo-V2-Pro | mimo-v2-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2-Omni | mimo-v2-omni | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5-Pro | mimo-v2.5-pro | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 | mimo-v2.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/alibaba` | 您的 OpenCode 設定中的 [model id](/docs/config/#models) 使用 `opencode-go/` 格式。例如,Kimi K2.6 在設定中應使用 `opencode-go/kimi-k2.6`。 diff --git a/packages/web/src/content/docs/zh-tw/zen.mdx b/packages/web/src/content/docs/zh-tw/zen.mdx index 3634b6eca510..3be6a3da004c 100644 --- a/packages/web/src/content/docs/zh-tw/zen.mdx +++ b/packages/web/src/content/docs/zh-tw/zen.mdx @@ -59,6 +59,8 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 | 模型 | Model ID | 端點 | AI SDK Package | | --------------------- | --------------------- | -------------------------------------------------- | --------------------------- | +| GPT 5.5 | gpt-5.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.5 Pro | gpt-5.5-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | | GPT 5.4 Mini | gpt-5.4-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | @@ -100,7 +102,7 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 | Nemotron 3 Super Free | nemotron-3-super-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode 設定中的 [模型 ID](/docs/config/#models) 會使用 `opencode/` -格式。例如,如果是 GPT 5.4,你會在設定中使用 `opencode/gpt-5.4`。 +格式。例如,如果是 GPT 5.5,你會在設定中使用 `opencode/gpt-5.5`。 --- @@ -147,7 +149,11 @@ https://opencode.ai/zen/v1/models | Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - | | Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - | | Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - | -| GPT 5.4 | $2.50 | $15.00 | $0.25 | - | +| GPT 5.5 (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | - | +| GPT 5.5 (> 272K tokens) | $10.00 | $45.00 | $1.00 | - | +| GPT 5.5 Pro | $30.00 | $180.00 | $30.00 | - | +| GPT 5.4 (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | - | +| GPT 5.4 (> 272K tokens) | $5.00 | $22.50 | $0.50 | - | | GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - | | GPT 5.4 Mini | $0.75 | $4.50 | $0.075 | - | | GPT 5.4 Nano | $0.20 | $1.25 | $0.02 | - | diff --git a/script/beta.ts b/script/beta.ts index 7c558d1e7e88..d738c36ff017 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -57,6 +57,19 @@ function lines(prs: PR[]) { return prs.map((x) => `- #${x.number}: ${x.title}`).join("\n") || "(none)" } +function group(title: string) { + if (process.env.GITHUB_ACTIONS !== "true") { + console.log(title) + return { [Symbol.dispose]() {} } + } + console.log(`::group::${title}`) + return { + [Symbol.dispose]() { + console.log("::endgroup::") + }, + } +} + async function typecheck() { console.log(" Running typecheck...") @@ -81,6 +94,39 @@ async function build() { } } +async function validate() { + if (!(await typecheck())) return false + if (!(await build())) return false + return true +} + +async function commitSmokeChanges() { + const out = await $`git status --porcelain`.text() + if (!out.trim()) { + console.log("Smoke check passed") + return true + } + + try { + await $`git add -A` + await $`git commit -m "Fix beta integration"` + } catch (err) { + console.log(`Failed to commit smoke fixes: ${err}`) + return false + } + + if (!(await validate())) return false + + const left = await $`git status --porcelain`.text() + if (!left.trim()) { + console.log("Smoke check passed") + return true + } + + console.log(`Smoke check left uncommitted changes:\n${left}`) + return false +} + async function install() { console.log(" Regenerating bun.lock...") @@ -143,11 +189,15 @@ async function fix(pr: PR, files: string[], prs: PR[], applied: number[], idx: n } async function smoke(prs: PR[], applied: number[]) { - console.log("\nRunning final smoke check with opencode...") + console.log("\nRunning final smoke check...") + + if (await validate()) return commitSmokeChanges() + + console.log("\nTrying to fix final smoke check with opencode...") const done = lines(prs.filter((x) => applied.includes(x.number))) const prompt = [ - "The beta merge batch is complete.", + "The beta merge batch is complete, but the deterministic final smoke check failed.", `Merged PRs on HEAD:\n${done}`, "Run `bun typecheck` at the repo root.", "Run `./script/build.ts --single` in `packages/opencode`.", @@ -162,38 +212,8 @@ async function smoke(prs: PR[], applied: number[]) { return false } - if (!(await typecheck())) { - return false - } - - if (!(await build())) { - return false - } - - const out = await $`git status --porcelain`.text() - if (!out.trim()) { - console.log("Smoke check passed") - return true - } - - try { - await $`git add -A` - await $`git commit -m "Fix beta integration"` - } catch (err) { - console.log(`Failed to commit smoke fixes: ${err}`) - return false - } - - if (!(await typecheck())) { - return false - } - - if (!(await build())) { - return false - } - - console.log("Smoke check passed") - return true + if (!(await validate())) return false + return commitSmokeChanges() } async function main() { @@ -220,8 +240,8 @@ async function main() { const failed: FailedPR[] = [] for (const [idx, pr] of prs.entries()) { - console.log(`\nProcessing PR ${idx + 1}/${prs.length} #${pr.number}: ${pr.title}`) - + console.log() + using _ = group(`Processing PR ${idx + 1}/${prs.length} #${pr.number}: ${pr.title}`) console.log(" Fetching PR head...") try { await $`git fetch origin pull/${pr.number}/head:pr/${pr.number}` @@ -294,17 +314,13 @@ async function main() { throw new Error(`${failed.length} PR(s) failed to merge`) } - if (applied.length > 0) { - console.log("\nSkipping final smoke check") - } - console.log("\nChecking if beta branch has changes...") await $`git fetch origin beta` - const localTree = await $`git rev-parse beta^{tree}`.text() + const localTree = (await $`git rev-parse beta^{tree}`.text()).trim() const remoteTrees = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n") - const matchIdx = remoteTrees.indexOf(localTree.trim()) + const matchIdx = remoteTrees.indexOf(localTree) if (matchIdx !== -1) { if (matchIdx !== 0) { console.log(`Beta branch contains this sync, but additional commits exist after it. Leaving beta branch as is.`) @@ -314,7 +330,25 @@ async function main() { return } - console.log("Force pushing beta branch...") + if (!(await smoke(prs, applied))) throw new Error("Final smoke check failed") + + await $`git fetch origin beta` + + const validatedTree = (await $`git rev-parse beta^{tree}`.text()).trim() + const remoteTreesAfterSmoke = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n") + const matchIdxAfterSmoke = remoteTreesAfterSmoke.indexOf(validatedTree) + if (matchIdxAfterSmoke !== -1) { + if (matchIdxAfterSmoke !== 0) { + console.log( + `Beta branch contains this validated sync, but additional commits exist after it. Leaving beta branch as is.`, + ) + } else { + console.log("Validated beta branch now matches remote contents, no push needed") + } + return + } + + console.log("Force pushing validated beta branch...") await $`git push origin beta --force --no-verify` console.log("Successfully synced beta branch") diff --git a/script/github/close-issues.ts b/script/github/close-issues.ts index e8f0573ebb87..9e1f597951f1 100755 --- a/script/github/close-issues.ts +++ b/script/github/close-issues.ts @@ -37,7 +37,7 @@ async function close(num: number) { const patch = await fetch(base, { method: "PATCH", headers, - body: JSON.stringify({ state: "closed", state_reason: "completed" }), + body: JSON.stringify({ state: "closed", state_reason: "not_planned" }), }) if (!patch.ok) throw new Error(`Failed to close #${num}: ${patch.status} ${patch.statusText}`) diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index e238b367022b..73c6cc9fb9c2 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.14.22", + "version": "1.14.25", "publisher": "sst-dev", "repository": { "type": "git",