diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1c41a66faa..037020c03a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -219,6 +219,7 @@ jobs: build-electron: needs: + - build-cli - version if: github.repository == 'anomalyco/opencode' continue-on-error: false @@ -315,7 +316,10 @@ jobs: env: OPENCODE_VERSION: ${{ needs.version.outputs.version }} OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} + OPENCODE_CLI_ARTIFACT: ${{ (runner.os == 'Windows' && 'opencode-cli-windows') || 'opencode-cli' }} RUST_TARGET: ${{ matrix.settings.target }} + GH_TOKEN: ${{ github.token }} + GITHUB_RUN_ID: ${{ github.run_id }} - name: Build run: bun run build @@ -339,7 +343,8 @@ jobs: env: OPENCODE_CHANNEL: ${{ (github.ref_name == 'beta' && 'beta') || 'prod' }} GH_TOKEN: ${{ steps.committer.outputs.token }} - CSC_KEYCHAIN: build.keychain + CSC_LINK: ${{ secrets.APPLE_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APPLE_API_KEY: ${{ runner.temp }}/apple-api-key.p8 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY }} APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} diff --git a/bun.lock b/bun.lock index e905ada8c5..4d6d657727 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -37,7 +37,6 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -66,7 +65,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", + "ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", @@ -96,7 +95,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.11", + "version": "1.18.3", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +143,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +157,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +193,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +220,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +242,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +266,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +286,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.11", + "version": "1.18.3", "bin": { "opencode": "./bin/opencode", }, @@ -303,7 +302,7 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", @@ -339,7 +338,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.11.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -381,16 +380,16 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@zip.js/zip.js": "2.7.62", - "drizzle-orm": "catalog:", "effect": "catalog:", "electron-context-menu": "4.1.2", "electron-log": "^5", "electron-store": "11.0.2", "electron-updater": "6.8.9", "electron-window-state": "^5.0.3", + "marked": "^15", }, "devDependencies": { "@actions/artifact": "4.0.0", @@ -435,7 +434,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +448,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "effect": "catalog:", }, @@ -461,7 +460,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +492,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +508,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +539,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +558,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.11", + "version": "1.18.3", "bin": { "opencode": "./bin/opencode", }, @@ -578,7 +577,7 @@ "@ai-sdk/google": "3.0.73", "@ai-sdk/google-vertex": "4.0.128", "@ai-sdk/groq": "3.0.31", - "@ai-sdk/mistral": "3.0.51", + "@ai-sdk/mistral": "3.0.27", "@ai-sdk/openai": "3.0.84", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/perplexity": "3.0.26", @@ -632,7 +631,7 @@ "drizzle-orm": "catalog:", "effect": "catalog:", "fuzzysort": "3.1.0", - "gitlab-ai-provider": "6.12.1", + "gitlab-ai-provider": "6.11.1", "glob": "13.0.5", "google-auth-library": "10.5.0", "gray-matter": "4.0.3", @@ -690,7 +689,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +765,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +780,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,10 +795,9 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -811,6 +809,7 @@ "@solid-primitives/media": "2.3.3", "@solid-primitives/resize-observer": "2.1.3", "@solidjs/meta": "catalog:", + "@solidjs/router": "catalog:", "diff": "catalog:", "dompurify": "3.3.1", "fuzzysort": "catalog:", @@ -840,7 +839,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -853,7 +852,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -887,7 +886,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -906,7 +905,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -948,7 +947,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -975,7 +974,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1001,7 +1000,6 @@ "remend": "catalog:", "shiki": "catalog:", "solid-list": "catalog:", - "solid-sonner": "catalog:", "strip-ansi": "7.1.2", }, "devDependencies": { @@ -1027,7 +1025,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.11", + "version": "1.18.3", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1076,13 +1074,11 @@ "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", - "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", - "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", + "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", - "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", }, "overrides": { "@opentui/core": "catalog:", @@ -1146,7 +1142,6 @@ "shiki": "4.2.0", "solid-js": "1.9.10", "solid-list": "0.3.0", - "solid-sonner": "0.3.1", "sst": "4.13.1", "tailwindcss": "4.1.11", "typescript": "5.8.2", @@ -1204,7 +1199,7 @@ "@ai-sdk/groq": ["@ai-sdk/groq@3.0.31", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XbbugpnFmXGu2TlXiq8KUJskP6/VVbuFcnFIGDzDIB/Chg6XHsNnqrTF80Zxkh0Pd3+NvbM+2Uqrtsndk6bDAg=="], - "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.40" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-83eXY6p0lUFhSuMvNDmTKDuMciK5XDAWDlNh5c0L80tKjmtCFRItA1MZHp4IKe1r7eK8Rb5nN7qtxqMLUFRIRw=="], + "@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], "@ai-sdk/openai": ["@ai-sdk/openai@3.0.48", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ALmj/53EXpcRqMbGpPJPP4UOSWw0q4VGpnDo7YctvsynjkrKDmoneDG/1a7VQnSPYHnJp6tTRMf5ZdxZ5whulg=="], @@ -3850,13 +3845,13 @@ "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#83c0a07", {}, "anomalyco-ghostty-web-83c0a07", "sha512-Lf2v1agHkVUpMpHBWWuCZrhOEmcwwin5/Hboc9rZwQ7/CKkIh5rU1r1CvfLlhkMoFv+ed8z52RZ8hkzGZZj3MQ=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#513463a", {}, "anomalyco-ghostty-web-513463a", "sha512-GZR8LSmgGzViWnBJrqRI8MpAZRCJxhcr1Hi9Tyeh7YRooHZQjK9J97FQRD3tbBaM2wjq05gzGY2UEsG+JtZeBw=="], "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="], + "gitlab-ai-provider": ["gitlab-ai-provider@6.11.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-SJ6f5qa7P8md6lPrserryER3zerLkrezlnqqYQ2AbvDPpHLbwtbyk0FYJ5kNRcmbI80i/VMcsMBP0YIRdc3ucQ=="], "glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="], @@ -5128,8 +5123,6 @@ "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], - "solid-sonner": ["solid-sonner@0.3.1", "", { "peerDependencies": { "solid-js": "^1.6.0" } }, "sha512-F/+zi9yKJTHh5hX1UGJfkDvyC+F34Vi3jgy44NJwOKCgic1QtAon0b1iT9OsDO77RTgR+PCil+3Y5B8T2Owy1Q=="], - "solid-stripe": ["solid-stripe@0.8.1", "", { "peerDependencies": { "@stripe/stripe-js": ">=1.44.1 <8.0.0", "solid-js": "^1.6.0" } }, "sha512-l2SkWoe51rsvk9u1ILBRWyCHODZebChSGMR6zHYJTivTRC0XWrRnNNKs5x1PYXsaIU71KYI6ov5CZB5cOtGLWw=="], "solid-transition-size": ["solid-transition-size@0.1.4", "", { "dependencies": { "@corvu/utils": "~0.3.2" }, "peerDependencies": { "solid-js": "^1.8" } }, "sha512-ocHVnbfy23CgfaH4cEUR/AFg0Y3CEL8Oh3n9Qv8OHFJgPh+zkmERKZQfi/xH5XvxDCizg8VjPrVUhiHB1Gza8g=="], @@ -5706,9 +5699,7 @@ "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="], - - "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="], + "@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -6034,8 +6025,6 @@ "@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], - "@opencode-ai/app/@opencode-ai/client": ["@opencode-ai/client@vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], - "@opencode-ai/core/@ai-sdk/openai": ["@ai-sdk/openai@3.0.84", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@ai-sdk/provider-utils": "4.0.38" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cmgbeJL0bbY0yTJH4/AdmP5E7MjWRL9G8UdhIi0JlV/So03o82ORJofW8OzwCZPTORVQblFbpZXYGDcUd9NdUQ=="], "@opencode-ai/core/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], @@ -6044,6 +6033,8 @@ "@opencode-ai/desktop/@actions/artifact": ["@actions/artifact@4.0.0", "", { "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^6.0.1", "@actions/http-client": "^2.1.0", "@azure/core-http": "^3.0.5", "@azure/storage-blob": "^12.15.0", "@octokit/core": "^5.2.1", "@octokit/plugin-request-log": "^1.0.4", "@octokit/plugin-retry": "^3.0.9", "@octokit/request": "^8.4.1", "@octokit/request-error": "^5.1.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1", "archiver": "^7.0.1", "jwt-decode": "^3.1.2", "unzip-stream": "^0.3.1" } }, "sha512-HCc2jMJRAfviGFAh0FsOR/jNfWhirxl7W6z8zDtttt0GltwxBLdEIjLiweOPFl9WbyJRW1VWnPUSAixJqcWUMQ=="], + "@opencode-ai/desktop/marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "@opencode-ai/desktop/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], "@opencode-ai/llm/@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.14", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw=="], @@ -6052,8 +6043,6 @@ "@opencode-ai/script/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "@opencode-ai/session-ui/@opencode-ai/client": ["@opencode-ai/client@../app/vendor/opencode-ai-client-1.17.13-v2.tgz", {}, "sha512-332kgNifvpQOF9e3UA+pIa5xPrMhLaQkUiNiO+meS0Ba9HjSE6hfsWnEojMkD0DPSLqPP6rCF1dDoF7U0Y0OCQ=="], - "@opencode-ai/session-ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], "@opencode-ai/ui/@solid-primitives/resize-observer": ["@solid-primitives/resize-observer@2.1.3", "", { "dependencies": { "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/rootless": "^1.5.2", "@solid-primitives/static-store": "^0.1.2", "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-zBLje5E06TgOg93S7rGPldmhDnouNGhvfZVKOp+oG2XU8snA+GoCSSCz1M+jpNAg5Ek2EakU5UVQqL152WmdXQ=="], @@ -6184,8 +6173,6 @@ "ai-gateway-provider/@ai-sdk/deepseek": ["@ai-sdk/deepseek@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-9DhYurbAvcurOEGN6u2myYDybrrzGfcrkG8hwmFjwTrePW6KCMggm0YxP7e8RkLYcQKqCEMgFlyEB4BM6EmiKg=="], - "ai-gateway-provider/@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZXe7nZQgliDdjz5ufH5RKpHWxbN72AzmzzKGbF/z+0K9GN5tUCnftrQRvTRFHA5jAzTapcm2BEevmGLVbMkW+A=="], - "ai-gateway-provider/@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="], @@ -7002,8 +6989,6 @@ "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], - "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], "ajv-keywords/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -7430,8 +7415,6 @@ "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ai-gateway-provider/@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], diff --git a/infra/console.ts b/infra/console.ts index 79556f5e0c..5fbf35de00 100644 --- a/infra/console.ts +++ b/infra/console.ts @@ -288,12 +288,8 @@ new sst.cloudflare.x.SolidStart("Console", { server: { placement: { region: "aws:us-east-2" }, transform: { - worker: (args) => { - args.compatibilityFlags = $resolve(args.compatibilityFlags).apply((flags) => [ - ...(flags ?? []), - "global_fetch_strictly_public", - ]) - args.tailConsumers = [{ service: logProcessor.nodes.worker.scriptName }] + worker: { + tailConsumers: [{ service: logProcessor.nodes.worker.scriptName }], }, }, }, diff --git a/nix/hashes.json b/nix/hashes.json index 25f0e76812..b5ca7802d5 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-a7NyYa9vRUEqDfZNDPXXmFO58RDEgioyuGSl5CPBvxo=", - "aarch64-linux": "sha256-l4OJtSEllHvRhktjcaJYwkXBSaJvsIoyoLusbZfYMcM=", - "aarch64-darwin": "sha256-IIl0BQGs1/HLFh0auiQjiwfSQ2nfHcK2G2BAphYW59c=", - "x86_64-darwin": "sha256-vVeuPyd4ZIRYrHouphTuEb4rkRZLKKTAHe840jNh9rU=" + "x86_64-linux": "sha256-zAftZieWR0hWQQVkhEa826iueHaRnKxtyRodhNb+Apc=", + "aarch64-linux": "sha256-cTG+gB7CuiFgaYHSFbzXaXUI1C2kiugV3B9rUDRUwlg=", + "aarch64-darwin": "sha256-ap4Xv5ZliourkI4mcD0ktbzSI88jb4oVBv5mMK3WdUs=", + "x86_64-darwin": "sha256-NGRJMMnaMNrf3FkcYwr5PcfVm5thhlNLGIeMmAK0MeE=" } } diff --git a/package.json b/package.json index 2a8eef6356..332d7520a6 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "@sentry/solid": "10.36.0", "@sentry/vite-plugin": "4.6.0", "solid-js": "1.9.10", - "solid-sonner": "0.3.1", "vite-plugin-solid": "2.11.10", "@lydell/node-pty": "1.2.0-beta.12" } @@ -144,14 +143,12 @@ "@types/node": "catalog:" }, "patchedDependencies": { - "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", "@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", - "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", diff --git a/packages/app/V1_API_MIGRATION.md b/packages/app/V1_API_MIGRATION.md deleted file mode 100644 index 2850f10740..0000000000 --- a/packages/app/V1_API_MIGRATION.md +++ /dev/null @@ -1,220 +0,0 @@ -# V1 API Migration Checklist - -The app is currently hybrid. In this document, V1 refers to the legacy unprefixed server APIs used by `@opencode-ai/sdk/v2`, despite the SDK package name. - -## Events - -- [x] Replace `GET /global/event` with `GET /api/event`. - - `src/context/server-sdk.tsx` -- [x] Reduce current granular session and message events into the existing app projections. - - `src/context/server-session-v2-reducer.ts` - - `src/context/server-session.ts` -- [ ] Remove transitional session event dependencies: `session.created`, `session.updated`, `session.diff`, `session.status`, `session.idle`, and `session.error`. - - `src/context/global-sync/event-reducer.ts` - - `src/context/server-session.ts` - - `src/context/notification.tsx` - - `src/pages/session/usage-exceeded-dialogs.tsx` -- [ ] Remove legacy message event compatibility: `message.updated`, `message.removed`, `message.part.updated`, `message.part.removed`, and `message.part.delta`. - - `src/context/global-sync/event-reducer.ts` - - `src/context/server-session.ts` -- [x] Adapt current permission and question events to the existing request model. - - `src/context/global-sync/event-reducer.ts` - - `src/context/permission.tsx` -- [x] Consume current file watcher events. - - `src/context/file.tsx` -- [x] Consume current VCS events. - - `src/context/global-sync/event-reducer.ts` - - `src/pages/session.tsx` -- [x] Consume current `pty.exited` events. - - `src/context/terminal.tsx` -- [ ] Migrate LSP and reference events. - - `src/context/global-sync/event-reducer.ts` - -## Sessions - -- [x] Replace `GET /session/status` with one server-scoped `GET /api/session/active` snapshot plus V2 execution events. - - `src/context/server-sync.tsx` -- [x] Migrate session listing from `GET /session`. - - `src/context/server-sync.tsx` - - `src/context/directory-sync.ts` - - `src/pages/layout.tsx` -- [x] Migrate the remaining direct session read from `GET /session/:sessionID`. - - `src/components/titlebar.tsx` -- [x] Migrate session updates from `PATCH /session/:sessionID`. - - `src/context/directory-sync.ts` - - `src/context/layout.tsx` - - `src/pages/home.tsx` - - `src/pages/layout.tsx` - - `src/pages/session/timeline/message-timeline.tsx` - - `src/components/titlebar-tab-nav.tsx` - - Renames use `POST /api/session/:sessionID/rename`; archival uses `POST /api/session/:sessionID/archive`. -- [x] Migrate session deletion from `DELETE /session/:sessionID`. - - `src/pages/session/timeline/message-timeline.tsx` -- [x] Remove session diff loading from `GET /session/:sessionID/diff`. - - Historical Session diffs remain unavailable until the current API defines their snapshot semantics. -- [x] Migrate abort from `POST /session/:sessionID/abort`. - - `src/components/prompt-input/submit.ts` - - `src/pages/session/use-session-commands.tsx` - - `src/pages/session.tsx` -- [x] Migrate revert and unrevert from `POST /session/:sessionID/revert` and `POST /session/:sessionID/unrevert`. - - `src/pages/session/use-session-commands.tsx` - - `src/pages/session.tsx` -- [x] Replace `POST /session/:sessionID/summarize` with the current compact API. - - `src/pages/session/use-session-commands.tsx` -- [x] Migrate slash commands from `POST /session/:sessionID/command`. - - `src/components/prompt-input/submit.ts` -- [x] Migrate shell execution from `POST /session/:sessionID/shell`. - - `src/components/prompt-input/submit.ts` -- [x] Migrate session fork from `POST /session/:sessionID/fork`. - - `src/components/dialog-fork.tsx` -- [ ] Migrate sharing from `POST /session/:sessionID/share` and `DELETE /session/:sessionID/share`. - - `src/pages/session/use-session-commands.tsx` - - `src/pages/session/timeline/message-timeline.tsx` - - Blocked: the current API has no sharing contract or implementation. - -## Session Compatibility Fallbacks - -These calls are retained as fallback adapters. The current production path supplies the current session and message APIs. - -- [ ] Remove fallback `GET /session/:sessionID` after compatibility support is unnecessary. - - `src/context/server-session.ts` -- [ ] Remove fallback `GET /session/:sessionID/message` after compatibility support is unnecessary. - - `src/context/server-session.ts` -- [ ] Remove fallback `GET /session/:sessionID/message/:messageID` after compatibility support is unnecessary. - - `src/context/server-session.ts` - -## Filesystem - -- [ ] Migrate file listing from `GET /file`. - - `src/context/file.tsx` -- [ ] Migrate file reads from `GET /file/content`. - - `src/context/file.tsx` - - `src/pages/session/review-tab.tsx` - - `src/pages/session/v2/review-panel-v2.tsx` -- [x] Migrate path discovery from `GET /path` to `GET /api/path`. - - `src/context/global-sync/bootstrap.ts` - - `src/components/dialog-select-directory.tsx` - - `src/components/dialog-select-directory-v2.tsx` - -## Projects And Worktrees - -- [x] Migrate project listing from `GET /project` to `GET /api/project`. - - `src/context/global-sync/bootstrap.ts` -- [x] Migrate the current project lookup from `GET /project/current` to `GET /api/project/current`. - - `src/context/global-sync/bootstrap.ts` -- [ ] Migrate Git initialization from `POST /project/git/init`. - - `src/pages/session.tsx` -- [x] Migrate project updates from `PATCH /project/:projectID` to `PATCH /api/project/:projectID`. - - `src/context/layout.tsx` - - `src/components/edit-project.ts` - - `src/pages/layout.tsx` -- [ ] Migrate experimental worktree listing, creation, removal, and reset from `/experimental/worktree`. - - `src/pages/layout.tsx` - - `src/components/prompt-input/submit.ts` - - Listing now uses `GET /api/project/:projectID/directories`; create, removal, and reset remain. -- [ ] Migrate instance disposal from `POST /instance/dispose`. - - `src/pages/layout.tsx` - -## VCS - -- [x] Migrate repository information from `GET /vcs` to `GET /api/vcs`. - - `src/context/global-sync/bootstrap.ts` -- [x] Migrate diffs from `GET /vcs/diff` to `GET /api/vcs/diff`. - - `src/pages/session.tsx` -- [x] Migrate status from `GET /vcs/status` to `GET /api/vcs/status`. - - `src/pages/layout.tsx` - -## Configuration And Authentication - -- [ ] Migrate global configuration reads from `GET /global/config`. - - `src/context/global-sync/bootstrap.ts` -- [ ] Migrate directory configuration reads from `GET /config`. - - `src/context/global-sync/bootstrap.ts` -- [ ] Migrate global configuration updates from `PATCH /global/config`. - - `src/context/server-sync.tsx` -- [x] Migrate provider authentication method discovery from `GET /provider/auth` to `GET /api/integration/:integrationID`. - - `src/components/dialog-connect-provider.tsx` -- [x] Migrate built-in provider OAuth authorization and callbacks to `/api/integration/:integrationID/connect/oauth/*`. - - `src/components/dialog-connect-provider.tsx` -- [ ] Migrate remaining credentials from `PUT /auth/:providerID` and `DELETE /auth/:providerID`. - - Built-in provider key connections now use `POST /api/integration/:integrationID/connect/key`. - - `src/components/dialog-connect-provider.tsx` - - `src/components/dialog-custom-provider.tsx` - - `src/components/settings-providers.tsx` - - `src/components/settings-v2/providers.tsx` -- [ ] Migrate global disposal from `POST /global/dispose`. - - `src/components/dialog-connect-provider.tsx` - - `src/components/settings-providers.tsx` - - `src/components/settings-v2/providers.tsx` - -## Permissions And Questions - -- [x] Migrate permission listing from `GET /permission` to `GET /api/permission/request`. - - `src/context/global-sync/bootstrap.ts` - - `src/context/permission.tsx` -- [x] Migrate permission responses from `/session/:sessionID/permissions/:permissionID`. - - `src/context/permission.tsx` - - `src/pages/session/composer/session-composer-state.ts` -- [x] Migrate question listing from `GET /question` to `GET /api/question/request`. - - `src/context/global-sync/bootstrap.ts` -- [x] Migrate question replies and rejections from `/question/:requestID/*` to `/api/session/:sessionID/question/:requestID/*`. - - `src/pages/session/composer/session-question-dock.tsx` - -## Commands, MCP, LSP, And References - -- [x] Migrate command listing from `GET /command` to `GET /api/command`. - - `src/context/global-sync/bootstrap.ts` - - `src/context/server-sync.tsx` -- [x] Migrate MCP listing, connection, and disconnection from `/mcp` to `/api/mcp`. - - `src/context/server-sync.tsx` -- [ ] Replace legacy MCP authentication with the Integration OAuth workflow. - - `src/context/server-sync.tsx` -- [x] Migrate experimental resource listing from `GET /experimental/resource` to `GET /api/mcp/resource`. - - `src/context/server-sync.tsx` -- [ ] Migrate LSP status from `GET /lsp`. - - `src/context/server-sync.tsx` -- [x] Move `GET /api/reference` off the legacy generated SDK transport. - - `src/context/global-sync/bootstrap.ts` - -## Search - -- [x] Migrate global session search from `GET /experimental/session` to `GET /api/session`. - - `src/components/command-palette.ts` - - `src/components/dialog-command-palette-v2.tsx` - -## PTY And Terminal - -- [x] Migrate PTY creation, reads, updates, and deletion from `/pty` to `/api/pty`. - - `src/context/terminal.tsx` - - `src/components/terminal.tsx` -- [x] Migrate shell listing from `GET /pty/shells` to `GET /api/pty/shells`. - - `src/components/settings-general.tsx` - - `src/components/settings-v2/general.tsx` -- [x] Migrate connection tokens from `POST /pty/:ptyID/connect-token` to `POST /api/pty/:ptyID/connect-token`. - - `src/components/terminal.tsx` -- [x] Migrate the direct WebSocket connection from `/pty/:ptyID/connect` to `/api/pty/:ptyID/connect`. - - `src/components/terminal.tsx` - -## Legacy Types And Adapters - -These are not V1 network requests, but they keep the UI coupled to V1 data contracts. - -- [ ] Replace the current-session-to-legacy-session adapter. - - `src/utils/session.ts` -- [ ] Replace the current-message-to-legacy-message-and-part adapter. - - `src/utils/session-message.ts` -- [ ] Replace current agent, provider, and model adapters to legacy SDK structures. - - `src/context/global-sync/utils.ts` -- [ ] Replace legacy `Session`, `Message`, `Part`, `PermissionRequest`, `QuestionRequest`, `Project`, `FileNode`, `FileDiffInfo`, and `Event` types throughout app state and rendering. -- [ ] Remove the `@opencode-ai/sdk` runtime dependency after all legacy calls and types are gone. - - `package.json` - -## Test Infrastructure - -- [ ] Replace V1 endpoint mocks with current API mocks. - - `e2e/utils/mock-server.ts` -- [x] Replace `/global/event` and `/event` interception with current event transport handling. - - `e2e/utils/sse-transport.ts` -- [ ] Replace `SessionV1` and legacy SDK fixtures in timeline performance tests. - - `e2e/performance/timeline-stability/fixture.ts` -- [ ] Remove remaining legacy SDK type fixtures from unit and browser tests. diff --git a/packages/app/e2e/performance/timeline-stability/fixture.ts b/packages/app/e2e/performance/timeline-stability/fixture.ts index df67da5a66..5095d95db0 100644 --- a/packages/app/e2e/performance/timeline-stability/fixture.ts +++ b/packages/app/e2e/performance/timeline-stability/fixture.ts @@ -97,7 +97,6 @@ export async function setupTimeline( locale?: string deviceScaleFactor?: number seedHistory?: boolean - protocol?: "v1" | "v2" } = {}, ) { const sessions = input.sessions ?? [session()] @@ -115,7 +114,6 @@ export async function setupTimeline( retry: input.eventRetry ?? 20, }) await mockOpenCodeServer(page, { - protocol: input.protocol, directory, project: project(), provider: provider(), diff --git a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts index 838af17c93..2a214831da 100644 --- a/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts +++ b/packages/app/e2e/performance/timeline/session-parent-hydration-benchmark.spec.ts @@ -41,12 +41,7 @@ const assistants = Array.from({ length: 14 }, (_, index) => { const messages = [user, ...assistants] const target = fixture.sessions.find((session) => session.id === fixture.targetID)! const lastID = userID -const lastAssistant = assistants.at(-1)! -const lastPart = lastAssistant.parts.at(-1)! -const lastPartID = - lastPart.type === "tool" - ? lastPart.id - : `${lastAssistant.info.id}:${lastPart.type}:${lastAssistant.parts.filter((part) => part.type === lastPart.type).length - 1}` +const lastPartID = assistants.at(-1)!.parts.at(-1)!.id benchmark("hydrates an orphaned latest turn after a cold session click", async ({ browser, report }, testInfo) => { benchmark.setTimeout(180_000) @@ -112,25 +107,9 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { return { items: items.slice(start, end), cursor: start > 0 ? items[start]!.info.id : undefined } }, }) - await page.route(`**/session/${fixture.targetID}`, (route) => { - const current = new URL(route.request().url()).pathname.startsWith("/api/") - return route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify( - current - ? { - data: { - ...target, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - location: { directory: target.directory }, - }, - } - : target, - ), - }) - }) + await page.route(`**/session/${fixture.targetID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(target) }), + ) await installStressSessionTabs(page, { sessionIDs: [fixture.sourceID] }) await page.goto(stressSessionHref(fixture.sourceID)) await expectSessionTitle(page, fixture.expected.sourceTitle) @@ -165,8 +144,8 @@ async function trial(page: Page, mode: ParentHydrationBenchmarkMode) { parent: requests.filter((request) => request.type === "parent").length, } if (mode === "candidate") { - expect(requestCounts.parent).toBe(0) - expect(historyGates).toBe(0) + expect(requestCounts.parent).toBe(1) + expect(historyGates).toBe(1) } return { metrics, requestCounts, historyGateCount: historyGates } } diff --git a/packages/app/e2e/regression/cross-server-tab-close.spec.ts b/packages/app/e2e/regression/cross-server-tab-close.spec.ts index f09a2c7b63..159b5a5067 100644 --- a/packages/app/e2e/regression/cross-server-tab-close.spec.ts +++ b/packages/app/e2e/regression/cross-server-tab-close.spec.ts @@ -1,6 +1,5 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" -import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -34,7 +33,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn await tabA.locator('[data-slot="tab-close"] button').click() await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) - await expect.poll(() => requests.some((url) => url.startsWith(`${serverB}/api/session/${sessionB.id}`))).toBe(true) + await expect.poll(() => requests.some((url) => url.startsWith(`${serverB}/session/${sessionB.id}`))).toBe(true) await expect(page.getByText(sessionB.title).first()).toBeVisible() const sessionBRequests = requests.filter((url) => url.includes(`/session/${sessionB.id}`)) expect(sessionBRequests.every((url) => url.startsWith(serverB))).toBe(true) @@ -85,21 +84,17 @@ async function mockServers(page: Page, requests: string[]) { const current = url.origin === serverA ? sessionA : sessionB const directory = url.searchParams.get("directory") if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route) - if (url.pathname === "/global/health") return json(route, {}, 404) - if (url.pathname === "/api/health") return json(route, { pid: 1 }) - if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) - if (url.pathname === "/api/session/active") return json(route, { data: {} }) - if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) - if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) + if (url.pathname === "/global/health") return json(route, { healthy: true }) + if (url.pathname === "/session") return json(route, [current]) if (url.pathname === `/session/${current.id}`) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (url.pathname === `/session/${current.id}/message`) return json(route, []) if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) - if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) + if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) + return json(route, {}) if (url.pathname === "/provider") return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) @@ -121,20 +116,7 @@ async function mockServers(page: Page, requests: string[]) { directory: current.directory, home: current.directory, }) - if (url.pathname === "/api/path") - return json(route, { - state: current.directory, - config: current.directory, - worktree: current.directory, - directory: current.directory, - home: current.directory, - }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { - location: { directory: current.directory }, - data: { branch: "main", defaultBranch: "main" }, - }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/open-file-expand-folder.spec.ts b/packages/app/e2e/regression/open-file-expand-folder.spec.ts deleted file mode 100644 index 37739d2fbb..0000000000 --- a/packages/app/e2e/regression/open-file-expand-folder.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { base64Encode } from "@opencode-ai/core/util/encode" -import { expect, test } from "@playwright/test" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectSessionTitle } from "../utils/waits" - -const directory = "C:/OpenCode/OpenFileExpand" -const projectID = "proj_open_file_expand" -const sessionID = "ses_open_file_expand" -const title = "Open file expand" -const server = `http://${process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"}:${process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"}` - -test.use({ viewport: { width: 1440, height: 900 } }) - -test("expands a folder whose path has a trailing Windows separator", async ({ page }) => { - await mockOpenCodeServer(page, { - directory, - project: { - id: projectID, - worktree: directory, - vcs: "git", - name: "open-file-expand", - time: { created: 1700000000000, updated: 1700000000000 }, - sandboxes: [], - }, - provider: { - all: [ - { - id: "opencode", - name: "OpenCode", - models: { test: { id: "test", name: "Test", limit: { context: 200_000 } } }, - }, - ], - connected: ["opencode"], - default: { providerID: "opencode", modelID: "test" }, - }, - sessions: [ - { - id: sessionID, - slug: sessionID, - projectID, - directory, - title, - version: "dev", - time: { created: 1700000000000, updated: 1700000000000 }, - }, - ], - vcsDiff: [], - fileList: (path) => { - if (path === "frontend\\" || path === "frontend") { - return [ - { - name: "app.ts", - path: "frontend\\app.ts", - absolute: `${directory}/frontend/app.ts`, - type: "file" as const, - ignored: false, - }, - ] - } - if (path) return [] - return [ - { - name: "frontend", - path: "frontend\\", - absolute: `${directory}/frontend`, - type: "directory" as const, - ignored: false, - }, - { - name: "README.md", - path: "README.md", - absolute: `${directory}/README.md`, - type: "file" as const, - ignored: false, - }, - ] - }, - fileContent: (path) => ({ type: "text", content: `contents:${path}` }), - pageMessages: () => ({ items: [] }), - }) - - await page.addInitScript( - ({ directory, server, sessionID }) => { - localStorage.setItem( - "settings.v3", - JSON.stringify({ general: { newLayoutDesigns: true, shouldDisplayTabsToast: false } }), - ) - localStorage.setItem( - "opencode.global.dat:server", - JSON.stringify({ - projects: { local: [{ worktree: directory, expanded: true }] }, - lastProject: { local: directory }, - }), - ) - localStorage.setItem( - "opencode.global.dat:layout", - JSON.stringify({ review: { diffStyle: "split", panelOpened: true } }), - ) - localStorage.setItem( - "opencode.global.dat:review-panel-v2", - JSON.stringify({ sidebarOpened: true, sidebarWidth: 240, expandMode: "collapse" }), - ) - localStorage.setItem( - "opencode.window.browser.dat:tabs", - JSON.stringify([{ type: "session", server, sessionId: sessionID }]), - ) - }, - { directory, server, sessionID }, - ) - - await page.goto(`/server/${base64Encode(server)}/session/${sessionID}`) - await expectSessionTitle(page, title) - - const panel = page.locator("#review-panel") - await panel.getByRole("button", { name: "Open file" }).click() - await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") - - const sidebar = panel.locator('[data-component="session-review-v2-sidebar-root"]') - await expect(sidebar).toBeVisible() - - const frontendRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend"]') - await expect(frontendRow).toBeVisible() - await expect(frontendRow).toHaveAttribute("aria-expanded", "false") - await frontendRow.click() - await expect(frontendRow).toHaveAttribute("aria-expanded", "true") - - const appRow = panel.locator('[data-slot="file-tree-v2-row"][data-path="frontend/app.ts"]') - await expect(appRow).toBeVisible() - await appRow.click() - await expect(panel.getByRole("tab", { name: "app.ts" })).toHaveAttribute("data-selected", "") - await expect(panel.getByText("contents:frontend/app.ts", { exact: true })).toBeVisible() -}) diff --git a/packages/app/e2e/regression/project-picker-recent-search.spec.ts b/packages/app/e2e/regression/project-picker-recent-search.spec.ts deleted file mode 100644 index 2cdb0b4a03..0000000000 --- a/packages/app/e2e/regression/project-picker-recent-search.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect, test } from "@playwright/test" -import type { Page } from "@playwright/test" -import { fixture, pageMessages } from "../smoke/session-timeline.fixture" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectAppVisible } from "../utils/waits" - -const NAMES = ["alpha-service", "bravo-web", "charlie-api", "delta-tools", "echo-infra", "foxtrot-docs"] -const worktrees = NAMES.map((name) => `/opencode-demo/${name}`) - -// The sixth project sits outside the five-item recent cap, so it is only reachable if the -// dialog hands every recent project to the list filter instead of a pre-truncated slice. -const OUTSIDE_CAP = "foxtrot-docs" - -// Dialog rows carry data-directory-path; the sidebar project list does not, so this -// scopes assertions to the picker instead of matching the sidebar entry of the same name. -const rows = (page: Page) => page.locator("[data-directory-path]") -const row = (page: Page, name: string) => page.locator(`[data-directory-path*="${name}"]`) - -async function openProjectDialog(page: Page) { - await mockOpenCodeServer(page, { - sessions: fixture.sessions, - provider: fixture.provider, - directory: fixture.directory, - project: fixture.project, - pageMessages, - fileList: () => [], - findFiles: () => [], - }) - await page.addInitScript((dirs) => { - localStorage.setItem( - "opencode.global.dat:server", - JSON.stringify({ - projects: { local: dirs.map((worktree: string) => ({ worktree, expanded: false })) }, - lastProject: {}, - }), - ) - }, worktrees) - await page.goto("/") - const add = page.getByRole("button", { name: "Add project" }).first() - await expectAppVisible(add) - await add.click() - await expect(rows(page)).toHaveCount(5) - return page.getByRole("textbox").last() -} - -test("searches every recent project, not just the five most recent", async ({ page }) => { - const search = await openProjectDialog(page) - await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) - - await search.fill("foxtrot") - - await expect(row(page, OUTSIDE_CAP)).toHaveCount(1) -}) - -test("still caps the idle recent list at five projects", async ({ page }) => { - await openProjectDialog(page) - - await expect(row(page, NAMES[4])).toHaveCount(1) - await expect(row(page, OUTSIDE_CAP)).toHaveCount(0) -}) diff --git a/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts b/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts deleted file mode 100644 index 50dcc8820b..0000000000 --- a/packages/app/e2e/regression/prompt-input-v2-command-draft.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect, test } from "@playwright/test" -import { base64Encode } from "@opencode-ai/core/util/encode" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectAppVisible } from "../utils/waits" - -const directory = "C:/OpenCode/PromptInputV2Editing" -const projectID = "proj_prompt_input_v2_editing" -const sessionID = "ses_prompt_input_v2_editing" - -test("preserves the draft when a populated command menu triggers a built-in", async ({ page }) => { - await mockOpenCodeServer(page, { - directory, - project: { - id: projectID, - worktree: directory, - vcs: "git", - name: "prompt-input-v2-editing", - time: { created: 1700000000000, updated: 1700000000000 }, - sandboxes: [], - }, - provider: { all: [], connected: [], default: {} }, - sessions: [ - { - id: sessionID, - slug: "prompt-input-v2-editing", - projectID, - directory, - title: "Prompt input V2 editing", - version: "dev", - time: { created: 1700000000000, updated: 1700000000000 }, - }, - ], - pageMessages: () => ({ items: [] }), - }) - await page.addInitScript(() => { - localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - }) - - await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) - const composer = page.locator('[data-component="prompt-input-v2"]') - const input = composer.locator('[data-component="prompt-input"]') - await expectAppVisible(composer) - - await input.fill("keep me") - await composer.getByRole("button", { name: "Add images and files" }).click() - await page.getByRole("menuitem", { name: "Commands" }).click() - await page.locator('[data-suggestion-id="model.choose"]').click() - - await expect(input).toHaveText("keep me") -}) diff --git a/packages/app/e2e/regression/remote-session-settings.spec.ts b/packages/app/e2e/regression/remote-session-settings.spec.ts index 35a0aa44cd..c17ae5c1c6 100644 --- a/packages/app/e2e/regression/remote-session-settings.spec.ts +++ b/packages/app/e2e/regression/remote-session-settings.spec.ts @@ -1,7 +1,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { expect, test, type Page, type Route } from "@playwright/test" import { installSseTransport } from "../utils/sse-transport" -import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -18,7 +17,7 @@ test("session settings use the remote server context", async ({ page }) => { await page.goto(`/server/${base64Encode(serverB)}/session/${sessionB.id}`) await expect(page.getByText(sessionB.title).first()).toBeVisible() - await page.keyboard.press("Control+,") + await page.keyboard.press(process.platform === "darwin" ? "Meta+," : "Control+,") const dialog = page.locator(".settings-v2-dialog") const autoAccept = dialog.locator('[data-action="settings-auto-accept-permissions"]') @@ -59,7 +58,7 @@ test("auto-accept responds for an unfocused server session", async ({ page }) => const hrefB = `/server/${base64Encode(serverB)}/session/${sessionB.id}` await page.goto(`/server/${base64Encode(serverA)}/session/${sessionA.id}`) await expect(page.getByText(sessionA.title).first()).toBeVisible() - await page.keyboard.press("Control+,") + await page.keyboard.press(process.platform === "darwin" ? "Meta+," : "Control+,") const autoAccept = page.locator(".settings-v2-dialog").locator('[data-action="settings-auto-accept-permissions"]') await autoAccept.locator('[data-slot="switch-control"]').click() await expect(autoAccept.getByRole("switch")).toBeChecked() @@ -181,36 +180,10 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR return json(route, true) } if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) if (url.pathname === "/global/health") return json(route, { healthy: true }) - if (url.pathname === "/api/provider" || url.pathname === "/api/model" || url.pathname === "/api/agent") - return json(route, { data: [] }) - if (url.pathname === "/api/model/default") return json(route, { data: null }) - if (["/api/command", "/api/reference", "/api/permission/request", "/api/question/request"].includes(url.pathname)) - return json(route, { location: { directory }, data: [] }) - if (url.pathname === "/api/mcp") return json(route, { location: { directory }, data: [] }) - if (url.pathname === "/api/mcp/resource") - return json(route, { location: { directory }, data: { resources: [], templates: [] } }) - if (url.pathname === "/api/project") { - return json(route, [ - { - id: remote ? sessionB.projectID : "project-server-a", - worktree: directory, - vcs: "git", - time: { created: 1, updated: 1 }, - sandboxes: [], - }, - ]) - } - if (url.pathname === "/api/project/current") - return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory }) - if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) - if (url.pathname === "/api/session/active") return json(route, { data: {} }) - const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`) - if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) - if (sessions.some((session) => url.pathname === `/api/session/${session.id}/message`)) - return json(route, { data: [], cursor: {} }) + if (url.pathname === "/session/status") return json(route, {}) + if (url.pathname === "/session") return json(route, sessions) const current = sessions.find((session) => url.pathname === `/session/${session.id}`) if (current) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) @@ -243,12 +216,7 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR directory, home: directory, }) - if (url.pathname === "/api/path") - return json(route, { state: directory, config: directory, worktree: directory, directory, home: directory }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { location: { directory }, data: { branch: "main", defaultBranch: "main" } }) - if (url.pathname === "/api/pty/shells") return json(route, { location: { directory }, data: [] }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/remote-tab-busy.spec.ts b/packages/app/e2e/regression/remote-tab-busy.spec.ts index 2d9b1e2349..119fc7ee2d 100644 --- a/packages/app/e2e/regression/remote-tab-busy.spec.ts +++ b/packages/app/e2e/regression/remote-tab-busy.spec.ts @@ -1,6 +1,5 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" -import { currentSession } from "../utils/mock-server" const serverA = "http://127.0.0.1:4096" const serverB = "http://127.0.0.1:4097" @@ -58,15 +57,11 @@ async function mockServers(page: Page) { const current = url.origin === serverA ? sessionA : sessionB const directory = url.searchParams.get("directory") if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route, url.pathname === "/api/event") - if (url.pathname === "/global/health") return json(route, {}, 404) - if (url.pathname === "/api/health") return json(route, { pid: 1 }) - if (url.pathname === "/api/session/active") - return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} }) - if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} }) - if (url.pathname === `/api/session/${current.id}`) return json(route, { data: currentSession(current) }) - if (url.pathname === `/api/session/${current.id}/message`) return json(route, { data: [], cursor: {} }) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) + if (url.pathname === "/global/health") return json(route, { healthy: true }) + if (url.pathname === "/session/status") + return json(route, url.origin === serverB ? { [sessionB.id]: { type: "busy" } } : {}) + if (url.pathname === "/session") return json(route, [current]) if (url.pathname === `/session/${current.id}`) return json(route, current) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) if (url.pathname === `/session/${current.id}/message`) return json(route, []) @@ -95,20 +90,7 @@ async function mockServers(page: Page) { directory: current.directory, home: current.directory, }) - if (url.pathname === "/api/path") - return json(route, { - state: current.directory, - config: current.directory, - worktree: current.directory, - directory: current.directory, - home: current.directory, - }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { - location: { directory: current.directory }, - data: { branch: "main", defaultBranch: "main" }, - }) return json(route, {}) }) } @@ -122,10 +104,6 @@ function json(route: Route, body: unknown, status = 200) { }) } -function sse(route: Route, current: boolean) { - return route.fulfill({ - status: 200, - contentType: "text/event-stream", - body: current ? 'data: {"id":"evt_connected","type":"server.connected","data":{}}\n\n' : ": ok\n\n", - }) +function sse(route: Route) { + return route.fulfill({ status: 200, contentType: "text/event-stream", body: ": ok\n\n" }) } diff --git a/packages/app/e2e/regression/review-line-comment.spec.ts b/packages/app/e2e/regression/review-line-comment.spec.ts index 7850f7820a..042f926c53 100644 --- a/packages/app/e2e/regression/review-line-comment.spec.ts +++ b/packages/app/e2e/regression/review-line-comment.spec.ts @@ -84,7 +84,6 @@ test("stages a submitted line comment in the prompt context", async ({ page }) = async function openReview(page: Page) { await page.setViewportSize({ width: 700, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: "proj_review_line_comment_regression", @@ -144,9 +143,9 @@ async function openReview(page: Page) { await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, title) - const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/api/vcs/diff") + const diffResponse = page.waitForResponse((response) => new URL(response.url()).pathname === "/vcs/diff") await page.getByRole("tab", { name: "Changes" }).click() - expect((await (await diffResponse).json()).data).toHaveLength(1) + expect(await (await diffResponse).json()).toHaveLength(1) const review = page.locator('[data-component="session-review"]') await expectAppVisible(review) diff --git a/packages/app/e2e/regression/review-open-file.spec.ts b/packages/app/e2e/regression/review-open-file.spec.ts index 04e6d2cced..25ebd3a37a 100644 --- a/packages/app/e2e/regression/review-open-file.spec.ts +++ b/packages/app/e2e/regression/review-open-file.spec.ts @@ -133,7 +133,7 @@ test("opens and searches project files inline", async ({ page }) => { await expect(panel.getByRole("tab", { name: "nested.ts" })).toHaveCount(1) await expect(panel.getByRole("tab", { name: "Open file" })).toHaveAttribute("data-selected", "") await expect(sidebarToggle).toBeDisabled() - await panel.locator("#session-side-panel-review-tab").click() + await panel.getByRole("tab", { name: /Review/ }).click() await expect(sidebarToggle).toBeEnabled() await panel.getByRole("tab", { name: "Open file" }).click() await page.keyboard.press("Control+w") diff --git a/packages/app/e2e/regression/review-state-persistence.spec.ts b/packages/app/e2e/regression/review-state-persistence.spec.ts index 0d6756201e..aa42f1bb51 100644 --- a/packages/app/e2e/regression/review-state-persistence.spec.ts +++ b/packages/app/e2e/regression/review-state-persistence.spec.ts @@ -46,7 +46,7 @@ test("restores review mode and selected file per session", async ({ page }) => { async function selectMode(page: Page, current: string, next: string) { await page.getByRole("button", { name: current }).click() - await page.getByRole("option", { name: next }).dispatchEvent("click") + await page.getByRole("option", { name: next }).click() } async function selectFile(page: Page, file: string) { @@ -65,7 +65,6 @@ async function switchSession(page: Page, title: string) { async function setup(page: Page) { await mockOpenCodeServer(page, { - protocol: "v1", directory, project: { id: projectID, diff --git a/packages/app/e2e/regression/review-terminal-stacked.spec.ts b/packages/app/e2e/regression/review-terminal-stacked.spec.ts index 79b564820e..154bab48c4 100644 --- a/packages/app/e2e/regression/review-terminal-stacked.spec.ts +++ b/packages/app/e2e/regression/review-terminal-stacked.spec.ts @@ -20,12 +20,10 @@ const branchDiffs = [ test("keeps the review tree and terminal sized when both panels are open", async ({ page }) => { test.setTimeout(120_000) const events: Array<{ directory: string; payload: Record }> = [] - const sessionStatus = { [sessionID]: { type: "idle" as "busy" | "idle" } } let detailVersion = 1 let detailFailures = 1 await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v1", directory, project: { id: projectID, @@ -57,7 +55,7 @@ test("keeps the review tree and terminal sized when both panels are open", async time: { created: 1700000000000, updated: 1700000000000 }, }, ], - sessionStatus: () => sessionStatus, + sessionStatus: { [sessionID]: { type: "idle" } }, pageMessages: () => ({ items: [] }), events: () => events.splice(0, 1), eventRetry: 16, @@ -66,10 +64,7 @@ test("keeps the review tree and terminal sized when both panels are open", async route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - branch: "review-pane-performance", - default_branch: "dev", - }), + body: JSON.stringify({ branch: "review-pane-performance", default_branch: "dev" }), }), ) await page.route("**/vcs/diff**", (route) => { @@ -91,51 +86,15 @@ test("keeps the review tree and terminal sized when both panels are open", async ), }) }) - await page.route("**/pty*", (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_review_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), + body: JSON.stringify({ id: "pty_review_terminal", title: "Terminal 1" }), }), ) - await page.route("**/pty/pty_review_terminal*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_review_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), - }), - ) - await page.route("**/pty/pty_review_terminal/connect-token*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { ticket: "e2e-ticket", expires_in: 60 }, - }), - }), + await page.route("**/pty/pty_review_terminal", (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), ) await page.routeWebSocket("**/pty/pty_review_terminal/connect", () => undefined) await page.addInitScript(() => { @@ -184,7 +143,6 @@ test("keeps the review tree and terminal sized when both panels are open", async const preview = page.locator('[data-slot="session-review-v2-diff-scroll"]') await expect(preview).toContainText("after-1") detailVersion = 2 - sessionStatus[sessionID] = { type: "busy" } events.push(statusEvent("busy")) await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() const refreshedDiff = page.waitForRequest((request) => { @@ -194,7 +152,6 @@ test("keeps the review tree and terminal sized when both panels are open", async url.searchParams.get("directory")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true ) }) - sessionStatus[sessionID] = { type: "idle" } events.push(statusEvent("idle")) await refreshedDiff await expect(preview).toContainText("after-2") diff --git a/packages/app/e2e/regression/session-list-path-loading.spec.ts b/packages/app/e2e/regression/session-list-path-loading.spec.ts index 3319514df6..4a3855122a 100644 --- a/packages/app/e2e/regression/session-list-path-loading.spec.ts +++ b/packages/app/e2e/regression/session-list-path-loading.spec.ts @@ -16,8 +16,8 @@ test("shows loaded sessions before the directory path request resolves", async ( const pathBlocked = new Promise((resolve) => { releasePath = resolve }) - await page.route("**/api/path?*", async (route) => { - if (!new URL(route.request().url()).searchParams.has("location[directory]")) return route.fallback() + await page.route("**/path?*", async (route) => { + if (!new URL(route.request().url()).searchParams.has("directory")) return route.fallback() await pathBlocked return route.fallback() }) diff --git a/packages/app/e2e/regression/session-request-docks.spec.ts b/packages/app/e2e/regression/session-request-docks.spec.ts index 5ea9d4f761..714d6ca96f 100644 --- a/packages/app/e2e/regression/session-request-docks.spec.ts +++ b/packages/app/e2e/regression/session-request-docks.spec.ts @@ -42,8 +42,7 @@ test("shows a pending question dock", async ({ page }) => { const rejectRequests: string[] = [] page.on("request", (request) => { if (request.method() !== "POST") return - if (new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reject`) - rejectRequests.push(request.url()) + if (new URL(request.url()).pathname === "/question/question-request/reject") rejectRequests.push(request.url()) }) await question.locator('[data-component="icon-button"][data-icon="chevron-down"]').click() @@ -65,9 +64,7 @@ test("shows a pending question dock", async ({ page }) => { await question.getByRole("radio", { name: /Minimal/ }).click() const reply = page.waitForRequest( - (request) => - request.method() === "POST" && - new URL(request.url()).pathname === `/api/session/${sessionID}/question/question-request/reply`, + (request) => request.method() === "POST" && new URL(request.url()).pathname === "/question/question-request/reply", ) await question.getByRole("button", { name: "Submit" }).click() expect((await reply).postDataJSON()).toEqual({ answers: [["Minimal"]] }) @@ -100,8 +97,8 @@ test("shows a pending permission dock", async ({ page }) => { const reply = page.waitForRequest((request) => request.method() === "POST") await permission.getByRole("button", { name: "Allow once" }).click() const request = await reply - expect(new URL(request.url()).pathname).toBe(`/api/session/${sessionID}/permission/permission-request/reply`) - expect(request.postDataJSON()).toEqual({ reply: "once" }) + expect(new URL(request.url()).pathname).toBe(`/session/${sessionID}/permissions/permission-request`) + expect(request.postDataJSON()).toEqual({ response: "once" }) }) test("restores the draft caret before typing after a request dock closes", async ({ page }) => { @@ -173,7 +170,6 @@ async function mockServer( }, ) { await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, diff --git a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts index b303071c87..3e2b171bca 100644 --- a/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts +++ b/packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts @@ -32,23 +32,6 @@ for (const expanded of [false, true]) { }) } -test("shows and expands a running shell command without shimmering it", async ({ page }) => { - const id = "prt_shell_running_command" - const command = "sleep 10 && echo done" - await setupTimeline(page, { - messages: [userMessage(), assistantMessage([shell(id, "running", "still running", command)], { completed: false })], - settings: { shellToolPartsExpanded: false }, - }) - - const tool = page.locator(`[data-timeline-part-id="${id}"]`) - await expect(tool.locator('[data-component="text-shimmer"]')).toHaveAttribute("data-active", "true") - await expect(tool.locator('[data-component="shell-submessage"]')).toHaveText(command) - await expect(tool.locator('[data-component="shell-submessage"] [data-component="text-shimmer"]')).toHaveCount(0) - await tool.locator('[data-slot="collapsible-trigger"]').click() - await expect(tool.locator('[data-slot="collapsible-trigger"]')).toHaveAttribute("aria-expanded", "true") - await expect(tool.locator('[data-slot="bash-pre"]')).toContainText("still running") -}) - test("transitions thinking and hidden reasoning through busy to idle", async ({ page }) => { const reasoningID = "prt_reasoning_hidden" const assistant = assistantMessage([reasoningPart(reasoningID, "## Inspecting stability")], { completed: false }) diff --git a/packages/app/e2e/regression/session-timeline-transport.spec.ts b/packages/app/e2e/regression/session-timeline-transport.spec.ts index 778ff3a3af..850e966d0b 100644 --- a/packages/app/e2e/regression/session-timeline-transport.spec.ts +++ b/packages/app/e2e/regression/session-timeline-transport.spec.ts @@ -89,8 +89,8 @@ test("reconnects after a stream error", async ({ page }) => { expect((await timeline.transport.connections())[0]?.endedBy).toBe("error") }) -test("does not request replay when reconnecting the volatile V2 event stream", async ({ page }) => { - const timeline = await setupTimeline(page, { eventRetry: 10, protocol: "v2" }) +test("records event IDs and reconnect Last-Event-ID headers", async ({ page }) => { + const timeline = await setupTimeline(page, { eventRetry: 10 }) const first = await timeline.transport.send(partUpdated(textPart("prt_transport_id", "event with id")), { id: "timeline-event-7", }) @@ -100,7 +100,7 @@ test("does not request replay when reconnecting the volatile V2 event stream", a const connection = await timeline.transport.waitForConnection({ after: first.connectionID }) expect(first.eventID).toBe("timeline-event-7") - expect(connection.headers["last-event-id"]).toBeUndefined() + expect(connection.headers["last-event-id"]).toBe("timeline-event-7") }) test("passes through non-event fetches", async ({ page }) => { diff --git a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts index 55e7121275..603c411d55 100644 --- a/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts +++ b/packages/app/e2e/regression/session-todo-dock-navigation.spec.ts @@ -27,7 +27,6 @@ test("animates todo lifecycle without replaying it across session tabs", async ( test.setTimeout(90_000) const events: EventPayload[] = [] const todos: Record = { [sourceID]: [], [otherID]: [] } - const sessionStatus: Record = {} await mockOpenCodeServer(page, { directory, @@ -57,11 +56,9 @@ test("animates todo lifecycle without replaying it across session tabs", async ( default: { providerID: "opencode", modelID: "claude-opus-4-6" }, }, sessions: [session(sourceID, sourceTitle, 1700000000000), session(otherID, otherTitle, 1700000001000)], - sessionStatus: { [sourceID]: { type: "busy" } }, pageMessages: () => ({ items: [] }), events: () => events.splice(0, 1), eventRetry: 16, - sessionStatus: () => sessionStatus, todos: (sessionID) => todos[sessionID] ?? [], }) await configurePage(page) @@ -71,7 +68,6 @@ test("animates todo lifecycle without replaying it across session tabs", async ( const dock = page.locator('[data-component="session-todo-dock"]') await expect(dock).toHaveCount(0) - sessionStatus[sourceID] = { type: "busy" } events.push(statusEvent(sourceID, "busy")) await expect(page.getByRole("button", { name: "Stop" })).toBeVisible() diff --git a/packages/app/e2e/regression/subagent-child-navigation.spec.ts b/packages/app/e2e/regression/subagent-child-navigation.spec.ts index 019cc156ec..19d2c29af0 100644 --- a/packages/app/e2e/regression/subagent-child-navigation.spec.ts +++ b/packages/app/e2e/regression/subagent-child-navigation.spec.ts @@ -1,6 +1,6 @@ import { base64Encode } from "@opencode-ai/core/util/encode" import { expect, test, type Page } from "@playwright/test" -import { currentSession, mockOpenCodeServer } from "../utils/mock-server" +import { mockOpenCodeServer } from "../utils/mock-server" import { expectSessionTitle } from "../utils/waits" const directory = "C:/OpenCode/SubagentNavigation" @@ -72,19 +72,16 @@ async function setup(page: Page, events?: () => EventPayload[]) { events, eventRetry: events ? 16 : undefined, }) - // The child session resolves by ID but is absent from the session list, + // The child session resolves via /session/:id but is absent from the /session list, // matching a subagent session that has not been loaded into the list cache yet. await page.route( - (url) => url.pathname === "/api/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"), + (url) => url.pathname === "/session" && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"), (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ - data: [currentSession(session(parentID, parentTitle, 1700000000000))], - cursor: {}, - }), + body: JSON.stringify([session(parentID, parentTitle, 1700000000000)]), }), ) await configurePage(page) diff --git a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts index b969b590d8..94afbc9a9d 100644 --- a/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts +++ b/packages/app/e2e/regression/tab-navigate-mousedown.spec.ts @@ -1,12 +1,9 @@ import { expect, test, type Page, type Route } from "@playwright/test" import { base64Encode } from "@opencode-ai/core/util/encode" -import { currentSession } from "../utils/mock-server" const server = "http://127.0.0.1:4096" const sessionA = session("ses_tab_a", "Tab A session") const sessionB = session("ses_tab_b", "Tab B session") -const sessionC = session("ses_tab_c", "Tab C session") -const unresolvedSessionID = "ses_tab_unresolved" test("pressing mouse down on a tab navigates before mouse up", async ({ page }) => { await mockServer(page) @@ -42,34 +39,6 @@ test("pressing mouse down on a tab navigates before mouse up", async ({ page }) await expect(page).toHaveURL(new RegExp(`${hrefB.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) }) -test("keyboard navigation follows the visible tab order", async ({ page }) => { - await mockServer(page) - await page.addInitScript( - ({ server, sessionA, unresolved, sessionC }) => { - localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem( - "opencode.window.browser.dat:tabs", - JSON.stringify([ - { type: "session", server, sessionId: sessionA }, - { type: "session", server, sessionId: unresolved }, - { type: "session", server, sessionId: sessionC }, - ]), - ) - }, - { server, sessionA: sessionA.id, unresolved: unresolvedSessionID, sessionC: sessionC.id }, - ) - - const hrefA = `/server/${base64Encode(server)}/session/${sessionA.id}` - const hrefC = `/server/${base64Encode(server)}/session/${sessionC.id}` - await page.goto(hrefA) - await expect(page.locator("[data-titlebar-tab-slot]:visible")).toHaveCount(2) - await expect(page.locator(`[data-titlebar-tab-slot]:has(a[href="${hrefC}"])`)).toBeVisible() - - await page.keyboard.press("Control+Alt+ArrowRight") - - await expect(page).toHaveURL(new RegExp(`${hrefC.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`)) -}) - function session(id: string, title: string) { return { id, @@ -83,21 +52,13 @@ function session(id: string, title: string) { } async function mockServer(page: Page) { - const sessions = [sessionA, sessionB, sessionC] + const sessions = [sessionA, sessionB] await page.route("**/*", async (route) => { const url = new URL(route.request().url()) if (url.origin !== server) return route.fallback() - if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname)) - return new Promise(() => {}) - if (url.pathname === "/global/event" || url.pathname === "/event" || url.pathname === "/api/event") - return sse(route) + if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route) if (url.pathname === "/global/health") return json(route, { healthy: true }) - if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} }) - if (url.pathname === "/api/session/active") return json(route, { data: {} }) - const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`) - if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) }) - if (sessions.some((item) => url.pathname === `/api/session/${item.id}/message`)) - return json(route, { data: [], cursor: {} }) + if (url.pathname === "/session") return json(route, sessions) const byId = sessions.find((item) => url.pathname === `/session/${item.id}`) if (byId) return json(route, byId) if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404) @@ -105,7 +66,8 @@ async function mockServer(page: Page) { if (/^\/session\/[^/]+\/(children|todo|diff)$/.test(url.pathname)) return json(route, []) if (["/skill", "/command", "/lsp", "/formatter", "/permission", "/question", "/vcs/diff"].includes(url.pathname)) return json(route, []) - if (["/global/config", "/config", "/provider/auth", "/mcp"].includes(url.pathname)) return json(route, {}) + if (["/global/config", "/config", "/provider/auth", "/mcp", "/session/status"].includes(url.pathname)) + return json(route, {}) if (url.pathname === "/provider") return json(route, { all: [], connected: [], default: { providerID: "", modelID: "" } }) if (url.pathname === "/agent") return json(route, [{ name: "build", mode: "primary" }]) @@ -127,20 +89,7 @@ async function mockServer(page: Page) { directory: sessionA.directory, home: sessionA.directory, }) - if (url.pathname === "/api/path") - return json(route, { - state: sessionA.directory, - config: sessionA.directory, - worktree: sessionA.directory, - directory: sessionA.directory, - home: sessionA.directory, - }) if (url.pathname === "/vcs") return json(route, { branch: "main", default_branch: "main" }) - if (url.pathname === "/api/vcs") - return json(route, { - location: { directory: sessionA.directory }, - data: { branch: "main", defaultBranch: "main" }, - }) return json(route, {}) }) } diff --git a/packages/app/e2e/regression/terminal-composer-focus.spec.ts b/packages/app/e2e/regression/terminal-composer-focus.spec.ts index 99bf689085..f672602782 100644 --- a/packages/app/e2e/regression/terminal-composer-focus.spec.ts +++ b/packages/app/e2e/regression/terminal-composer-focus.spec.ts @@ -13,7 +13,6 @@ test.use({ viewport: { width: 1440, height: 900 } }) test.beforeEach(async ({ page }) => { await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, @@ -47,30 +46,25 @@ test.beforeEach(async ({ page }) => { ], pageMessages: () => ({ items: [] }), }) - await page.route("**/api/pty*", (route) => { - expect(new URL(route.request().url()).searchParams.get("location[directory]")).toBe(directory) - return route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), - }) - }) - await page.route(`**/api/pty/${ptyID}*`, (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), + body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), }), ) - await page.route(`**/api/pty/${ptyID}/connect-token*`, (route) => + await page.route(`**/pty/${ptyID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), + ) + await page.route(`**/pty/${ptyID}/connect-token*`, (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), + body: JSON.stringify({ ticket: "e2e-ticket" }), }), ) - await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), () => undefined) + await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), () => undefined) await page.addInitScript(() => { localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) }) @@ -101,12 +95,12 @@ test("keeps composer focus when a cached terminal finishes mounting", async ({ p const ghostty = Promise.withResolvers() const release = Promise.withResolvers() const created = { count: 0 } - await page.route("**/api/pty*", (route) => { + await page.route("**/pty", (route) => { created.count += 1 return route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(ptyID, "Terminal 1") }), + body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), }) }) await page.route(/ghostty-web/, async (route) => { @@ -161,31 +155,27 @@ test("keeps newer composer focus while an explicit terminal open finishes", asyn test("focuses a terminal created from the new-terminal button", async ({ page }) => { const created = { count: 0 } - await page.route("**/api/pty*", (route) => { + await page.route("**/pty", (route) => { created.count += 1 - const next = created.count === 1 ? ptyInfo(ptyID, "Terminal 1") : ptyInfo(newPtyID, "Terminal 2") + const next = created.count === 1 ? { id: ptyID, title: "Terminal 1" } : { id: newPtyID, title: "Terminal 2" } return route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: next }), + body: JSON.stringify(next), }) }) - await page.route(`**/api/pty/${newPtyID}*`, (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo(newPtyID, "Terminal 2") }), - }), + await page.route(`**/pty/${newPtyID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), ) - await page.route(`**/api/pty/${newPtyID}/connect-token*`, (route) => + await page.route(`**/pty/${newPtyID}/connect-token*`, (route) => route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), + body: JSON.stringify({ ticket: "e2e-ticket" }), }), ) - await page.routeWebSocket(new RegExp(`/api/pty/${newPtyID}/connect`), () => undefined) + await page.routeWebSocket(new RegExp(`/pty/${newPtyID}/connect`), () => undefined) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, "Terminal composer focus") @@ -217,11 +207,3 @@ function seedCachedTerminal(page: Page) { { terminalKey: `${base64Encode(directory)}/terminal.v1`, ptyID }, ) } - -function ptyLocation() { - return { directory, project: { id: projectID, directory } } -} - -function ptyInfo(id: string, title: string) { - return { id, title, command: "cmd.exe", args: [], cwd: directory, status: "running", pid: 1 } -} diff --git a/packages/app/e2e/regression/terminal-hidden.spec.ts b/packages/app/e2e/regression/terminal-hidden.spec.ts index 8e08d60ff2..73821580af 100644 --- a/packages/app/e2e/regression/terminal-hidden.spec.ts +++ b/packages/app/e2e/regression/terminal-hidden.spec.ts @@ -10,7 +10,6 @@ const title = "Hidden terminal regression" test("unmounts the terminal panel while it is hidden", async ({ page }) => { await page.setViewportSize({ width: 1400, height: 900 }) await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, @@ -44,53 +43,17 @@ test("unmounts the terminal panel while it is hidden", async ({ page }) => { ], pageMessages: () => ({ items: [] }), }) - await page.route("**/api/pty*", (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_hidden_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), + body: JSON.stringify({ id: "pty_hidden_terminal", title: "Terminal 1" }), }), ) - await page.route("**/api/pty/pty_hidden_terminal*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { - id: "pty_hidden_terminal", - title: "Terminal 1", - command: "cmd.exe", - args: [], - cwd: directory, - status: "running", - pid: 1, - }, - }), - }), + await page.route("**/pty/pty_hidden_terminal", (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), ) - await page.route("**/api/pty/pty_hidden_terminal/connect-token*", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - location: { directory, project: { id: projectID, directory } }, - data: { ticket: "e2e-ticket", expires_in: 60 }, - }), - }), - ) - await page.routeWebSocket("**/api/pty/pty_hidden_terminal/connect", () => undefined) + await page.routeWebSocket("**/pty/pty_hidden_terminal/connect", () => undefined) await page.goto(`/${base64Encode(directory)}/session/${sessionID}`) await expectSessionTitle(page, title) diff --git a/packages/app/e2e/regression/terminal-tab-switch.spec.ts b/packages/app/e2e/regression/terminal-tab-switch.spec.ts index 165920753c..cbb72958ad 100644 --- a/packages/app/e2e/regression/terminal-tab-switch.spec.ts +++ b/packages/app/e2e/regression/terminal-tab-switch.spec.ts @@ -29,10 +29,6 @@ test("keeps the terminal session alive when switching session tabs in a workspac const terminal = page.locator('[data-component="terminal"]') await expect(terminal).toBeVisible() await expect.poll(() => connections.length).toBe(1) - const connection = new URL(connections[0]!) - expect(connection.pathname).toBe(`/api/pty/${ptyID}/connect`) - expect(connection.searchParams.get("location[directory]")).toBe(directory) - expect(connection.searchParams.get("ticket")).toBeNull() await writeProbe(page) await switchTab(page, titleB) @@ -66,7 +62,6 @@ async function readProbe(page: Page) { async function setup(page: Page) { await mockOpenCodeServer(page, { - protocol: "v2", directory, project: { id: projectID, @@ -90,33 +85,26 @@ async function setup(page: Page) { sessions: [session(sessionA, titleA, 1700000000000), session(sessionB, titleB, 1700000001000)], pageMessages: () => ({ items: [] }), }) - await page.route("**/api/pty*", (route) => + await page.route("**/pty", (route) => route.fulfill({ status: 200, contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo() }), + body: JSON.stringify({ id: ptyID, title: "Terminal 1" }), }), ) - await page.route(`**/api/pty/${ptyID}*`, (route) => + await page.route(`**/pty/${ptyID}`, (route) => + route.fulfill({ status: 200, contentType: "application/json", body: "{}" }), + ) + await page.route(`**/pty/${ptyID}/connect-token*`, (route) => route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ location: ptyLocation(), data: ptyInfo() }), - }), - ) - await page.route(`**/api/pty/${ptyID}/connect-token*`, (route) => { - expect(route.request().headers()["x-opencode-ticket"]).toBe("1") - const url = new URL(route.request().url()) - expect(url.searchParams.get("location[directory]")).toBe(directory) - return route.fulfill({ status: 200, contentType: "application/json", headers: { "access-control-allow-origin": "*" }, - body: JSON.stringify({ location: ptyLocation(), data: { ticket: "e2e-ticket", expires_in: 60 } }), - }) - }) + body: JSON.stringify({ ticket: "e2e-ticket" }), + }), + ) const connections: string[] = [] - await page.routeWebSocket(new RegExp(`/api/pty/${ptyID}/connect`), (ws) => { + await page.routeWebSocket(new RegExp(`/pty/${ptyID}/connect`), (ws) => { connections.push(ws.url()) }) @@ -155,11 +143,3 @@ function session(id: string, title: string, created: number) { function sessionHref(sessionID: string) { return `/server/${base64Encode(server)}/session/${sessionID}` } - -function ptyLocation() { - return { directory, project: { id: projectID, directory } } -} - -function ptyInfo() { - return { id: ptyID, title: "Terminal 1", command: "cmd.exe", args: [], cwd: directory, status: "running", pid: 1 } -} diff --git a/packages/app/e2e/user-story/model-selection-flow.spec.ts b/packages/app/e2e/user-story/model-selection-flow.spec.ts deleted file mode 100644 index 22b8bb41fe..0000000000 --- a/packages/app/e2e/user-story/model-selection-flow.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { expect, test } from "@playwright/test" -import { mockOpenCodeServer } from "../utils/mock-server" -import { expectAppVisible } from "../utils/waits" - -const directory = "C:/OpenCode/NewProject" - -test("creates a session in a new project, connects OpenCode Go, and selects its model", async ({ page }) => { - let connectedGo = false - let pendingGo = false - const connections: Array<{ integrationID: string; body: unknown }> = [] - - await mockOpenCodeServer(page, { - directory, - project: { - id: "proj_model_selection_flow", - worktree: directory, - vcs: "git", - name: "NewProject", - time: { created: 1_700_000_000_000, updated: 1_700_000_000_000 }, - sandboxes: [], - }, - provider: () => ({ - all: [ - { - id: "opencode", - name: "OpenCode", - models: { - "free-model": { - id: "free-model", - name: "Free Model", - cost: { input: 0, output: 0 }, - limit: { context: 200_000 }, - }, - }, - }, - { - id: "opencode-go", - name: "OpenCode Go", - models: { - "go-model-1": { - id: "go-model-1", - name: "Go Model 1", - cost: { input: 1, output: 1 }, - limit: { context: 200_000 }, - }, - }, - }, - ], - connected: connectedGo ? ["opencode", "opencode-go"] : ["opencode"], - default: { providerID: "opencode", modelID: "free-model" }, - }), - integrationMethods: { "opencode-go": [{ type: "api", label: "API key" }] }, - onConnectKey: (input) => { - connections.push(input) - if (input.integrationID === "opencode-go") pendingGo = true - }, - onInstanceDispose: () => { - if (pendingGo) connectedGo = true - }, - sessions: [], - pageMessages: () => ({ items: [] }), - fileList: (path) => - path ? [] : [{ name: "NewProject", path: "NewProject", absolute: directory, type: "directory", ignored: false }], - findFiles: () => ["NewProject"], - }) - await page.addInitScript(() => { - localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } })) - localStorage.setItem("opencode.global.dat:server", JSON.stringify({ projects: { local: [] } })) - }) - - await page.goto("/") - const addProject = page.locator('[data-action="home-add-project-row"]') - await expectAppVisible(addProject) - await addProject.click() - await page.locator("[data-directory-path]").click() - - await page.locator('[data-action="home-new-session"]').click() - await expectAppVisible(page.locator('[data-component="prompt-input-v2"]')) - - const modelControl = page.locator('[data-action="prompt-model"]') - await modelControl.click() - await expect(page.locator('[data-section="free-models"]')).toContainText("Free models provided by OpenCode") - - await page.locator('[data-provider-id="opencode-go"]').click() - await page.locator('[data-input="provider-api-key"]').fill("mock-go-api-key") - await page.locator('[data-action="provider-connect-submit"]').click() - await expect(page.locator('[data-component="dialog-v2"]')).toHaveCount(0) - expect(connections).toEqual([{ integrationID: "opencode-go", body: { type: "api", key: "mock-go-api-key" } }]) - - await expect(modelControl).toHaveAttribute("data-control-type", "popover") - await modelControl.click() - const goModel = page.locator('[data-option-key="opencode-go:go-model-1"]') - await expect(goModel).toBeVisible() - await goModel.click() - - await expect(modelControl).toContainText("Go Model 1") -}) diff --git a/packages/app/e2e/utils/mock-server.ts b/packages/app/e2e/utils/mock-server.ts index 76987421b6..2bfba5871a 100644 --- a/packages/app/e2e/utils/mock-server.ts +++ b/packages/app/e2e/utils/mock-server.ts @@ -4,11 +4,7 @@ const emptyList = new Set(["/skill", "/command", "/lsp", "/formatter", "/vcs/sta const emptyObject = new Set(["/global/config", "/config", "/provider/auth", "/mcp", "/experimental/resource"]) export interface MockServerConfig { - protocol?: "v1" | "v2" - provider: unknown | (() => unknown) - integrationMethods?: Record - onConnectKey?: (input: { integrationID: string; body: unknown }) => void - onInstanceDispose?: () => void + provider: unknown directory: string project: unknown sessions: ({ id: string } & Record)[] @@ -27,13 +23,14 @@ export interface MockServerConfig { fileList?: (path: string) => unknown | Promise fileContent?: (path: string) => unknown | Promise findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown - sessionStatus?: Record | (() => Record) + sessionStatus?: unknown } export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { const cursors = new Map() let nextCursor = 0 const staticRoutes: Record = { + "/provider": config.provider, "/path": { state: config.directory, config: config.directory, @@ -57,47 +54,19 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (url.port !== targetPort && url.port !== appPort) return route.fallback() const path = url.pathname - if (path === "/global/event" || path === "/event" || path === "/api/event") { - const events = config.events?.() - return sse( - route, - path === "/api/event" - ? [{ id: "evt_mock_connected", type: "server.connected", data: {} }, ...(events?.map(currentEvent) ?? [])] - : [ - ...(path === "/global/event" - ? [{ payload: { id: "evt_mock_connected", type: "server.connected", properties: {} } }] - : []), - ...(events ?? []), - ], - config.eventRetry, - ) - } - if (path === "/global/health") - return config.protocol === "v2" ? json(route, {}, undefined, 404) : json(route, { healthy: true }) - if (path === "/api/health" && config.protocol === "v2") - return json(route, { healthy: true, version: "2.0.0", pid: 1 }) - if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: true }) - if (path === "/provider") - return json(route, typeof config.provider === "function" ? config.provider() : config.provider) - if (path === "/provider/auth") return json(route, config.integrationMethods ?? {}) - const legacyAuth = path.match(/^\/auth\/([^/]+)$/)?.[1] - if (legacyAuth && route.request().method() === "PUT") { - config.onConnectKey?.({ integrationID: legacyAuth, body: route.request().postDataJSON() }) - return json(route, true) - } - if (path === "/instance/dispose" && route.request().method() === "POST") { - config.onInstanceDispose?.() - return json(route, true) - } + if (path === "/global/event" || path === "/event") return sse(route, config.events?.(), config.eventRetry) + if (path === "/global/health") return json(route, { healthy: true }) + if (path === "/api/session") + return json(route, { + data: config.sessions.map((session) => v2Session(session, config.directory)), + cursor: {}, + }) + if (path === "/experimental/capabilities") return json(route, { backgroundSubagents: false }) if (path === "/permission") return json(route, typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])) if (path === "/question") return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? [])) - if (path === "/session/status") - return json( - route, - typeof config.sessionStatus === "function" ? config.sessionStatus() : (config.sessionStatus ?? {}), - ) + if (path === "/session/status") return json(route, config.sessionStatus ?? {}) if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff) if (path === "/file" && config.fileList) return json(route, await config.fileList(url.searchParams.get("path") ?? "")) @@ -120,138 +89,10 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }, data: [], }) - if (path === "/api/agent") - return json(route, { - location: location(config), - data: [ - { - id: "build", - name: "Build", - mode: "primary", - hidden: false, - request: { settings: {}, headers: {}, body: {} }, - permissions: [], - }, - ], - }) - if (path === "/api/command") return json(route, { location: location(config), data: [] }) - if (path === "/api/mcp") return json(route, { location: location(config), data: [] }) - if (path === "/api/mcp/resource") - return json(route, { location: location(config), data: { resources: [], templates: [] } }) - const integration = path.match(/^\/api\/integration\/([^/]+)$/)?.[1] - if (integration && route.request().method() === "GET") - return json(route, { - location: location(config), - data: { id: integration, name: integration, methods: [{ type: "key", label: "API key" }], connections: [] }, - }) - const integrationConnect = path.match(/^\/api\/integration\/([^/]+)\/connect\/key$/)?.[1] - if (integrationConnect && route.request().method() === "POST") { - config.onConnectKey?.({ integrationID: integrationConnect, body: route.request().postDataJSON() }) - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (path === "/api/project") return json(route, [config.project]) - if (path === "/api/project/current") - return json(route, { id: (config.project as { id?: string }).id, directory: config.directory }) - if (path.startsWith("/api/project/") && route.request().method() === "PATCH") return json(route, config.project) - if (path === "/api/path") - return json(route, { - state: config.directory, - config: config.directory, - worktree: config.directory, - directory: config.directory, - home: "C:/OpenCode", - }) - if (path === "/api/permission/request") - return json(route, { - location: location(config), - data: (typeof config.permissions === "function" ? config.permissions() : (config.permissions ?? [])).map( - currentPermission, - ), - }) - if (path === "/api/question/request") - return json(route, { - location: location(config), - data: typeof config.questions === "function" ? config.questions() : (config.questions ?? []), - }) - if (path === "/api/vcs") - return json(route, { location: location(config), data: { branch: "main", defaultBranch: "main" } }) - if (path === "/api/vcs/status") return json(route, { location: location(config), data: [] }) - if (path === "/api/vcs/diff") return json(route, { location: location(config), data: config.vcsDiff ?? [] }) - if (path === "/api/pty/shells") return json(route, { location: location(config), data: [] }) - if (/^\/api\/pty\/[^/]+\/connect-token$/.test(path)) - return json(route, { location: location(config), data: { ticket: "e2e-ticket", expires_in: 60 } }) if (emptyObject.has(path)) return json(route, {}) if (emptyList.has(path)) return json(route, []) - if (path === "/api/session") { - const directory = url.searchParams.get("directory") - const parentID = url.searchParams.get("parentID") - const limit = Number(url.searchParams.get("limit") ?? 50) - const offset = Number(url.searchParams.get("cursor") ?? 0) - const sessions = config.sessions - .filter((session) => !directory || session.directory === directory) - .filter((session) => parentID !== "null" || session.parentID === undefined) - .filter((session) => { - const search = url.searchParams.get("search")?.toLowerCase() - return ( - !search || - String(session.title ?? "") - .toLowerCase() - .includes(search) - ) - }) - const ordered = url.searchParams.get("order") === "asc" ? sessions.toReversed() : sessions - const data = ordered.slice(offset, offset + limit) - const next = offset + limit < ordered.length ? String(offset + limit) : undefined - return json(route, { - data: data.map((session) => currentSession(session, config.directory)), - cursor: { next }, - }) - } - if (path === "/api/session/active") { - const statuses = (config.sessionStatus ?? {}) as Record - return json(route, { - data: Object.fromEntries( - Object.entries(statuses).flatMap(([id, status]) => - status.type === "idle" ? [] : [[id, { type: "running" }]], - ), - ), - }) - } - if (/^\/api\/session\/[^/]+\/shell$/.test(path) && route.request().method() === "POST") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/api\/session\/[^/]+\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") { - return json(route, true) - } - if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST") { - return json(route, true) - } - if ( - /^\/api\/session\/[^/]+\/(archive|rename|interrupt|revert\/clear|revert\/commit)$/.test(path) && - route.request().method() === "POST" - ) { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } - if (/^\/api\/session\/[^/]+$/.test(path) && route.request().method() === "DELETE") { - return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } }) - } if (path in staticRoutes) return json(route, staticRoutes[path]) - const currentSessionMatch = path.match(/^\/api\/session\/([^/]+)$/) - if (currentSessionMatch) { - const session = config.sessions.find((item) => item.id === currentSessionMatch[1]) - if (!session) return json(route, { error: "Session not found" }, undefined, 404) - return json(route, { - data: currentSession(session, config.directory), - }) - } - const sessionMatch = path.match(/^\/session\/([^/]+)$/) if (sessionMatch) { const session = config.sessions.find((s) => s.id === sessionMatch[1]) @@ -274,24 +115,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? []) if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, []) - const currentMessagesMatch = path.match(/^\/api\/session\/([^/]+)\/message$/) - if (currentMessagesMatch) { - const token = url.searchParams.get("cursor") ?? undefined - const before = token ? cursors.get(token) : undefined - if (token && !before) return json(route, { error: "Invalid cursor" }, undefined, 400) - config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "start" }) - await config.beforeMessagesResponse?.({ sessionID: currentMessagesMatch[1]!, before }) - if (config.messageDelay !== undefined) await new Promise((resolve) => setTimeout(resolve, config.messageDelay)) - const pageData = config.pageMessages(currentMessagesMatch[1], Number(url.searchParams.get("limit") ?? 50), before) - config.onMessages?.({ sessionID: currentMessagesMatch[1], before, phase: "end" }) - const cursor = pageData.cursor ? `cursor_${++nextCursor}` : undefined - if (cursor) cursors.set(cursor, pageData.cursor!) - return json(route, { - data: pageData.items.map(currentMessage).reverse(), - cursor: { next: cursor }, - }) - } - const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/) if (messagesMatch) { const token = url.searchParams.get("before") ?? undefined @@ -314,37 +137,12 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) { }) } -function location(config: MockServerConfig) { - return { - directory: config.directory, - project: { id: (config.project as { id?: string }).id, directory: config.directory }, - } -} - -function currentPermission(value: unknown) { - const permission = value as Record - if (permission.action) return permission - const tool = permission.tool as { messageID?: string; callID?: string } | undefined - return { - id: permission.id, - sessionID: permission.sessionID, - action: permission.permission, - resources: permission.patterns ?? [], - save: permission.always, - metadata: permission.metadata, - source: - tool?.messageID && tool.callID ? { type: "tool", messageID: tool.messageID, callID: tool.callID } : undefined, - } -} - -export function currentSession(session: { id: string } & Record, fallbackDirectory?: string) { +function v2Session(session: { id: string } & Record, fallbackDirectory: string) { const time = session.time && typeof session.time === "object" ? session.time : {} return { id: session.id, parentID: session.parentID, projectID: session.projectID ?? "project", - agent: session.agent ?? "build", - model: session.model ?? { id: "mock-model", providerID: "mock-provider" }, cost: session.cost ?? 0, tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { @@ -359,67 +157,7 @@ export function currentSession(session: { id: string } & Record directory: typeof session.directory === "string" ? session.directory : fallbackDirectory, ...(typeof session.workspaceID === "string" ? { workspaceID: session.workspaceID } : {}), }, - subpath: session.path, - revert: session.revert, - } -} - -function currentMessage(value: unknown) { - const item = value as { - info: Record & { id: string; role: "user" | "assistant"; time: { created: number } } - parts: Array & { type: string }> - } - if (item.info.role === "user") { - return { - id: item.info.id, - type: "user", - time: item.info.time, - text: item.parts - .flatMap((part) => (part.type === "text" && typeof part.text === "string" ? [part.text] : [])) - .join("\n"), - } - } - return { - id: item.info.id, - type: "assistant", - time: item.info.time, - agent: item.info.agent ?? "build", - model: { id: item.info.modelID ?? "model", providerID: item.info.providerID ?? "provider" }, - cost: item.info.cost, - tokens: item.info.tokens, - error: item.info.error, - content: item.parts.flatMap((part) => { - if (part.type === "text" || part.type === "reasoning") return [{ type: part.type, text: part.text ?? "" }] - if (part.type !== "tool") return [] - const state = part.state as Record - return [ - { - type: "tool", - id: part.id, - name: part.tool, - time: state.time ?? { created: item.info.time.created }, - state: - state.status === "pending" - ? { status: "streaming", input: state.raw ?? JSON.stringify(state.input ?? {}) } - : state.status === "completed" - ? { - status: "completed", - input: state.input ?? {}, - structured: state.metadata ?? {}, - content: [{ type: "text", text: state.output ?? "" }], - } - : state.status === "error" - ? { - status: "error", - input: state.input ?? {}, - structured: state.metadata ?? {}, - content: [], - error: { type: "ToolError", message: state.error ?? "Tool failed" }, - } - : { status: "running", input: state.input ?? {}, structured: state.metadata ?? {}, content: [] }, - }, - ] - }), + ...(typeof session.path === "string" ? { subpath: session.path } : {}), } } @@ -443,18 +181,3 @@ function sse(route: Route, events?: unknown[], retry?: number) { body: `${retry === undefined ? "" : `retry: ${retry}\n\n`}${events?.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("") || ": ok\n\n"}`, }) } - -function currentEvent(input: unknown) { - if (!input || typeof input !== "object" || !("payload" in input)) return input - const envelope = input as { directory?: string; payload?: unknown } - if (!envelope.payload || typeof envelope.payload !== "object") return input - const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } - if (!payload.type) return input - return { - id: payload.id ?? `evt_mock_${Date.now()}`, - created: Date.now(), - type: payload.type, - data: payload.properties ?? {}, - location: envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, - } -} diff --git a/packages/app/e2e/utils/sse-transport.ts b/packages/app/e2e/utils/sse-transport.ts index b0e3b74c6d..55420485f3 100644 --- a/packages/app/e2e/utils/sse-transport.ts +++ b/packages/app/e2e/utils/sse-transport.ts @@ -3,7 +3,7 @@ import type { Page } from "@playwright/test" export type SseConnectionRecord = { id: number url: string - path: "/global/event" | "/event" | "/api/event" + path: "/global/event" | "/event" headers: Record openedAt: number endedAt?: number @@ -93,21 +93,6 @@ export async function installSseTransport( eventOptions.retry === undefined ? "" : `retry: ${eventOptions.retry}\n`, `data: ${JSON.stringify(payload)}\n\n`, ].join("") - const currentEvent = (input: unknown) => { - if (!input || typeof input !== "object" || !("payload" in input)) return input - const envelope = input as { directory?: string; payload?: unknown } - if (!envelope.payload || typeof envelope.payload !== "object") return input - const payload = envelope.payload as { id?: string; type?: string; properties?: unknown } - if (!payload.type) return input - return { - id: payload.id ?? `evt_mock_${Date.now()}`, - created: Date.now(), - type: payload.type, - data: payload.properties ?? {}, - location: - envelope.directory && envelope.directory !== "global" ? { directory: envelope.directory } : undefined, - } - } const acknowledge = ( connection: Connection, bytes: number, @@ -155,13 +140,15 @@ export async function installSseTransport( output.forEach((chunk) => connection.controller.enqueue(chunk)) return acknowledge(connection, input.bytes.length, output.length) } - const encoded = input.deliveries.map((delivery) => { - const payload = connection.path === "/api/event" ? currentEvent(delivery.payload) : delivery.payload - return { delivery, payload, bytes: encoder.encode(frame(payload, delivery.options)) } - }) + const encoded = input.deliveries.map((delivery) => ({ + delivery, + bytes: encoder.encode(frame(delivery.payload, delivery.options)), + })) encoded.forEach((item) => marker(item.delivery.options?.marker)) if (input.burst) { - const bytes = encoder.encode(encoded.map((item) => frame(item.payload, item.delivery.options)).join("")) + const bytes = encoder.encode( + encoded.map((item) => frame(item.delivery.payload, item.delivery.options)).join(""), + ) connection.controller.enqueue(bytes) return encoded.map((item) => acknowledge(connection, item.bytes.byteLength, 1, item.delivery.options?.id)) } @@ -174,10 +161,7 @@ export async function installSseTransport( const fetch = (input: RequestInfo | URL, init?: RequestInit) => { const request = new Request(input, init) const url = new URL(request.url) - if ( - url.origin !== server || - (url.pathname !== "/global/event" && url.pathname !== "/event" && url.pathname !== "/api/event") - ) + if (url.origin !== server || (url.pathname !== "/global/event" && url.pathname !== "/event")) return originalFetch(request) const id = ++nextConnectionID @@ -193,18 +177,6 @@ export async function installSseTransport( record.controller = controller connections.push(record) if (retry !== undefined) controller.enqueue(encoder.encode(`retry: ${retry}\n\n`)) - if (url.pathname === "/api/event") - controller.enqueue( - encoder.encode(frame({ id: `evt_mock_connected_${id}`, type: "server.connected", data: {} })), - ) - if (url.pathname === "/global/event") - controller.enqueue( - encoder.encode( - frame({ - payload: { id: `evt_mock_connected_${id}`, type: "server.connected", properties: {} }, - }), - ), - ) request.signal.addEventListener( "abort", () => { diff --git a/packages/app/package.json b/packages/app/package.json index 2dd4a05d86..482fe7ff85 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.11", + "version": "1.18.3", "description": "", "type": "module", "exports": { @@ -19,9 +19,9 @@ "build": "vite build", "serve": "vite preview", "test": "bun run test:unit && bun run test:browser", - "test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src", + "test:unit": "bun test --only-failures --preload ./happydom.ts ./src", "test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser", - "test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src", + "test:unit:watch": "bun test --watch --preload ./happydom.ts ./src", "test:e2e": "playwright test", "test:e2e:local": "playwright test", "test:e2e:ui": "playwright test --ui", @@ -53,7 +53,6 @@ "@dnd-kit/helpers": "0.5.0", "@dnd-kit/solid": "0.5.0", "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", "@opencode-ai/core": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", @@ -82,7 +81,7 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", + "ghostty-web": "github:anomalyco/ghostty-web#513463a6f1190253057e8a3f0dac8f6ee8393553", "luxon": "catalog:", "marked": "catalog:", "marked-shiki": "catalog:", diff --git a/packages/app/src/addons/serialize.test.ts b/packages/app/src/addons/serialize.test.ts index d45af34c43..6828e60f84 100644 --- a/packages/app/src/addons/serialize.test.ts +++ b/packages/app/src/addons/serialize.test.ts @@ -37,14 +37,6 @@ function writeAndWait(term: Terminal, data: string): Promise { } describe("SerializeAddon", () => { - test("preserves color scheme reporting mode", async () => { - const { term, addon } = createTerminal() - await writeAndWait(term, "\x1b[?2031h") - - expect(addon.serialize().startsWith("\x1b[?2031h")).toBe(true) - expect(addon.serialize({ excludeModes: true }).startsWith("\x1b[?2031h")).toBe(false) - }) - describe("ANSI color preservation", () => { test("should preserve text attributes (bold, italic, underline)", async () => { const { term, addon } = createTerminal() diff --git a/packages/app/src/addons/serialize.ts b/packages/app/src/addons/serialize.ts index 515153488c..3823fb443a 100644 --- a/packages/app/src/addons/serialize.ts +++ b/packages/app/src/addons/serialize.ts @@ -89,13 +89,6 @@ const getTerminalBuffers = (value: ITerminalCore): TerminalBuffers | undefined = return { active, normal, alternate } } -const getTerminalMode = (value: ITerminalCore, mode: number) => { - if (!isRecord(value)) return false - const terminal = value.wasmTerm - if (!isRecord(terminal) || typeof terminal.getMode !== "function") return false - return terminal.getMode(mode) === true -} - // ============================================================================ // Types // ============================================================================ @@ -551,8 +544,7 @@ export class SerializeAddon implements ITerminalAddon { return "" } - let content = !options?.excludeModes && getTerminalMode(this._terminal, 2031) ? "\u001b[?2031h" : "" - content += options?.range + let content = options?.range ? this._serializeBufferByRange(normalBuffer, options.range, true) : this._serializeBufferByScrollback(normalBuffer, options?.scrollback) diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx index f47c432e42..b4496ba7de 100644 --- a/packages/app/src/app.tsx +++ b/packages/app/src/app.tsx @@ -67,8 +67,7 @@ import { legacySessionHref, legacySessionServer, requireServerKey, sessionHref } import { createSessionLineage } from "@/pages/session/session-lineage" import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session" -import { NewHome } from "@/pages/home" -import { LegacyHome } from "@/pages/home/legacy-home" +import { NewHome, LegacyHome } from "@/pages/home" const NewSession = lazy(() => import("@/pages/new-session")) @@ -237,30 +236,6 @@ function UiI18nBridge(props: ParentProps) { return {props.children} } -function LayoutCompatibility(props: ParentProps) { - const global = useGlobal() - const navigate = useNavigate() - const server = useServer() - const settings = useSettings() - - createEffect(() => { - if (settings.general.newLayoutDesigns()) return - const current = server.current - if (!current) return - const protocol = global.ensureServerCtx(current).sdk.protocolKind() - if (protocol !== "v2") return - const next = global.servers.list().find((s) => { - if (ServerConnection.key(s) === ServerConnection.key(current)) return false - return global.ensureServerCtx(s).sdk.protocolKind() !== "v2" - }) - if (!next) return - navigate("/") - queueMicrotask(() => server.setActive(ServerConnection.key(next))) - }) - - return <>{props.children} -} - declare global { interface Window { __OPENCODE__?: { diff --git a/packages/app/src/components/command-palette.ts b/packages/app/src/components/command-palette.ts index 8014ea1c43..59d3cbd1da 100644 --- a/packages/app/src/components/command-palette.ts +++ b/packages/app/src/components/command-palette.ts @@ -1,6 +1,5 @@ import { getFilename } from "@opencode-ai/core/util/path" -import type { Project } from "@opencode-ai/sdk/v2/client" -import type { SessionInfo } from "@opencode-ai/client/promise" +import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client" import { useDialog } from "@opencode-ai/ui/context/dialog" import { createMemo, onCleanup } from "solid-js" import { commandPaletteOptions, useCommand, type CommandOption } from "@/context/command" @@ -14,7 +13,6 @@ import { useTabs } from "@/context/tabs" import { displayName, projectForSession } from "@/pages/layout/helpers" import { createSessionTabs } from "@/pages/session/helpers" import { useSessionLayout } from "@/pages/session/session-layout" -import { normalizeSessionInfo } from "@/utils/session" export type CommandPaletteEntry = { id: string @@ -146,7 +144,8 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on server: ServerConnection.key(serverSDK.server), opened: serverCtx.projects.list, stored: () => serverCtx.sync.data.project, - load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }), + load: (search, signal) => + serverSDK.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }), untitled: () => language.t("command.session.new"), category: () => language.t("command.category.session"), }) @@ -220,7 +219,7 @@ export function createServerSessionEntries(props: { server: ServerConnection.Key opened: () => LocalProject[] stored: () => Project[] - load: (search: string, signal: AbortSignal) => Promise<{ data: SessionInfo[] }> + load: (search: string, signal: AbortSignal) => Promise<{ data?: GlobalSession[] }> untitled: () => string category: () => string }) { @@ -256,8 +255,7 @@ export function createServerSessionEntries(props: { return props .load(search, current.signal) .then((result) => - result.data - .map(normalizeSessionInfo) + (result.data ?? []) .filter((session) => !session.time.archived) .map((session) => { const project = @@ -266,7 +264,7 @@ export function createServerSessionEntries(props: { id: `session:${props.server}:${session.id}`, type: "session" as const, title: session.title || props.untitled(), - description: project ? displayName(project) : getFilename(session.directory), + description: project ? displayName(project) : session.project?.name || getFilename(session.directory), category: props.category(), directory: session.directory, sessionID: session.id, diff --git a/packages/app/src/components/debug-bar.tsx b/packages/app/src/components/debug-bar.tsx index adadeda303..67c9a89929 100644 --- a/packages/app/src/components/debug-bar.tsx +++ b/packages/app/src/components/debug-bar.tsx @@ -66,7 +66,7 @@ function Cell(props: {
-
- {props.label} -
-
- {props.value} -
+ {props.label} +
+
+ {props.value}
) @@ -124,25 +116,15 @@ function FocusCell(props: { active: boolean; inline?: boolean; onClick: () => vo aria-pressed={props.active} classList={{ "flex min-w-0 items-center font-mono uppercase hover:bg-surface-raised-base focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-border-focus": true, - "min-h-[20px] w-fit justify-start rounded px-1.5 py-0.5 text-left": !!props.inline, + "min-h-[20px] w-fit flex-row justify-start gap-1.5 rounded px-1.5 py-0.5 text-left": !!props.inline, "min-h-[42px] w-full flex-col justify-center rounded-[8px] px-0.5 py-1 text-center": !props.inline, "bg-surface-raised-base text-text-strong": props.active, }} onClick={props.onClick} > - - FOCUS - - {props.active ? "ON" : "OFF"} - + FOCUS + + {props.active ? "ON" : "OFF"} ) diff --git a/packages/app/src/components/dialog-command-palette-v2.tsx b/packages/app/src/components/dialog-command-palette-v2.tsx index e996fd0be7..85ca44ae69 100644 --- a/packages/app/src/components/dialog-command-palette-v2.tsx +++ b/packages/app/src/components/dialog-command-palette-v2.tsx @@ -79,7 +79,8 @@ export function DialogHomeCommandPaletteV2(props: { server: ServerConnection.key(props.server), opened: serverCtx.projects.list, stored: () => serverCtx.sync.data.project, - load: (search, signal) => serverCtx.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }), + load: (search, signal) => + serverCtx.sdk.client.experimental.session.list({ roots: true, search, limit: 50 }, { signal }), untitled: () => language.t("command.session.new"), category: () => language.t("command.category.session"), }) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 0267b4b4ef..4c58857249 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -1,4 +1,4 @@ -import type { IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode-ai/client/promise" +import type { ProviderAuthAuthorization, ProviderAuthMethod } from "@opencode-ai/sdk/v2/client" import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" @@ -28,18 +28,15 @@ import { Switch, } from "solid-js" import { createStore, produce } from "solid-js/store" -import { useParams } from "@solidjs/router" -import { ExternalLink } from "@/components/external-link" +import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" import { useSettings } from "@/context/settings" import { popularProviders, useProviders } from "@/hooks/use-providers" import { CustomProviderForm } from "./dialog-custom-provider" -import { decode64 } from "@/utils/base64" const CUSTOM_ID = "_custom" -type ConnectMethod = Extract export function useProviderConnectController(options: { onBack?: () => void } = {}) { const [store, setStore] = createStore({ selected: undefined as string | undefined }) @@ -157,7 +154,7 @@ function ProviderPicker(props: { const settings = useSettings() if (settings.general.newLayoutDesigns()) return - const providers = useProviders(() => props.directory?.()) + const providers = useProviders(props.directory) const language = useLanguage() const popularGroup = () => language.t("dialog.provider.group.popular") const otherGroup = () => language.t("dialog.provider.group.other") @@ -229,8 +226,10 @@ function ProviderPickerV2(props: { onSelect: (provider: string) => void onPrepare?: () => void }) { - const providers = useProviders(() => props.directory?.()) + const providers = useProviders(props.directory) const language = useLanguage() + const serverSync = useServerSync() + const serverSDK = useServerSDK() const [store, setStore] = createStore({ filter: "", active: undefined as string | undefined, @@ -267,7 +266,19 @@ function ProviderPickerV2(props: { const connect = (provider: string) => { props.onPrepare?.() - props.onSelect(provider) + if (provider === CUSTOM_ID || serverSync().data.provider_auth[provider]) { + props.onSelect(provider) + return + } + if (store.connecting) return + setStore("connecting", provider) + void serverSDK() + .client.provider.auth() + .then((response) => { + serverSync().set("provider_auth", response.data ?? {}) + props.onSelect(provider) + }) + .catch(() => props.onSelect(provider)) } const move = (event: KeyboardEvent, direction: number) => { @@ -384,16 +395,10 @@ function ProviderConnection(props: { const dialog = useDialog() const serverSync = useServerSync() const serverSDK = useServerSDK() - const params = useParams() const language = useLanguage() const settings = useSettings() const newLayout = settings.general.newLayoutDesigns - const providers = useProviders(() => props.directory?.()) - const directory = () => props.directory?.() ?? decode64(params.dir) - const location = () => { - const value = directory() - return value ? { directory: value } : undefined - } + const providers = useProviders(props.directory) const alive = { value: true } const timer = { current: undefined as ReturnType | undefined } @@ -408,34 +413,38 @@ function ProviderConnection(props: { const provider = createMemo( () => providers.all().get(props.provider) ?? serverSync().data.provider.all.get(props.provider)!, ) - const fallback = createMemo(() => [ + const fallback = createMemo(() => [ { - type: "key" as const, + type: "api" as const, label: language.t("provider.connect.method.apiKey"), }, ]) - const [integration] = createResource( - () => ({ provider: props.provider, directory: directory() }), - (input) => - serverSDK() - .api.integration.get({ - integrationID: input.provider, - location: input.directory ? { directory: input.directory } : undefined, - }) - .then((result) => result.data), + const [auth] = createResource( + () => props.provider, + async () => { + const cached = serverSync().data.provider_auth[props.provider] + if (cached) return cached + const res = await serverSDK().client.provider.auth() + if (!alive.value) return fallback() + serverSync().set("provider_auth", res.data ?? {}) + return res.data?.[props.provider] ?? fallback() + }, ) - const loading = createMemo(() => integration.loading) - const methods = createMemo(() => { - const values = integration.latest?.methods.filter( - (method): method is ConnectMethod => method.type === "key" || method.type === "oauth", - ) - return values?.length ? values : fallback() - }) + const loading = createMemo(() => auth.loading && !serverSync().data.provider_auth[props.provider]) + const methods = createMemo(() => auth.latest ?? serverSync().data.provider_auth[props.provider] ?? fallback()) + const cachedMethods = serverSync().data.provider_auth[props.provider] + const directMethod = + cachedMethods?.length === 1 && cachedMethods[0].type === "api" && !cachedMethods[0].prompts?.length ? 0 : undefined const [store, setStore] = createStore({ - methodIndex: undefined as undefined | number, - authorization: undefined as undefined | IntegrationOauthConnectOutput["data"], + methodIndex: directMethod as undefined | number, + authorization: undefined as undefined | ProviderAuthAuthorization, promptInputs: undefined as undefined | Record, - state: "pending" as undefined | "pending" | "complete" | "error" | "prompt", + state: (directMethod === undefined ? "pending" : undefined) as + | undefined + | "pending" + | "complete" + | "error" + | "prompt", error: undefined as string | undefined, }) @@ -445,7 +454,7 @@ function ProviderConnection(props: { | { type: "auth.prompt" } | { type: "auth.inputs"; inputs: Record } | { type: "auth.pending" } - | { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] } + | { type: "auth.complete"; authorization: ProviderAuthAuthorization } | { type: "auth.error"; error: string } function dispatch(action: Action) { @@ -499,7 +508,7 @@ function ProviderConnection(props: { const methodLabel = (value?: { type?: string; label?: string }) => { if (!value) return "" - if (value.type === "key") return language.t("provider.connect.method.apiKey") + if (value.type === "api") return language.t("provider.connect.method.apiKey") return value.label ?? "" } @@ -509,7 +518,7 @@ function ProviderConnection(props: { const hint = suffix?.[1] return { label: suffix ? label.slice(0, -suffix[0].length) : label, - hint: hint ? hint[0].toUpperCase() + hint.slice(1) : value?.type === "key" ? "Browser" : undefined, + hint: hint ? hint[0].toUpperCase() + hint.slice(1) : value?.type === "api" ? "Browser" : undefined, } } @@ -540,22 +549,46 @@ function ProviderConnection(props: { const method = methods()[index] dispatch({ type: "method.select", index }) + if (method.type === "api" && method.prompts?.length) { + if (!inputs) { + dispatch({ type: "auth.prompt" }) + return + } + dispatch({ type: "auth.inputs", inputs }) + return + } + if (method.type === "oauth") { if (method.prompts?.length && !inputs) { dispatch({ type: "auth.prompt" }) return } dispatch({ type: "auth.pending" }) + const start = Date.now() await serverSDK() - .api.integration.oauth.connect({ - integrationID: props.provider, - methodID: method.id, - inputs: inputs ?? {}, - location: location(), - }) + .client.provider.oauth.authorize( + { + providerID: props.provider, + method: index, + inputs, + }, + { throwOnError: true }, + ) .then((x) => { if (!alive.value) return - dispatch({ type: "auth.complete", authorization: x.data }) + const elapsed = Date.now() - start + const delay = 1000 - elapsed + + if (delay > 0) { + if (timer.current !== undefined) clearTimeout(timer.current) + timer.current = setTimeout(() => { + timer.current = undefined + if (!alive.value) return + dispatch({ type: "auth.complete", authorization: x.data! }) + }, delay) + return + } + dispatch({ type: "auth.complete", authorization: x.data! }) }) .catch((e) => { if (!alive.value) return @@ -570,9 +603,9 @@ function ProviderConnection(props: { index: 0, }) - const prompts = createMemo(() => { + const prompts = createMemo>(() => { const value = method() - return value?.type === "oauth" ? (value.prompts ?? []) : [] + return value?.prompts ?? [] }) const matches = (prompt: NonNullable[number]>, value: Record) => { if (!prompt.when) return true @@ -603,6 +636,10 @@ function ProviderConnection(props: { setFormStore("index", next) return } + if (method()?.type === "api") { + dispatch({ type: "auth.inputs", inputs: value }) + return + } await selectMethod(store.methodIndex, value) } @@ -704,9 +741,7 @@ function ProviderConnection(props: { }) async function complete() { - await serverSync() - .refreshProviders() - .catch(() => undefined) + await serverSDK().client.global.dispose() dialog.close() showToast({ variant: "success", @@ -770,7 +805,7 @@ function ProviderConnection(props: { listRef = ref }} items={methods} - key={(m) => m?.label ?? m?.type} + key={(m) => m?.label} onSelect={async (selected, index) => { if (!selected) return void selectMethod(index) @@ -816,10 +851,13 @@ function ProviderConnection(props: { } setFormStore("error", undefined) - await serverSDK().api.integration.connect.key({ - integrationID: props.provider, - location: location(), - key: apiKey, + await serverSDK().client.auth.set({ + providerID: props.provider, + auth: { + type: "api", + key: apiKey, + ...(store.promptInputs ? { metadata: store.promptInputs } : {}), + }, }) await complete() } @@ -836,12 +874,12 @@ function ProviderConnection(props: {
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} - {language.t("provider.connect.opencodeZen.visit.link")} - + {language.t("provider.connect.opencodeZen.visit.suffix")}
@@ -853,7 +891,6 @@ function ProviderConnection(props: { ref={apiKey} class="!w-full" name="apiKey" - data-input="provider-api-key" placeholder={language.t("provider.connect.apiKey.placeholder")} value={formStore.value} invalid={formStore.error !== undefined} @@ -870,7 +907,7 @@ function ProviderConnection(props: { )} - + {language.t("common.continue")} @@ -886,9 +923,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.opencodeZen.line2")}
{language.t("provider.connect.opencodeZen.visit.prefix")} - + {language.t("provider.connect.opencodeZen.visit.link")} - + {language.t("provider.connect.opencodeZen.visit.suffix")}
@@ -947,13 +984,12 @@ function ProviderConnection(props: { setFormStore("error", undefined) const result = await serverSDK() - .api.integration.oauth.complete({ - integrationID: props.provider, - attemptID: store.authorization!.attemptID, - location: location(), + .client.provider.oauth.callback({ + providerID: props.provider, + method: store.methodIndex, code, }) - .then(() => ({ ok: true as const })) + .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) .catch((error) => ({ ok: false as const, error })) if (result.ok) { await complete() @@ -967,9 +1003,9 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.code.visit.prefix")} - + {language.t("provider.connect.oauth.code.visit.link")} - + {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
@@ -1006,9 +1042,7 @@ function ProviderConnection(props: {
{language.t("provider.connect.oauth.code.visit.prefix")} - - {language.t("provider.connect.oauth.code.visit.link")} - + {language.t("provider.connect.oauth.code.visit.link")} {language.t("provider.connect.oauth.code.visit.suffix", { provider: provider().name })}
@@ -1042,46 +1076,32 @@ function ProviderConnection(props: { }) onMount(() => { - const poll = async () => { - const authorization = store.authorization - if (!authorization || !alive.value) return + void (async () => { const result = await serverSDK() - .api.integration.oauth.status({ - integrationID: props.provider, - attemptID: authorization.attemptID, - location: location(), + .client.provider.oauth.callback({ + providerID: props.provider, + method: store.methodIndex, }) - .then((value) => ({ ok: true as const, status: value.data })) + .then((value) => (value.error ? { ok: false as const, error: value.error } : { ok: true as const })) .catch((error) => ({ ok: false as const, error })) + if (!alive.value) return + if (!result.ok) { - dispatch({ type: "auth.error", error: formatError(result.error, language.t("common.requestFailed")) }) + const message = formatError(result.error, language.t("common.requestFailed")) + dispatch({ type: "auth.error", error: message }) return } - if (result.status.status === "complete") { - await complete() - return - } - if (result.status.status === "failed") { - dispatch({ type: "auth.error", error: result.status.message }) - return - } - if (result.status.status === "expired") { - dispatch({ type: "auth.error", error: language.t("common.requestFailed") }) - return - } - timer.current = setTimeout(poll, 1_000) - } - void poll() + + await complete() + })() }) return (
{language.t("provider.connect.oauth.auto.visit.prefix")} - - {language.t("provider.connect.oauth.auto.visit.link")} - + {language.t("provider.connect.oauth.auto.visit.link")} {language.t("provider.connect.oauth.auto.visit.suffix", { provider: provider().name })}
- + - + - + diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index e34e4c39b8..9e04cd83ad 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -8,7 +8,7 @@ import { TextField } from "@opencode-ai/ui/text-field" import { showToast } from "@/utils/toast" import { batch, For } from "solid-js" import { createStore, produce } from "solid-js/store" -import { ExternalLink } from "@/components/external-link" +import { Link } from "@/components/link" import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { useLanguage } from "@/context/language" @@ -131,7 +131,6 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) { const saveMutation = useMutation(() => ({ mutationFn: async (result: NonNullable>) => { - if ((await serverSDK().protocol) !== "v1") throw new Error("Custom providers are unavailable on this server") const disabledProviders = serverSync().data.config.disabled_providers ?? [] const nextDisabled = disabledProviders.filter((id) => id !== result.providerID) @@ -185,9 +184,9 @@ export function CustomProviderForm(props: { autofocus?: boolean } = {}) {

{language.t("provider.custom.description.prefix")} - + {language.t("provider.custom.description.link")} - + {language.t("provider.custom.description.suffix")}

diff --git a/packages/app/src/components/dialog-fork.tsx b/packages/app/src/components/dialog-fork.tsx index 5187d980ea..601f03084c 100644 --- a/packages/app/src/components/dialog-fork.tsx +++ b/packages/app/src/components/dialog-fork.tsx @@ -69,11 +69,15 @@ export const DialogFork: Component = () => { const dir = base64Encode(sdk().directory) sdk() - .api.session.fork({ sessionID, messageID: item.id }) + .client.session.fork({ sessionID, messageID: item.id }) .then((forked) => { + if (!forked.data) { + showToast({ title: language.t("common.requestFailed") }) + return + } dialog.close() - prompt.set(restored, undefined, { dir, id: forked.id }) - navigate(`/${dir}/session/${forked.id}`) + prompt.set(restored, undefined, { dir, id: forked.data.id }) + navigate(`/${dir}/session/${forked.data.id}`) }) .catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err) diff --git a/packages/app/src/components/dialog-select-directory-v2.tsx b/packages/app/src/components/dialog-select-directory-v2.tsx index f3376ad35f..69d46ddcb6 100644 --- a/packages/app/src/components/dialog-select-directory-v2.tsx +++ b/packages/app/src/components/dialog-select-directory-v2.tsx @@ -8,7 +8,6 @@ import { createEffect, createMemo, createResource, createSignal, For, onCleanup, import { useGlobal } from "@/context/global" import { useLanguage } from "@/context/language" import { ServerConnection } from "@/context/server" -import type { Path } from "@opencode-ai/sdk/v2/client" import { absoluteTreePath, activeTreeNavigation, @@ -29,7 +28,6 @@ import { } from "./directory-picker-domain" import "./dialog-select-directory-v2.css" import { DividerV2 } from "@opencode-ai/ui/v2/divider-v2" -import { getFilename } from "@opencode-ai/core/util/path" interface DialogSelectDirectoryV2Props { title?: string @@ -69,13 +67,11 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { const missingBase = createMemo(() => !(sync.data.path.home || sync.data.path.directory)) const [fallbackPath] = createResource( () => (missingBase() ? true : undefined), - async (): Promise => { - if ((await sdk.protocol) !== "v1") return - return sdk.client.path + () => + sdk.client.path .get() .then((result) => result.data) - .catch(() => undefined) - }, + .catch(() => undefined), { initialValue: undefined }, ) const home = createMemo(() => sync.data.path.home || fallbackPath()?.home || "") @@ -89,26 +85,18 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { ) const search = createDirectorySearch({ sdk, home, base: () => root() || start() }) const [suggestions] = createResource(input, async (value) => { - const cleaned = cleanPickerInput(value) - const typed = cleaned.replace(/\/+$/, "") + const typed = cleanPickerInput(value).replace(/\/+$/, "") const current = displayPickerPath(root(), value, home()).replace(/\/+$/, "") - if (!cleaned || (root() && typed === current)) return { query: value, items: [] } + if (!typed || typed === current) return { query: value, items: [] } const directories = (await search(value)).map((absolute) => ({ absolute, type: "directory" as const })) if (!policy.includeFiles) return { query: value, items: directories.slice(0, 5) } - const base = pickerRoot(cleaned) || root() || start() - if (!base) return { query: value, items: directories.slice(0, 5) } - const files = await sdk.api.file - .find({ - location: { directory: base }, - query: pickerFileSearchQuery(base, value, home()), - type: "file", - limit: 20, - }) - .then((result) => result.data) + const files = await sdk.client.find + .files({ directory: root(), query: pickerFileSearchQuery(root(), value, home()), type: "file", limit: 20 }) + .then((result) => result.data ?? []) .catch(() => []) const results = [ ...directories, - ...files.map((entry) => ({ absolute: absoluteTreePath(base, entry.path), type: "file" as const })), + ...files.map((path) => ({ absolute: absoluteTreePath(root(), path), type: "file" as const })), ] return { query: value, @@ -127,14 +115,9 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { existing ?? loads.schedule(`${generation}:${key}`, eager ? "background" : "user", () => { if (!activeTreeNavigation(generation, navigation)) return Promise.resolve(undefined) - return sdk.api.file - .list({ location: { directory: absolute } }) - .then((result) => - result.data.map((entry) => ({ - name: getFilename(entry.path.replace(/[\\/]+$/, "")), - type: entry.type, - })), - ) + return sdk.client.file + .list({ directory: absolute, path: "" }) + .then((result) => result.data ?? []) .catch(() => undefined) }) listings.set(key, request) @@ -329,7 +312,6 @@ export function DialogSelectDirectoryV2(props: DialogSelectDirectoryV2Props) { {(suggestion, index) => (
- - - } - class="absolute top-[10px] left-[-36px]" - /> - -
+ +

July 14

- + , "href"> { + href: string +} + +export function Link(props: LinkProps) { + const platform = usePlatform() + const [local, rest] = splitProps(props, ["href", "children", "class"]) + + return ( + { + if (!local.href) return + event.preventDefault() + platform.openLink(local.href) + }} + {...rest} + > + {local.children} + + ) +} diff --git a/packages/app/src/components/prompt-input-v2.tsx b/packages/app/src/components/prompt-input-v2.tsx index 96b99aaf4a..921d8ff45c 100644 --- a/packages/app/src/components/prompt-input-v2.tsx +++ b/packages/app/src/components/prompt-input-v2.tsx @@ -5,7 +5,7 @@ import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" import { Icon } from "@opencode-ai/ui/v2/icon" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client" +import type { Prompt, ReferenceInfo } from "@opencode-ai/sdk/v2/client" import { createEffect, createMemo, on, Show } from "solid-js" import { ModelSelectorPopoverV2 } from "@/components/dialog-select-model" import { DialogSelectModelUnpaidV2 } from "@/components/dialog-select-model-unpaid-v2" @@ -37,9 +37,11 @@ export type PromptInputV2ComposerProps = { class?: string controller: PromptInputV2ComposerController borderUnderlay?: boolean + edit?: PromptInputProps["edit"] + onEditLoaded?: PromptInputProps["onEditLoaded"] } -export type PromptInputV2ControllerProps = Omit +export type PromptInputV2ControllerProps = Omit export type PromptInputV2ComposerController = PromptInputV2Interaction & { readonly model: PromptInputProps["controls"]["model"] } @@ -49,13 +51,15 @@ export function PromptInputV2Composer(props: PromptInputV2ComposerProps) { const command = useCommand() const language = useLanguage() + useCommands(props) + useEditHandler(props) + return (
{ + const prompt = usePrompt() + + createEffect( + on( + () => props.edit?.id, + (id) => { + const edit = props.edit + if (!id || !edit) return + prompt.context.items().forEach((item) => prompt.context.remove(item.key)) + edit.context.forEach((item) => + prompt.context.add({ + type: item.type, + path: item.path, + selection: item.selection, + comment: item.comment, + commentID: item.commentID, + commentOrigin: item.commentOrigin, + preview: item.preview, + }), + ) + props.controller.dispatch({ type: "mode.normal" }) + props.controller.resetHistory() + prompt.set(edit.prompt, promptLength(edit.prompt)) + props.controller.restoreFocus() + props.onEditLoaded?.() + }, + { defer: true }, + ), + ) +} + +const useCommands = (props: PromptInputV2ComposerProps) => { + const command = useCommand() + const language = useLanguage() + + command.register("prompt-input", () => [ + { + id: "file.attach", + title: language.t("prompt.action.attachFile"), + category: language.t("command.category.file"), + keybind: "mod+u", + disabled: props.controller.state.mode !== "normal", + onSelect: () => props.controller.attach(), + }, + { + id: "prompt.mode.shell", + title: language.t("command.prompt.mode.shell"), + category: language.t("command.category.session"), + keybind: "mod+shift+x", + disabled: props.controller.state.mode === "shell", + onSelect: () => props.controller.dispatch({ type: "mode.shell" }), + }, + { + id: "prompt.mode.normal", + title: language.t("command.prompt.mode.normal"), + category: language.t("command.category.session"), + keybind: "mod+shift+e", + disabled: props.controller.state.mode === "normal", + onSelect: () => props.controller.dispatch({ type: "mode.normal" }), + }, + ]) +} + export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): PromptInputV2ComposerController { const sdk = useSDK() const sync = useSync() @@ -242,7 +310,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): ) const resources = createMemo(() => Object.values(sync().data.mcp_resource).map((resource) => ({ - id: `resource:${resource.server}:${resource.uri}`, + id: `resource:${resource.client}:${resource.uri}`, kind: "resource" as const, label: `@${resource.name}`, path: resource.uri, @@ -259,7 +327,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): source: { type: "resource" as const, text: { value: `@${resource.name}`, start: 0, end: resource.name.length + 1 }, - clientName: resource.server, + clientName: resource.client, uri: resource.uri, }, }, @@ -344,7 +412,7 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): if (item?.commentID) comments.remove(item.path, item.commentID) }, openAttachment: (attachment) => - dialog.show(() => ), + dialog.show(() => ), openContext(key) { const item = controller.contextItem(key) if (item) openComment(item, props, sync, layout, files, comments) @@ -368,7 +436,6 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): title: language.t("prompt.toast.pasteUnsupported.title"), description: language.t("prompt.toast.pasteUnsupported.description"), }), - duplicate: () => showToast({ title: language.t("prompt.toast.attachmentDuplicate.title") }), onError: (error) => showToast({ variant: "error", @@ -377,20 +444,18 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): }), readClipboardImage: platform.readClipboardImage, getPathForFile: platform.getPathForFile, - store: platform.draftStore?.putBlob, }, view: { placeholder: designPlaceholder, - get agent() { - return props.controls.agents.visible && props.controls.agents.options.length > 0 + agent: + props.controls.agents.visible && props.controls.agents.options.length > 0 ? { options: () => props.controls.agents.options.map((name) => ({ id: name, label: name })), current: () => props.controls.agents.current, - onSelect: (value: string) => props.controls.agents.select(value), + onSelect: props.controls.agents.select, keybind: () => command.keybindParts("agent.cycle"), } - : undefined - }, + : undefined, variant: { options: () => variants().map((value) => ({ id: value, label: value })), current: () => props.controls.model.selection.variant.current() ?? "default", @@ -406,62 +471,6 @@ export function usePromptInputV2Controller(props: PromptInputV2ControllerProps): }, }) Object.defineProperty(controller, "model", { get: () => props.controls.model }) - - command.register("prompt-input", () => [ - { - id: "file.attach", - title: language.t("prompt.action.attachFile"), - category: language.t("command.category.file"), - keybind: "mod+u", - disabled: controller.state.mode !== "normal", - onSelect: () => controller.attach(), - }, - { - id: "prompt.mode.shell", - title: language.t("command.prompt.mode.shell"), - category: language.t("command.category.session"), - keybind: "mod+shift+x", - disabled: controller.state.mode === "shell", - onSelect: () => controller.dispatch({ type: "mode.shell" }), - }, - { - id: "prompt.mode.normal", - title: language.t("command.prompt.mode.normal"), - category: language.t("command.category.session"), - keybind: "mod+shift+e", - disabled: controller.state.mode === "normal", - onSelect: () => controller.dispatch({ type: "mode.normal" }), - }, - ]) - - createEffect( - on( - () => props.edit?.id, - (id) => { - const edit = props.edit - if (!id || !edit) return - prompt.context.items().forEach((item) => prompt.context.remove(item.key)) - edit.context.forEach((item) => - prompt.context.add({ - type: item.type, - path: item.path, - selection: item.selection, - comment: item.comment, - commentID: item.commentID, - commentOrigin: item.commentOrigin, - preview: item.preview, - }), - ) - controller.dispatch({ type: "mode.normal" }) - controller.resetHistory() - prompt.set(edit.prompt, promptLength(edit.prompt)) - controller.restoreFocus() - props.onEditLoaded?.() - }, - { defer: true }, - ), - ) - return controller as PromptInputV2ComposerController } @@ -511,7 +520,6 @@ function PromptInputV2ModelControl(props: { fallback={ ( - - {content()} - - )} + triggerAs={ButtonV2} + triggerProps={{ + variant: "ghost-muted", + size: "normal", + style: { height: "28px" }, + class: "min-w-0 max-w-[220px] justify-start ![font-weight:440] group", + classList: { "animate-in fade-in": shouldAnimate() }, + "data-action": "prompt-model", + }} onClose={props.onClose} - /> + > + {content()} + diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 08c1ee3489..bcc5acc0bb 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -591,7 +591,7 @@ export const PromptInput: Component = (props) => { type: "resource", name: resource.name, uri: resource.uri, - client: resource.server, + client: resource.client, display: resource.name, description: resource.description, mime: resource.mimeType, @@ -709,7 +709,7 @@ export const PromptInput: Component = (props) => { title: cmd.name, description: cmd.description, type: "custom" as const, - // source: cmd.source, + source: cmd.source, })) return [...custom, ...builtin] @@ -1489,7 +1489,7 @@ export const PromptInput: Component = (props) => { - dialog.show(() => ) + dialog.show(() => ) } onRemove={removeAttachment} removeLabel={language.t("prompt.attachment.remove")} @@ -1723,31 +1723,29 @@ export const PromptInput: Component = (props) => { > ( - - )} + triggerAs={Button} + triggerProps={{ + variant: "ghost", + size: "normal", + style: control(), + class: "min-w-0 max-w-[320px] text-13-regular text-text-base group", + "data-action": "prompt-model", + }} onClose={restoreFocus} - /> + > + + + + + {props.controls.model.selection.current()?.name ?? + language.t("dialog.model.select.title")} + + +
diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index 6f3ef57c66..9568b17d19 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -3,13 +3,28 @@ import { makeEventListener } from "@solid-primitives/event-listener" import { showToast } from "@/utils/toast" import { type ContentPart, type ImageAttachmentPart, type usePrompt } from "@/context/prompt" import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" import { uuid } from "@/utils/uuid" import { getCursorPosition } from "./editor-dom" -import { createBlobReference, type DraftStore } from "@/utils/draft-store" import { attachmentMime } from "./files" import { normalizePaste, pasteMode } from "./paste" +function dataUrl(file: File, mime: string) { + return new Promise((resolve) => { + const reader = new FileReader() + reader.addEventListener("error", () => resolve("")) + reader.addEventListener("load", () => { + const value = typeof reader.result === "string" ? reader.result : "" + const idx = value.indexOf(",") + if (idx === -1) { + resolve(value) + return + } + resolve(`data:${mime};base64,${value.slice(idx + 1)}`) + }) + reader.readAsDataURL(file) + }) +} + type PromptTarget = Pick["capture"]>, "current" | "cursor" | "set"> type AttachmentTarget = { prompt: PromptTarget; cursor: number | undefined } @@ -21,7 +36,6 @@ type PromptAttachmentsCoreInput = { warn?: () => void readClipboardImage?: () => Promise getPathForFile?: (file: File) => string - draftStore?: DraftStore } export type PromptAttachmentsInput = { @@ -51,13 +65,16 @@ export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) { return false } + const url = await dataUrl(file, mime) + if (!url) return false + const attachment: ImageAttachmentPart = { type: "image", id: uuid(), filename: file.name, sourcePath: input.getPathForFile?.(file) || undefined, mime, - blob: input.draftStore ? await input.draftStore.putBlob(file) : await createBlobReference(file), + dataUrl: url, } target.prompt.set([...target.prompt.current(), attachment], target.cursor) return true @@ -149,10 +166,8 @@ export function createPromptAttachmentsCore(input: PromptAttachmentsCoreInput) { export function createPromptAttachments(input: PromptAttachmentsInput) { const language = useLanguage() - const platform = usePlatform() const attachments = createPromptAttachmentsCore({ ...input, - draftStore: platform.draftStore, capture: input.prompt.capture, warn: () => { showToast({ 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 a1e3448b31..7f0fea2bfe 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -22,7 +22,7 @@ type ContextFile = { type BuildRequestPartsInput = { prompt: Prompt context: ContextFile[] - images: (Omit & { dataUrl: string })[] + images: ImageAttachmentPart[] text: string messageID: string sessionID: string diff --git a/packages/app/src/components/prompt-input/history-store.ts b/packages/app/src/components/prompt-input/history-store.ts index 3858b8b9bc..b64636f3c0 100644 --- a/packages/app/src/components/prompt-input/history-store.ts +++ b/packages/app/src/components/prompt-input/history-store.ts @@ -1,13 +1,7 @@ import { createStore, type SetStoreFunction, type Store } from "solid-js/store" import type { Prompt } from "@/context/prompt" import { Persist, persisted } from "@/utils/persist" -import { - clonePromptHistoryComments, - clonePromptParts, - prependHistoryEntry, - type PromptHistoryComment, - type PromptHistoryStoredEntry, -} from "./history" +import { prependHistoryEntry, type PromptHistoryComment, type PromptHistoryStoredEntry } from "./history" export type PromptInputHistory = { entries: (mode: "normal" | "shell") => PromptHistoryStoredEntry[] @@ -41,23 +35,13 @@ export function createPromptInputHistory(): PromptInputHistory { } export function createPersistedPromptInputHistory() { - const [normal, setNormal, normalInit] = persisted( - Persist.prompt(Persist.global("prompt-history", ["prompt-history.v1"])), + const [normal, setNormal] = persisted( + Persist.global("prompt-history", ["prompt-history.v1"]), createStore({ entries: [] }), ) - const [shell, setShell, shellInit] = persisted( - Persist.prompt(Persist.global("prompt-history-shell", ["prompt-history-shell.v1"])), + const [shell, setShell] = persisted( + Persist.global("prompt-history-shell", ["prompt-history-shell.v1"]), createStore({ entries: [] }), ) - const history = createPromptInputHistoryStore(normal, setNormal, shell, setShell) - return { - ...history, - add(prompt: Prompt, mode: "normal" | "shell", comments: PromptHistoryComment[]) { - const ready = mode === "shell" ? shellInit : normalInit - if (!(ready instanceof Promise)) return history.add(prompt, mode, comments) - const saved = clonePromptParts(prompt) - const metadata = clonePromptHistoryComments(comments) - void ready.then(() => history.add(saved, mode, metadata)) - }, - } + return createPromptInputHistoryStore(normal, setNormal, shell, setShell) } diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts index 94f24ce935..5e9c2c66ea 100644 --- a/packages/app/src/components/prompt-input/history.test.ts +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -112,7 +112,7 @@ describe("prompt-input history", () => { end: 12, selection: { startLine: 1, startChar: 1, endLine: 2, endChar: 1 }, }, - { type: "image", id: "1", filename: "img.png", mime: "image/png", blob: { id: "blob", url: "blob:test" } }, + { type: "image", id: "1", filename: "img.png", mime: "image/png", dataUrl: "data:image/png;base64,abc" }, ] const copy = clonePromptParts(original) expect(copy).not.toBe(original) diff --git a/packages/app/src/components/prompt-input/image-attachments.tsx b/packages/app/src/components/prompt-input/image-attachments.tsx index ecc38db888..3a0cc3e4dd 100644 --- a/packages/app/src/components/prompt-input/image-attachments.tsx +++ b/packages/app/src/components/prompt-input/image-attachments.tsx @@ -100,7 +100,7 @@ export const PromptImageAttachments: Component = (p } > {attachment.filename} props.onOpen(attachment)} diff --git a/packages/app/src/components/prompt-input/submit.test.ts b/packages/app/src/components/prompt-input/submit.test.ts index b3201b3ef6..f563a50982 100644 --- a/packages/app/src/components/prompt-input/submit.test.ts +++ b/packages/app/src/components/prompt-input/submit.test.ts @@ -7,11 +7,6 @@ let createPromptSubmit: typeof import("./submit").createPromptSubmit const createdClients: string[] = [] const createdSessions: string[] = [] -const sessionCreateInputs: Array<{ - agent?: string - model?: { id: string; providerID: string; variant?: string } - location?: { directory: string } -}> = [] const enabledAutoAccept: Array<{ server: string; sessionID: string; directory: string }> = [] const optimistic: Array<{ directory?: string @@ -25,14 +20,9 @@ const optimistic: Array<{ const optimisticSeeded: boolean[] = [] const storedSessions: Record> = {} const promoted: Array<{ directory: string; sessionID: string }> = [] -const sentShell: Array<{ sessionID: string; id?: string; command: string }> = [] +const sentShell: string[] = [] const syncedDirectories: string[] = [] const promotedDrafts: Array<{ draftID: string; server: string; sessionId: string }> = [] -const sentPrompts: string[] = [] -const promptInputs: unknown[] = [] -const sentCommands: unknown[] = [] -const commands: Array<{ name: string }> = [] -let serverSessionSyncs = 0 let params: { id?: string } = {} let search: { draftId?: string } = {} @@ -41,7 +31,7 @@ let variant: string | undefined let permissionServer = "server-a" let createSessionGate: Promise | undefined -let promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] +const promptValue: Prompt = [{ type: "text", content: "ls", start: 0, end: 2 }] const [promptStore, setPromptStore] = createStore({ prompt: promptValue, cursor: 0, @@ -73,39 +63,23 @@ const prompt = { const clientFor = (directory: string) => { createdClients.push(directory) return { - api: { - session: { - create: async (input: (typeof sessionCreateInputs)[number]) => { - await createSessionGate - const location = input.location?.directory ?? directory - createdSessions.push(location) - sessionCreateInputs.push(input) - return { - id: `session-${createdSessions.length}`, - projectID: "project", - agent: input.agent, - model: input.model, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1, updated: 1 }, - title: `New session ${createdSessions.length}`, - location: { directory: location }, - } - }, - prompt: async (input: unknown) => { - sentPrompts.push(directory) - promptInputs.push(input) - return { data: undefined } - }, - command: async (input: unknown) => { - sentCommands.push(input) - }, - shell: async (input: { sessionID: string; id?: string; command: string }) => { - sentShell.push(input) - }, - }, - }, session: { + create: async () => { + await createSessionGate + createdSessions.push(directory) + return { + data: { + id: `session-${createdSessions.length}`, + title: `New session ${createdSessions.length}`, + }, + } + }, + shell: async () => { + sentShell.push(directory) + return { data: undefined } + }, + prompt: async () => ({ data: undefined }), + promptAsync: async () => ({ data: undefined }), command: async () => ({ data: undefined }), abort: async () => ({ data: undefined }), }, @@ -198,7 +172,6 @@ beforeAll(async () => { scope: "local", directory: "/repo/main", client: rootClient, - api: rootClient.api, url: "http://localhost:4096", createClient(opts: any) { return clientFor(opts.directory) @@ -210,7 +183,7 @@ beforeAll(async () => { mock.module("@/context/sync", () => ({ useSync: () => () => ({ - data: { command: commands }, + data: { command: [] }, session: { optimistic: { add: (value: { @@ -237,9 +210,6 @@ beforeAll(async () => { session: { remember: () => undefined, set: () => undefined, - sync: async () => { - serverSessionSyncs++ - }, }, child: (directory: string) => { syncedDirectories.push(directory) @@ -281,17 +251,11 @@ beforeAll(async () => { beforeEach(() => { createdClients.length = 0 createdSessions.length = 0 - sessionCreateInputs.length = 0 enabledAutoAccept.length = 0 optimistic.length = 0 optimisticSeeded.length = 0 promoted.length = 0 promotedDrafts.length = 0 - sentPrompts.length = 0 - promptInputs.length = 0 - sentCommands.length = 0 - commands.length = 0 - promptValue = [{ type: "text", content: "ls", start: 0, end: 2 }] params = {} search = {} sentShell.length = 0 @@ -300,7 +264,6 @@ beforeEach(() => { variant = undefined permissionServer = "server-a" createSessionGate = undefined - serverSessionSyncs = 0 for (const key of Object.keys(storedSessions)) delete storedSessions[key] }) @@ -334,24 +297,8 @@ describe("prompt submit worktree selection", () => { expect(createdClients).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) expect(createdSessions).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) - expect(sessionCreateInputs).toEqual([ - { - agent: "agent", - model: { id: "model", providerID: "provider", variant: undefined }, - location: { directory: "/repo/worktree-a" }, - }, - { - agent: "agent", - model: { id: "model", providerID: "provider", variant: undefined }, - location: { directory: "/repo/worktree-b" }, - }, - ]) - expect(sentShell).toEqual([ - expect.objectContaining({ sessionID: "session-1", id: expect.stringMatching(/^evt_/), command: "ls" }), - expect.objectContaining({ sessionID: "session-2", id: expect.stringMatching(/^evt_/), command: "ls" }), - ]) + expect(sentShell).toEqual(["/repo/worktree-a", "/repo/worktree-b"]) expect(syncedDirectories).toEqual(["/repo/worktree-a", "/repo/worktree-a", "/repo/worktree-b", "/repo/worktree-b"]) - expect(serverSessionSyncs).toBe(0) expect(promoted).toEqual([ { directory: "/repo/worktree-a", sessionID: "session-1" }, { directory: "/repo/worktree-b", sessionID: "session-2" }, @@ -472,7 +419,6 @@ describe("prompt submit worktree selection", () => { const event = { preventDefault: () => undefined } as unknown as Event await submit.handleSubmit(event) - await Bun.sleep(0) expect(optimistic).toHaveLength(1) expect(optimistic[0]).toMatchObject({ @@ -481,56 +427,6 @@ describe("prompt submit worktree selection", () => { model: { providerID: "provider", modelID: "model", variant: "high" }, }, }) - expect(sentPrompts).toEqual(["/repo/main"]) - expect(promptInputs[0]).toMatchObject({ - sessionID: "session-1", - text: "ls", - files: [], - agents: [], - }) - expect((promptInputs[0] as { id?: string }).id).toStartWith("msg_") - expect((promptInputs[0] as { legacyParts?: { id: string; type: string; text?: string }[] }).legacyParts).toEqual([ - { id: expect.stringMatching(/^prt_/), type: "text", text: "ls" }, - ]) - }) - - test("submits slash commands through the current session API", async () => { - params = { id: "session-1" } - variant = "high" - commands.push({ name: "review" }) - promptValue = [{ type: "text", content: "/review staged changes", start: 0, end: 22 }] - - const submit = createPromptSubmit({ - prompt, - info: () => ({ id: "session-1" }), - imageAttachments: () => [], - commentCount: () => 0, - autoAccept: () => false, - mode: () => "normal", - working: () => false, - editor: () => undefined, - queueScroll: () => undefined, - promptLength: (value) => value.reduce((sum, part) => sum + ("content" in part ? part.content.length : 0), 0), - addToHistory: () => undefined, - resetHistoryNavigation: () => undefined, - setMode: () => undefined, - setPopover: () => undefined, - }) - - await submit.handleSubmit({ preventDefault: () => undefined } as unknown as Event) - - expect(sentCommands).toEqual([ - { - sessionID: "session-1", - id: expect.stringMatching(/^msg_/), - command: "review", - arguments: "staged changes", - agent: "agent", - model: { id: "model", providerID: "provider", variant: "high" }, - files: [], - }, - ]) - expect(serverSessionSyncs).toBe(0) }) test("uses an injected model selection", async () => { @@ -591,8 +487,7 @@ describe("prompt submit worktree selection", () => { await submit.handleSubmit(event) - expect(storedSessions["/repo/worktree-a"]).toHaveLength(1) - expect(storedSessions["/repo/worktree-a"]?.[0]).toMatchObject({ id: "session-1", title: "New session 1" }) + expect(storedSessions["/repo/worktree-a"]).toEqual([{ id: "session-1", title: "New session 1" }]) expect(optimisticSeeded).toEqual([true]) }) }) diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index c82c3a4399..203722fc6c 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -20,9 +20,6 @@ import { setCursorPosition } from "./editor-dom" import { formatServerError } from "@/utils/server-errors" import { ScopedKey } from "@/utils/server-scope" import { createPromptSubmissionState } from "./submission-state" -import { normalizeSessionInfo } from "@/utils/session" -import { Event } from "@opencode-ai/schema/event" -import { blobDataUrl } from "@/utils/draft-store" type PendingPrompt = { abort: AbortController @@ -42,7 +39,7 @@ export type FollowupDraft = { } type FollowupSendInput = { - api: DirectorySDK["api"]["session"] + client: DirectorySDK["client"] serverSync: ServerSync sync: DirectorySync draft: FollowupDraft @@ -84,24 +81,20 @@ export async function sendFollowupDraft(input: FollowupSendInput) { return false } - const messageID = Identifier.ascending("message") - await input.api.command({ + await input.client.session.command({ sessionID: input.draft.sessionID, - id: messageID, command: cmd, arguments: tail.join(" "), agent: input.draft.agent, - model: { - id: input.draft.model.modelID, - providerID: input.draft.model.providerID, - variant: input.draft.variant, - }, - files: await Promise.all( - images.map(async (attachment) => ({ - uri: await blobDataUrl(attachment.blob, attachment.mime), - name: attachment.filename, - })), - ), + model: `${input.draft.model.providerID}/${input.draft.model.modelID}`, + variant: input.draft.variant, + parts: images.map((attachment) => ({ + id: Identifier.ascending("part"), + type: "file" as const, + mime: attachment.mime, + url: attachment.dataUrl, + filename: attachment.filename, + })), }) return true } catch (err) { @@ -111,16 +104,10 @@ export async function sendFollowupDraft(input: FollowupSendInput) { } const messageID = input.messageID ?? Identifier.ascending("message") - const encodedImages = await Promise.all( - images.map(async (attachment) => ({ - ...attachment, - dataUrl: await blobDataUrl(attachment.blob, attachment.mime), - })), - ) const { requestParts, optimisticParts } = buildRequestParts({ prompt: input.draft.prompt, context: input.draft.context, - images: encodedImages, + images, text, sessionID: input.draft.sessionID, messageID, @@ -165,37 +152,13 @@ export async function sendFollowupDraft(input: FollowupSendInput) { return false } - await input.api.prompt({ + await input.client.session.promptAsync({ sessionID: input.draft.sessionID, - id: messageID, agent: input.draft.agent, model: input.draft.model, + messageID, + parts: requestParts, variant: input.draft.variant, - legacyParts: requestParts, - text: requestParts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"), - files: requestParts.flatMap((part) => { - if (part.type !== "file") return [] - const text = part.source?.text - return [ - { - uri: part.url, - name: part.filename, - mention: text ? { start: text.start, end: text.end, text: text.value } : undefined, - }, - ] - }), - agents: requestParts.flatMap((part) => - part.type === "agent" - ? [ - { - name: part.name, - mention: part.source - ? { start: part.source.start, end: part.source.end, text: part.source.value } - : undefined, - }, - ] - : [], - ), }) return true } catch (err) { @@ -247,7 +210,6 @@ export function createPromptSubmit(input: PromptSubmitInput) { const pendingKey = (sessionID: string) => ScopedKey.from(sdk().scope, sessionID) const errorMessage = (err: unknown) => { - if (err && typeof err === "object" && "message" in err && typeof err.message === "string") return err.message if (err && typeof err === "object" && "data" in err) { const data = (err as { data?: { message?: string } }).data if (data?.message) return data.message @@ -273,7 +235,9 @@ export function createPromptSubmit(input: PromptSubmitInput) { return Promise.resolve() } return sdk() - .api.session.interrupt({ sessionID }) + .client.session.abort({ + sessionID, + }) .catch(() => {}) } @@ -400,13 +364,9 @@ export function createPromptSubmit(input: PromptSubmitInput) { let session = input.info() if (!session && isNewSession) { - const created = await sdk() - .api.session.create({ - agent: currentAgent.name, - model: { id: currentModel.id, providerID: currentModel.provider.id, variant }, - location: { directory: sessionDirectory }, - }) - .then(normalizeSessionInfo) + const created = await client.session + .create() + .then((x) => x.data ?? undefined) .catch((err) => { showToast({ title: language.t("prompt.toast.sessionCreateFailed.title"), @@ -490,14 +450,12 @@ export function createPromptSubmit(input: PromptSubmitInput) { if (mode === "shell") { clearInput() - const eventID = Event.ID.create() - sdk() - .api.session.shell({ + client.session + .shell({ sessionID: session.id, - id: eventID, - command: text, agent, model, + command: text, }) .catch((err) => { showToast({ @@ -515,25 +473,23 @@ export function createPromptSubmit(input: PromptSubmitInput) { const customCommand = sync().data.command.find((c) => c.name === commandName) if (customCommand) { clearInput() - const messageID = Identifier.ascending("message") - serverSync().session.set("session_status", session.id, { type: "busy" }) - sdk() - .api.session.command({ + client.session + .command({ sessionID: session.id, - id: messageID, command: commandName, arguments: args.join(" "), agent, - model: { id: model.modelID, providerID: model.providerID, variant }, - files: await Promise.all( - images.map(async (attachment) => ({ - uri: await blobDataUrl(attachment.blob, attachment.mime), - name: attachment.filename, - })), - ), + model: `${model.providerID}/${model.modelID}`, + variant, + parts: images.map((attachment) => ({ + id: Identifier.ascending("part"), + type: "file" as const, + mime: attachment.mime, + url: attachment.dataUrl, + filename: attachment.filename, + })), }) .catch((err) => { - serverSync().session.set("session_status", session.id, { type: "idle" }) showToast({ title: language.t("prompt.toast.commandSendFailed.title"), description: formatServerError(err, language.t, language.t("common.requestFailed")), @@ -617,7 +573,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { } void sendFollowupDraft({ - api: sdk().api.session, + client, sync: sync(), serverSync: serverSync(), draft, diff --git a/packages/app/src/components/prompt-project-selector.tsx b/packages/app/src/components/prompt-project-selector.tsx index b84afdb56b..1e5445517d 100644 --- a/packages/app/src/components/prompt-project-selector.tsx +++ b/packages/app/src/components/prompt-project-selector.tsx @@ -18,7 +18,6 @@ import { useLanguage } from "@/context/language" import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" import { pathKey } from "@/utils/path-key" import { handleDocumentSearchKeydown } from "@/utils/search-keydown" -import { createMenuDismissController } from "@/utils/menu-dismiss-controller" export type PromptProject = { name?: string @@ -198,8 +197,8 @@ export function PromptProjectSelector(props: { }) { const [triggerReady, setTriggerReady] = createSignal(false) let contentRef: HTMLDivElement | undefined - const dismiss = createMenuDismissController(() => contentRef) let triggerFrame: number | undefined + let restoreTrigger = true // Floating UI requires a connected anchor; route transitions can construct this trigger before adoption. const setTriggerRef = (element: HTMLButtonElement) => { @@ -222,15 +221,25 @@ export function PromptProjectSelector(props: { props.controller.active() ? contentRef?.querySelector(`[data-option-key="${CSS.escape(props.controller.active())}"]`) : undefined + const afterClose = (callback: () => void) => { + const complete = () => { + if (contentRef?.isConnected) { + requestAnimationFrame(complete) + return + } + requestAnimationFrame(() => requestAnimationFrame(callback)) + } + requestAnimationFrame(complete) + } const selectProject = (project: PromptProject) => { - dismiss.preventTriggerRestore() + restoreTrigger = false props.controller.setOpen(false) - dismiss.afterClose(() => props.controller.select(project)) + afterClose(() => props.controller.select(project)) } const selectAction = (server?: string) => { - dismiss.preventTriggerRestore() + restoreTrigger = false props.controller.setOpen(false) - dismiss.afterClose(() => props.controller.add(server)) + afterClose(() => props.controller.add(server)) } const selectActive = () => { const project = props.controller.activeProject() @@ -258,7 +267,7 @@ export function PromptProjectSelector(props: { ) .filter((element) => !contentRef?.contains(element) && !element.hasAttribute("data-focus-trap")) .findLast((element) => element.offsetParent !== null) - dismiss.preventTriggerRestore() + restoreTrigger = false target?.focus() queueMicrotask(() => { if (props.controller.open()) props.controller.setOpen(false) @@ -282,10 +291,7 @@ export function PromptProjectSelector(props: { placement={props.placement ?? "bottom"} gutter={4} modal={false} - onOpenChange={(open) => { - if (open) dismiss.allowTriggerRestore() - props.controller.setOpen(open) - }} + onOpenChange={(open) => props.controller.setOpen(open)} > @@ -294,9 +300,11 @@ export function PromptProjectSelector(props: { id="prompt-project-menu" class="w-[243px] overflow-hidden rounded-md border-0 bg-v2-background-bg-layer-01 p-0 shadow-[var(--v2-elevation-floating)] focus:outline-none [&[data-closed]]:!animate-none" onOpenAutoFocus={(event) => event.preventDefault()} - onPointerDownOutside={dismiss.preventTriggerRestore} - onFocusOutside={dismiss.preventTriggerRestore} - onCloseAutoFocus={dismiss.onCloseAutoFocus} + onPointerDownOutside={() => (restoreTrigger = false)} + onFocusOutside={() => (restoreTrigger = false)} + onCloseAutoFocus={(event) => { + if (!restoreTrigger) event.preventDefault() + }} >
@@ -356,45 +364,41 @@ export function PromptProjectSelector(props: {
-
- 1} - fallback={ - - - {(project) => ( - - )} - - - } - > - - props.controller.projects().some((project) => project.server?.key === server!.key), + 1} + fallback={ + + + {(project) => ( + )} - > - {(server) => ( -
-
- {server!.name} -
- - project.server?.key === server!.key)} - > - {(project) => ( - - )} - - -
+
+
+ } + > + + props.controller.projects().some((project) => project.server?.key === server!.key), )} - -
-
+ > + {(server) => ( +
+
+ {server!.name} +
+ + project.server?.key === server!.key)}> + {(project) => ( + + )} + + +
+ )} + +
diff --git a/packages/app/src/components/server/server-row-menu.tsx b/packages/app/src/components/server/server-row-menu.tsx index 0a2920dec7..9d5f5e5a32 100644 --- a/packages/app/src/components/server/server-row-menu.tsx +++ b/packages/app/src/components/server/server-row-menu.tsx @@ -15,47 +15,9 @@ export const ServerRowMenu: Component<{ }> = (props) => { const language = useLanguage() const key = ServerConnection.key(props.server) - return ( - props.controller.setDefault(key)} - onRemoveDefault={() => props.controller.setDefault(null)} - onRemove={() => props.controller.handleRemove(key)} - open={props.open} - onOpenChange={props.onOpenChange} - /> - ) -} + const builtin = ServerConnection.builtin(props.server) + const isDefault = () => props.controller.defaultKey() === key -export function serverMenuLabels(language: ReturnType) { - return { - more: language.t("common.moreOptions"), - server: language.t("settings.section.server"), - edit: language.t("dialog.server.menu.edit"), - default: language.t("dialog.server.menu.default"), - defaultRemove: language.t("dialog.server.menu.defaultRemove"), - delete: language.t("dialog.server.menu.delete"), - } -} - -export const ServerRowMenuView: Component<{ - server: ServerConnection.Any - labels: ReturnType - canDefault: boolean - isDefault: boolean - onEdit: (server: ServerConnection.Http) => void - onSetDefault: () => void - onRemoveDefault: () => void - onRemove: () => void - open?: boolean - onOpenChange?: (open: boolean) => void -}> = (props) => { - const builtin = () => ServerConnection.builtin(props.server) - const httpServer = () => (props.server.type === "http" ? props.server : undefined) return ( } - aria-label={props.labels.more} + aria-label={language.t("common.moreOptions")} /> - {props.labels.server} + {language.t("settings.section.server")} { - const server = httpServer() - if (server) props.onEdit(server) - }} + disabled={builtin || props.server.type !== "http"} + onSelect={() => props.onEdit(props.server as ServerConnection.Http)} > - {props.labels.edit} + {language.t("dialog.server.menu.edit")} - - {props.labels.default} + + props.controller.setDefault(key)}> + {language.t("dialog.server.menu.default")} + - - {props.labels.defaultRemove} + + props.controller.setDefault(null)}> + {language.t("dialog.server.menu.defaultRemove")} + - - {props.labels.delete} + props.controller.handleRemove(key)}> + {language.t("dialog.server.menu.delete")} diff --git a/packages/app/src/components/session/index.ts b/packages/app/src/components/session/index.ts index c4fdb2ff38..9e44cea17b 100644 --- a/packages/app/src/components/session/index.ts +++ b/packages/app/src/components/session/index.ts @@ -4,3 +4,4 @@ export { SortableTab, FileVisual } from "./session-sortable-tab" export { SortableTabV2 } from "./session-sortable-tab-v2" export { SortableTerminalTab } from "./session-sortable-terminal-tab" export { NewSessionView } from "./session-new-view" +export { NewSessionDesignView } from "./session-new-design-view" diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index dc6cdaa6f3..da1770cd13 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -289,9 +289,9 @@ export function SessionHeader() { return ( <> - + {(mount) => ( - +
- + {(keybind) => ( - {keybind} + {keybind()} )} @@ -319,9 +319,9 @@ export function SessionHeader() { )} - + {(mount) => ( - + +
+
+ +
{props.children}
+
+
+
+ ) +} diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx index 8adaa1f0fd..3beb97225a 100644 --- a/packages/app/src/components/settings-general.tsx +++ b/packages/app/src/components/settings-general.tsx @@ -29,7 +29,7 @@ import { } from "@/context/settings" import { decode64 } from "@/utils/base64" import { playSoundById, SOUND_OPTIONS } from "@/utils/sound" -import { ExternalLink } from "./external-link" +import { Link } from "./link" import { SettingsList } from "./settings-list" let demoSoundState = { @@ -127,14 +127,11 @@ export const SettingsGeneral: Component = () => { const serverSdk = useServerSDK() const [shells] = createResource( - async () => { - const sdk = serverSdk() - if ((await sdk.protocol) === "v1") { - return (await sdk.client.pty.shells()).data ?? [] - } - // return (await sdk.api.pty.shells()).data - return [] as ShellOption[] - }, + () => + serverSdk() + .client.pty.shells() + .then((res) => res.data ?? []) + .catch(() => [] as ShellOption[]), { initialValue: [] as ShellOption[] }, ) @@ -482,7 +479,7 @@ export const SettingsGeneral: Component = () => { description={ <> {language.t("settings.general.row.theme.description")}{" "} - {language.t("common.learnMore")} + {language.t("common.learnMore")} } > diff --git a/packages/app/src/components/settings-keybinds.tsx b/packages/app/src/components/settings-keybinds.tsx index ce74be4586..cdec8b435c 100644 --- a/packages/app/src/components/settings-keybinds.tsx +++ b/packages/app/src/components/settings-keybinds.tsx @@ -30,8 +30,6 @@ type KeybindMeta = { type KeybindMap = Record type CommandContext = ReturnType -type LanguageContext = ReturnType -type SettingsContext = ReturnType const GROUPS: KeybindGroup[] = ["General", "Session", "Navigation", "Model and agent", "Terminal", "Prompt"] @@ -124,7 +122,7 @@ function keybinds(value: unknown): KeybindMap { return value as KeybindMap } -function listFor(command: Pick, map: KeybindMap, palette: string) { +function listFor(command: CommandContext, map: KeybindMap, palette: string) { const out = new Map() out.set(PALETTE_ID, { title: palette, group: "General" }) @@ -264,274 +262,7 @@ function useKeyCapture(input: { }) } -export function createKeybindSettingsController( - input: { - command: Pick - settings: { - current: { keybinds: unknown } - keybinds: Pick - } - target?: Document - notify?: (toast: { title: string; description: string }) => void - }, - language: Pick = useLanguage(), -) { - const [store, setStore] = createStore({ active: null as string | null }) - const overrides = createMemo(() => keybinds(input.settings.current.keybinds)) - const list = createMemo(() => { - language.locale() - return listFor(input.command, overrides(), language.t("command.palette")) - }) - const grouped = createMemo(() => groupedFor(list())) - const title = (id: string) => list().get(id)?.title ?? "" - const effective = (id: string) => { - if (id === PALETTE_ID) return input.settings.keybinds.get(id) ?? DEFAULT_PALETTE_KEYBIND - - const custom = input.settings.keybinds.get(id) - if (typeof custom === "string") return custom - - const live = input.command.options.find((item) => item.id === id) - if (live?.keybind) return live.keybind - return input.command.catalog.find((item) => item.id === id)?.keybind - } - const used = createMemo(() => { - const value = new Map() - - for (const id of list().keys()) { - for (const signature of signatures(effective(id))) { - const items = value.get(signature) - if (items) { - items.push({ id, title: title(id) }) - continue - } - value.set(signature, [{ id, title: title(id) }]) - } - } - - return value - }) - const stop = () => { - if (!store.active) return - setStore("active", null) - input.command.keybinds(true) - } - const toggle = (id: string) => { - if (store.active === id) { - stop() - return - } - if (store.active) stop() - setStore("active", id) - input.command.keybinds(false) - } - const notify = input.notify ?? ((toast: { title: string; description: string }) => showToast(toast)) - - const handle = (event: KeyboardEvent) => { - const id = store.active - if (!id) return - - event.preventDefault() - event.stopPropagation() - event.stopImmediatePropagation() - - if (event.key === "Escape") { - stop() - return - } - - const clear = - (event.key === "Backspace" || event.key === "Delete") && - !event.ctrlKey && - !event.metaKey && - !event.altKey && - !event.shiftKey - if (clear) { - input.settings.keybinds.set(id, "none") - stop() - return - } - - const next = recordKeybind(event) - if (!next) return - - const conflicts = new Map() - for (const signature of signatures(next)) { - for (const item of used().get(signature) ?? []) { - if (item.id === id) continue - conflicts.set(item.id, item.title) - } - } - - if (conflicts.size > 0) { - notify({ - title: language.t("settings.shortcuts.conflict.title"), - description: language.t("settings.shortcuts.conflict.description", { - keybind: formatKeybind(next, language.t), - titles: [...conflicts.values()].join(", "), - }), - }) - return - } - - input.settings.keybinds.set(id, next) - stop() - } - - const target = input.target ?? (typeof document === "object" ? document : undefined) - if (target) makeEventListener(target, "keydown", handle, { capture: true }) - - onCleanup(() => { - if (store.active) input.command.keybinds(true) - }) - - return { - catalog: { - groups: GROUPS, - filtered: (query: string) => - filteredFor(query, list(), grouped(), (id) => formatKeybind(effective(id) ?? "", language.t)), - title, - keybind: (id: string) => formatKeybind(effective(id) ?? "", language.t), - }, - capture: { - active: () => store.active, - toggle, - }, - settings: { - hasOverrides: () => Object.values(overrides()).some((value) => typeof value === "string"), - reset: () => { - stop() - input.settings.keybinds.resetAll() - notify({ - title: language.t("settings.shortcuts.reset.toast.title"), - description: language.t("settings.shortcuts.reset.toast.description"), - }) - }, - }, - } -} - -function SettingsKeybindsV2() { - const command = useCommand() - const settings = useSettings() - const controller = createKeybindSettingsController({ - command, - settings, - }) - - return ( - - ) -} - -function SettingsKeybindsV2View(props: { - groups: KeybindGroup[] - filtered: (query: string) => Map - title: (id: string) => string - keybind: (id: string) => string - active: () => string | null - onCapture: (id: string) => void - hasOverrides: () => boolean - onReset: () => void -}) { - const language = useLanguage() - const [store, setStore] = createStore({ filter: "" }) - const filtered = createMemo(() => props.filtered(store.filter)) - const hasResults = createMemo(() => props.groups.some((group) => (filtered().get(group)?.length ?? 0) > 0)) - - return ( - <> -
-
-

{language.t("settings.shortcuts.title")}

- - {language.t("settings.shortcuts.reset.button")} - -
- -
-
-
- - {(group) => ( - 0}> -
-

{language.t(groupKey[group])}

- - - {(id) => ( -
- {props.title(id)} - -
- )} -
-
-
-
- )} -
- -
- {language.t("settings.shortcuts.search.empty")} - "{store.filter}" -
-
-
-
- - ) -} - export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => { - if (props.v2) return - const command = useCommand() const language = useLanguage() const settings = useSettings() @@ -745,37 +476,78 @@ export const SettingsKeybinds: Component<{ v2?: boolean }> = (props) => { ) return ( -
-
-
-
-

{language.t("settings.shortcuts.title")}

- -
+ +
+
+
+

{language.t("settings.shortcuts.title")}

+ +
-
- - + + setStore("filter", v)} + placeholder={language.t("settings.shortcuts.search.placeholder")} + spellcheck={false} + autocorrect="off" + autocomplete="off" + autocapitalize="off" + class="flex-1" + /> + + setStore("filter", "")} /> + +
+
+
+ {groups} +
+ } + > + <> +
+
+

{language.t("settings.shortcuts.title")}

+ + {language.t("settings.shortcuts.reset.button")} + +
+
-
- {groups} -
+
{groups}
+ +
) } diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 080e6c517b..bcd30edbc7 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -6,7 +6,7 @@ import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" -import { useServerProtocol, useServerSDK } from "@/context/server-sdk" +import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { DialogConnectProvider, useProviderConnectController } from "./dialog-connect-provider" import { DialogCustomProvider } from "./dialog-custom-provider" @@ -39,9 +39,8 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => const dialog = useDialog() const language = useLanguage() const serverSDK = useServerSDK() - const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders(() => undefined) + const providers = useProviders() const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { @@ -84,8 +83,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => return language.t("settings.providers.tag.other") } - const canDisconnect = (item: ProviderItem) => - source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id)) + const canDisconnect = (item: ProviderItem) => source(item) !== "env" const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key @@ -98,7 +96,6 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => } const disableProvider = async (providerID: string, name: string) => { - if (protocol() !== "v1") return const before = serverSync().data.config.disabled_providers ?? [] const next = before.includes(providerID) ? before : [...before, providerID] serverSync().set("config", "disabled_providers", next) @@ -221,33 +218,31 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) => )} - -
-
-
- - {language.t("provider.custom.title")} - {language.t("settings.providers.tag.custom")} -
- - {language.t("settings.providers.custom.description")} - +
+
+
+ + {language.t("provider.custom.title")} + {language.t("settings.providers.tag.custom")}
- + + {language.t("settings.providers.custom.description")} +
- + +
- - - - - {(item) => { - const key = { providerID: item.provider.id, modelID: item.id } - return ( - -
- { - models.setVisibility(key, checked) - }} - hideLabel - > - {item.name} - -
-
- ) - }} -
-
-
+ {(group) => ( +
+
+ +

{group.items[0].provider.name}

- ) - }} + + + {(item) => { + const key = { providerID: item.provider.id, modelID: item.id } + return ( + +
+ { + models.setVisibility(key, checked) + }} + hideLabel + > + {item.name} + +
+
+ ) + }} +
+
+
+ )} diff --git a/packages/app/src/components/settings-v2/providers.tsx b/packages/app/src/components/settings-v2/providers.tsx index acd73ddbe5..f945fa33c6 100644 --- a/packages/app/src/components/settings-v2/providers.tsx +++ b/packages/app/src/components/settings-v2/providers.tsx @@ -4,9 +4,9 @@ import { useDialog } from "@opencode-ai/ui/context/dialog" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" -import { createMemo, type Accessor, type Component, For, Show } from "solid-js" +import { createMemo, type Component, For, Show } from "solid-js" import { useLanguage } from "@/context/language" -import { useServerProtocol, useServerSDK } from "@/context/server-sdk" +import { useServerSDK } from "@/context/server-sdk" import { useServerSync } from "@/context/server-sync" import { DialogConnectProvider, useProviderConnectController } from "../dialog-connect-provider" import { DialogCustomProvider } from "../dialog-custom-provider" @@ -29,21 +29,17 @@ const PROVIDER_NOTES = [ const PROVIDER_ICON_SIZE = 16 -export const SettingsProvidersV2: Component<{ - directory: Accessor - onBack?: () => void -}> = (props) => { +export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) => { const dialog = useDialog() const language = useLanguage() const serverSdk = useServerSDK() - const protocol = useServerProtocol() const serverSync = useServerSync() - const providers = useProviders(props.directory) + const providers = useProviders() const providerConnect = useProviderConnectController({ onBack: props.onBack }) const connect = (provider?: string) => { providerConnect.select(provider) - void dialog.show(() => ) + void dialog.show(() => ) } const connected = createMemo(() => { @@ -81,8 +77,7 @@ export const SettingsProvidersV2: Component<{ return language.t("settings.providers.tag.other") } - const canDisconnect = (item: ProviderItem) => - source(item) !== "env" && (protocol() === "v1" || !isConfigCustom(item.id)) + const canDisconnect = (item: ProviderItem) => source(item) !== "env" const note = (id: string) => PROVIDER_NOTES.find((item) => item.match(id))?.key @@ -95,7 +90,6 @@ export const SettingsProvidersV2: Component<{ } const disableProvider = async (providerID: string, name: string) => { - if (protocol() !== "v1") return const before = serverSync().data.config.disabled_providers ?? [] const next = before.includes(providerID) ? before : [...before, providerID] serverSync().set("config", "disabled_providers", next) @@ -224,37 +218,33 @@ export const SettingsProvidersV2: Component<{ )} - -
-
- -
-
- {language.t("provider.custom.title")} - {language.t("settings.providers.tag.custom")} -
-

- {language.t("settings.providers.custom.description")} -

+
+
+ +
+
+ {language.t("provider.custom.title")} + {language.t("settings.providers.tag.custom")}
+

{language.t("settings.providers.custom.description")}

- { - dialog.show(() => ) - }} - > - {language.t("common.connect")} -
- + { + dialog.show(() => ) + }} + > + {language.t("common.connect")} + +
- - -
-
- 0} - fallback={
{pluginEmpty()}
} - > - - {(plugin) => ( -
-
- {plugin} -
- )} - - -
+ +
+
+ 0} + fallback={
{pluginEmpty()}
} + > + + {(plugin) => ( +
+
+ {plugin} +
+ )} + +
- -
+
+
) diff --git a/packages/app/src/components/status-popover-indicator.test.ts b/packages/app/src/components/status-popover-indicator.test.ts index d6d6d07a1d..e3c62d2a95 100644 --- a/packages/app/src/components/status-popover-indicator.test.ts +++ b/packages/app/src/components/status-popover-indicator.test.ts @@ -1,21 +1,11 @@ import { describe, expect, test } from "bun:test" -import { - hasNonBlockingServiceIssue, - hasServiceNeedingAttention, - serverStatusDotClass, -} from "./status-popover-indicator" +import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator" describe("serverStatusDotClass", () => { test("uses the success token while the server and services are healthy", () => { expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: false })).toBe("bg-icon-success-base") }) - test("uses the session attention token when a service needs attention", () => { - expect(serverStatusDotClass({ ready: true, serverHealth: true, attention: true, issue: true })).toBe( - "bg-v2-background-bg-accent", - ) - }) - test("uses the warning token for non-blocking issues while the server is online", () => { expect(serverStatusDotClass({ ready: true, serverHealth: true, issue: true })).toBe("bg-icon-warning-base") }) @@ -36,7 +26,7 @@ describe("hasNonBlockingServiceIssue", () => { expect(hasNonBlockingServiceIssue({ mcp: ["failed"], lsp: [] })).toBe(true) expect(hasNonBlockingServiceIssue({ mcp: ["needs_auth"], lsp: [] })).toBe(true) expect(hasNonBlockingServiceIssue({ mcp: ["needs_client_registration"], lsp: [] })).toBe(true) - expect(hasNonBlockingServiceIssue({ mcp: ["connected", "pending", "disabled"], lsp: [] })).toBe(false) + expect(hasNonBlockingServiceIssue({ mcp: ["connected", "disabled"], lsp: [] })).toBe(false) }) test("detects LSP failures that do not block chatting", () => { @@ -44,15 +34,3 @@ describe("hasNonBlockingServiceIssue", () => { expect(hasNonBlockingServiceIssue({ mcp: [], lsp: ["connected"] })).toBe(false) }) }) - -describe("hasServiceNeedingAttention", () => { - test("detects MCP states that need user attention", () => { - expect(hasServiceNeedingAttention({ mcp: ["needs_auth"] })).toBe(true) - expect(hasServiceNeedingAttention({ mcp: ["needs_client_registration"] })).toBe(true) - }) - - test("ignores states that do not need user attention", () => { - expect(hasServiceNeedingAttention({ mcp: ["failed"] })).toBe(false) - expect(hasServiceNeedingAttention({ mcp: ["connected", "pending", "disabled"] })).toBe(false) - }) -}) diff --git a/packages/app/src/components/status-popover-indicator.ts b/packages/app/src/components/status-popover-indicator.ts index 62fc3bedbf..efb7473753 100644 --- a/packages/app/src/components/status-popover-indicator.ts +++ b/packages/app/src/components/status-popover-indicator.ts @@ -1,29 +1,18 @@ -import type { LspStatus } from "@opencode-ai/sdk/v2/client" -import type { McpServer } from "@opencode-ai/client/promise" - -export function hasServiceNeedingAttention(input: { mcp: Array }) { - return input.mcp.some((status) => status === "needs_auth" || status === "needs_client_registration") -} +import type { LspStatus, McpStatus } from "@opencode-ai/sdk/v2/client" export function hasNonBlockingServiceIssue(input: { - mcp: Array + mcp: Array lsp: Array }) { return ( - input.mcp.some((status) => status !== "connected" && status !== "pending" && status !== "disabled") || + input.mcp.some((status) => status !== "connected" && status !== "disabled") || input.lsp.some((status) => status === "error") ) } -export function serverStatusDotClass(input: { - ready: boolean - serverHealth: boolean | undefined - attention?: boolean - issue: boolean -}) { +export function serverStatusDotClass(input: { ready: boolean; serverHealth: boolean | undefined; issue: boolean }) { if (input.serverHealth === false) return "bg-icon-critical-base" if (!input.ready || input.serverHealth === undefined) return "bg-border-weak-base" - if (input.attention) return "bg-v2-background-bg-accent" if (input.issue) return "bg-icon-warning-base" if (input.serverHealth === true) return "bg-icon-success-base" return "bg-border-weak-base" diff --git a/packages/app/src/components/status-popover.tsx b/packages/app/src/components/status-popover.tsx index 767e7e62c6..2aed45702a 100644 --- a/packages/app/src/components/status-popover.tsx +++ b/packages/app/src/components/status-popover.tsx @@ -9,11 +9,7 @@ import { ServerConnection, useServer } from "@/context/server" import { useServerSDK } from "@/context/server-sdk" import { useSync } from "@/context/sync" import { useGlobal } from "@/context/global" -import { - hasNonBlockingServiceIssue, - hasServiceNeedingAttention, - serverStatusDotClass, -} from "./status-popover-indicator" +import { hasNonBlockingServiceIssue, serverStatusDotClass } from "./status-popover-indicator" const Body = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverBody }))) const ServerBody = lazy(() => import("./status-popover-body").then((x) => ({ default: x.StatusPopoverServerBody }))) @@ -26,11 +22,6 @@ export function StatusPopover() { const [shown, setShown] = createSignal(false) const serverHealth = () => global.servers.health[server.key]?.healthy const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) - const attention = createMemo(() => - hasServiceNeedingAttention({ - mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), - }), - ) const issue = createMemo(() => hasNonBlockingServiceIssue({ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), @@ -58,7 +49,6 @@ export function StatusPopover() { class={`absolute -top-px -right-px size-1.5 rounded-full ${serverStatusDotClass({ ready: ready(), serverHealth: serverHealth(), - attention: attention(), issue: issue(), })}`} /> @@ -95,11 +85,6 @@ function DirectoryStatusPopover() { const [shown, setShown] = createSignal(false) const serverHealth = () => global.servers.health[ServerConnection.key(server().server)]?.healthy const ready = createMemo(() => serverHealth() === false || (sync().data.mcp_ready && sync().data.lsp_ready)) - const attention = createMemo(() => - hasServiceNeedingAttention({ - mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), - }), - ) const issue = createMemo(() => hasNonBlockingServiceIssue({ mcp: Object.values(sync().data.mcp ?? {}).map((item) => item.status), @@ -110,7 +95,6 @@ function DirectoryStatusPopover() { shown: shown(), ready: ready(), serverHealth: serverHealth(), - attention: attention(), issue: issue(), label: language.t("status.popover.trigger"), onOpenChange: setShown, @@ -134,7 +118,6 @@ function ServerStatusPopover() { shown: shown(), ready: serverHealth() !== undefined, serverHealth: serverHealth(), - attention: false, issue: false, label: language.t("status.popover.trigger"), onOpenChange: setShown, @@ -152,7 +135,6 @@ type StatusPopoverState = { shown: boolean ready: boolean serverHealth: boolean | undefined - attention: boolean issue: boolean label: string onOpenChange: (value: boolean) => void diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 7d25e7c649..5a0b510787 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -178,6 +178,7 @@ export const Terminal = (props: TerminalProps) => { // Terminal captures its connection for the PTY lifetime, so callers must key it per server/session. const connection = useServerSDK()().server const directory = sdk().directory + const client = sdk().client const url = sdk().url const auth = connection.http const username = auth?.username ?? "opencode" @@ -240,21 +241,10 @@ export const Terminal = (props: TerminalProps) => { } } - const pushSize = async (cols: number, rows: number) => { - if ((await sdk().protocol) === "v1") { - return sdk() - .client.pty.update({ - ptyID: id, - size: { cols, rows }, - }) - .catch((err) => { - debugTerminal("failed to sync terminal size", err) - }) - } - return sdk() - .api.pty.update({ + const pushSize = (cols: number, rows: number) => { + return client.pty + .update({ ptyID: id, - location: { directory }, size: { cols, rows }, }) .catch((err) => { @@ -302,12 +292,7 @@ export const Terminal = (props: TerminalProps) => { const scheduleSize = (cols: number, rows: number) => { if (disposed) return - if (lastSize?.cols === cols && lastSize?.rows === rows) { - pendingSize = undefined - if (sizeTimer !== undefined) clearTimeout(sizeTimer) - sizeTimer = undefined - return - } + if (lastSize?.cols === cols && lastSize?.rows === rows) return pendingSize = { cols, rows } @@ -332,10 +317,8 @@ export const Terminal = (props: TerminalProps) => { createEffect(() => { const colors = terminalColors() - const mode = theme.mode() === "dark" ? "dark" : "light" if (!term) return setOptionIfSupported(term, "theme", colors) - setOptionIfSupported(term, "colorScheme", mode) }) createEffect(() => { @@ -382,11 +365,7 @@ export const Terminal = (props: TerminalProps) => { event.preventDefault() event.stopImmediatePropagation() - if (URL.canParse(text) && new URL(text).protocol === "file:" && platform.openLocalFile) { - platform.openLocalFile(text) - return - } - platform.openExternal(text) + platform.openLink(text) } onMount(() => { @@ -417,7 +396,6 @@ export const Terminal = (props: TerminalProps) => { } _ghostty = g term = t - setOptionIfSupported(t, "colorScheme", theme.mode() === "dark" ? "dark" : "light") output = terminalWriter((data, done) => t.write(data, () => { done?.() @@ -536,54 +514,34 @@ export const Terminal = (props: TerminalProps) => { local.onConnectError?.(err) } - const gone = async () => { - if ((await sdk().protocol) === "v1") { - return sdk() - .client.pty.get({ ptyID: id }, { throwOnError: false }) - .then((result) => result.response.status === 404) - .catch((err) => { - debugTerminal("failed to inspect terminal session", err) - return false - }) - } - return sdk() - .api.pty.get({ ptyID: id, location: { directory } }) - .then((result) => result.data.status === "exited") + const gone = () => + client.pty + .get({ ptyID: id }, { throwOnError: false }) + .then((result) => result.response.status === 404) .catch((err) => { - if (err && typeof err === "object" && "_tag" in err && err._tag === "PtyNotFoundError") return true debugTerminal("failed to inspect terminal session", err) return false }) - } const connectToken = async () => { - if ((await sdk().protocol) === "v1") { - const result = await sdk() - .client.pty.connectToken( - { ptyID: id, directory }, - { - throwOnError: false, - headers: { "x-opencode-ticket": "1" }, - }, - ) - .catch((err: unknown) => { - if (err instanceof Error && err.message.includes("Request is not supported")) return - throw err - }) - if (!result) return - if (result.response.status === 200 && result.data?.ticket) return result.data.ticket - if (result.response.status === 404 || result.response.status === 405) return - if (result.response.status === 403) - throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.") - throw new Error(`PTY connect ticket failed with ${result.response.status}`) - } - // return sdk() - // .api.pty.connectToken({ - // ptyID: id, - // location: { directory }, - // "x-opencode-ticket": "1", - // }) - // .then((result) => result.data.ticket) + const result = await client.pty + .connectToken( + { ptyID: id, directory }, + { + throwOnError: false, + headers: { "x-opencode-ticket": "1" }, + }, + ) + .catch((err: unknown) => { + if (err instanceof Error && err.message.includes("Request is not supported")) return + throw err + }) + if (!result) return + if (result.response.status === 200 && result.data?.ticket) return result.data.ticket + if (result.response.status === 404 || result.response.status === 405) return + if (result.response.status === 403) + throw new Error("PTY connect ticket rejected by origin or CSRF checks. Check the server CORS config.") + throw new Error(`PTY connect ticket failed with ${result.response.status}`) } const retry = (err: unknown) => { @@ -613,14 +571,11 @@ export const Terminal = (props: TerminalProps) => { fail(err) return undefined }) - const protocol = await sdk().protocol - // if (protocol === "v2" && !ticket) return if (once.value) return if (disposed) return const socket = new WebSocket( terminalWebSocketURL({ - protocol, url, id, directory, @@ -640,7 +595,6 @@ export const Terminal = (props: TerminalProps) => { tries = 0 local.onConnect?.() scheduleSize(t.cols, t.rows) - if (t.getMode(2031)) t.write("\x1b[?996n") } const handleMessage = (event: MessageEvent) => { diff --git a/packages/app/src/components/titlebar-tab-gesture.ts b/packages/app/src/components/titlebar-tab-gesture.ts index 0aa5118936..329bf5e864 100644 --- a/packages/app/src/components/titlebar-tab-gesture.ts +++ b/packages/app/src/components/titlebar-tab-gesture.ts @@ -12,6 +12,6 @@ export function forwardTabRef(ref: Ref | undefined, element: HTM if (typeof ref === "function") ref(element) } -export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, pending: boolean) { - return !dragging && !editing && !pending +export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, committing: boolean) { + return !dragging && !editing && !committing } diff --git a/packages/app/src/components/titlebar-tab-nav.css b/packages/app/src/components/titlebar-tab-nav.css index 13dfbe04fc..08bed7cf26 100644 --- a/packages/app/src/components/titlebar-tab-nav.css +++ b/packages/app/src/components/titlebar-tab-nav.css @@ -9,86 +9,53 @@ justify-content: center; } -[data-titlebar-tab] { - --tab-base: var(--v2-background-bg-deep); - --tab-overlay: transparent; - background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base); -} - -[data-titlebar-tab]:is(:hover, :has(> [data-slot="tab-link"]:focus-visible)):not([data-state="pressed"]):not( - [data-dragging="true"] - ):not([data-editing="true"]) { - --tab-base: var(--v2-background-bg-layer-02); - --tab-overlay: var(--v2-overlay-simple-overlay-hover); -} - -[data-titlebar-tab]:is([data-state="pressed"], [data-dragging="true"], [data-editing="true"]) { - --tab-base: var(--v2-background-bg-layer-02); - --tab-overlay: var(--v2-overlay-simple-overlay-pressed); -} - -[data-titlebar-tab]:is(:hover, [data-state="pressed"]) [data-slot="tab-close"] { - background: linear-gradient(var(--tab-overlay), var(--tab-overlay)), var(--tab-base); -} - [data-titlebar-tab][data-editing="true"] [data-slot="tab-close"] { display: none; } [data-titlebar-tab-list] { - gap: 6px; + gap: 13.5px; } [data-titlebar-tab-slot] { - --tab-separator: var(--v2-background-bg-layer-03); position: relative; } -[data-color-scheme="dark"] [data-titlebar-tab-slot] { - --tab-separator: var(--v2-background-bg-layer-02); -} - [data-titlebar-tab-slot]:not(:first-child):not([data-active="true"])::before { content: ""; position: absolute; top: 8px; - left: -3.75px; + left: -6.75px; width: 1.5px; height: 12px; border-radius: 9999px; - background: var(--tab-separator); + background: var(--v2-background-bg-layer-02); } [data-titlebar-tab-slot][data-active="true"] + [data-titlebar-tab-slot]::before { display: none; } -[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover)::before, -[data-titlebar-tab-slot]:not([data-active="true"]):has([data-titlebar-tab]:hover) + [data-titlebar-tab-slot]::before { +[data-titlebar-tab] [data-slot="tab-close"]::before { + content: ""; + position: absolute; + top: 0; + height: 28px; + width: 16px; + pointer-events: none; + background: linear-gradient(to right, transparent, var(--tab-bg)); display: none; } -[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-link"] { - --tab-title-fade-offset: 4px; - -webkit-mask-image: linear-gradient( - to right, - black 0, - black calc(100% - var(--tab-title-fade-offset) - 16px), - transparent calc(100% - var(--tab-title-fade-offset)), - transparent 100% - ); - mask-image: linear-gradient( - to right, - black 0, - black calc(100% - var(--tab-title-fade-offset) - 16px), - transparent calc(100% - var(--tab-title-fade-offset)), - transparent 100% - ); +[data-titlebar-tab][data-title-overflow="true"]:not([data-editing="true"]) [data-slot="tab-close"]::before { + display: block; + right: 0; } -[data-titlebar-tab][data-title-overflow="true"]:is(:hover, [data-active="true"]):not([data-editing="true"]) - [data-slot="tab-link"] { - --tab-title-fade-offset: 24px; +[data-titlebar-tab][data-title-overflow="true"]:hover:not([data-editing="true"]) [data-slot="tab-close"]::before, +[data-titlebar-tab][data-title-overflow="true"][data-active="true"]:not([data-editing="true"]) + [data-slot="tab-close"]::before { + right: 100%; } [data-titlebar-tab][data-title-overflow="true"]:not(:hover):not([data-active="true"]):not([data-editing="true"]) diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 5003d0ed30..a397046f9b 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -1,13 +1,14 @@ import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" -import { createMutation } from "@tanstack/solid-query" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { useGlobal } from "@/context/global" +import { useLanguage } from "@/context/language" import { ServerConnection, serverName } from "@/context/server" import { displayName, projectForSession } from "@/pages/layout/helpers" import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar" +import { showToast } from "@/utils/toast" import type { Session } from "@opencode-ai/sdk/v2" import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture" import { TabPreviewPopover } from "./titlebar-tab-popover" @@ -22,7 +23,8 @@ export function TabNavItem(props: { server: ServerConnection.Key session: () => Session | undefined fallbackTitle?: string - onRename: (title: string) => Promise + onTitleChange?: (title: string) => void + onTitleChangeFailed?: (title: string) => void onClose: () => void onNavigate: () => void active?: boolean @@ -32,12 +34,13 @@ export function TabNavItem(props: { pressed?: boolean hidden?: boolean }) { + const language = useLanguage() const [editing, setEditing] = createSignal(false) const [titleOverflowing, setTitleOverflowing] = createSignal(false) let tabRoot!: HTMLDivElement let titleEl!: HTMLSpanElement + let committing = false let measureFrame: number | undefined - const rename = createMutation(() => ({ mutationFn: props.onRename })) const closeTab = (event: MouseEvent) => { event.preventDefault() @@ -113,20 +116,41 @@ export function TabNavItem(props: { selection?.addRange(range) } + const rename = async (title: string) => { + const ctx = serverCtx() + const session = props.session() + if (!ctx || !session) return + const client = ctx.sdk.createClient({ directory: session.directory, throwOnError: true }) + await client.session.update({ sessionID: session.id, title }) + } + const closeRename = async (save: boolean) => { - if (rename.isPending || !editing()) return + if (committing || !editing()) return + committing = true const original = props.session()?.title ?? "" const next = (titleEl.textContent ?? "").trim() titleEl.scrollLeft = 0 + if (save && next && next !== original) props.onTitleChange?.(next) setEditing(false) if (!save || !next || next === original) { + committing = false return } - await rename.mutateAsync(next) + try { + await rename(next) + } catch (err) { + props.onTitleChangeFailed?.(original) + showToast({ + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : undefined, + }) + } + + committing = false } createEffect(() => { @@ -140,7 +164,7 @@ export function TabNavItem(props: { const openRename = (event: MouseEvent) => { event.preventDefault() event.stopPropagation() - if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return + if (!canOpenTabRename(props.dragging, editing(), committing)) return const session = props.session() if (!session) return titleEl.textContent = session.title @@ -180,11 +204,11 @@ export function TabNavItem(props: { data-slot="titlebar-tab-item" data-title-overflow={titleOverflowing()} data-editing={editing()} - class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] px-1.5 [container-type:inline-size]" + class="group relative flex h-7 w-full min-w-0 select-none flex-row items-center gap-1.5 overflow-hidden whitespace-nowrap rounded-[6px] bg-[var(--tab-bg)] px-1.5 [container-type:inline-size] [--tab-bg:var(--v2-background-bg-deep)] hover:[--tab-bg:var(--v2-background-bg-layer-02)] has-[>a:focus-visible]:[--tab-bg:var(--v2-background-bg-layer-02)] data-[active='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[dragging='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[pressed='true']:[--tab-bg:var(--v2-background-bg-layer-02)] data-[editing='true']:[--tab-bg:var(--v2-background-bg-layer-02)]" classList={{ invisible: props.hidden }} data-active={props.active} data-dragging={props.dragging} - data-state={props.active || props.pressed ? "pressed" : undefined} + data-pressed={props.pressed} onMouseDown={(event) => { if (event.button !== MIDDLE_MOUSE_BUTTON) return event.preventDefault() @@ -195,89 +219,90 @@ export function TabNavItem(props: { closeTab(event) }} > - { - event.preventDefault() - event.stopPropagation() - }} - onMouseDown={(event) => { - // Navigate on mousedown to shave the press-release delay off tab switches. - if (event.button !== 0) return - if (editing()) return - if (props.suppressNavigation?.()) return - props.onNavigate() - }} - onClick={(event) => { - event.preventDefault() - // Mouse navigation already happened on mousedown; detail 0 means keyboard activation. - if (event.detail > 0) return - if (editing()) return - if (props.suppressNavigation?.()) return - props.onNavigate() - }} - class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]" - > - - - - { - titleEl = el - titleEl.textContent = title() ?? "" - }} - data-slot="tab-title" - data-titlebar-tab-title - class="min-w-0 flex-1 outline-none leading-4" - classList={{ - "overflow-hidden text-clip whitespace-nowrap": !editing(), - "select-text": editing(), - }} - contenteditable={editing() ? true : undefined} - onDblClick={openRename} - onKeyDown={(event) => { - event.stopPropagation() - if (event.key === "Enter") { - event.preventDefault() - void closeRename(true) - return - } - if (event.key !== "Escape") return + + { event.preventDefault() - titleEl.textContent = props.session()?.title ?? "" - void closeRename(false) - }} - onBlur={() => void closeRename(true)} - onPointerDown={(event) => { - if (!editing()) return event.stopPropagation() }} + onMouseDown={(event) => { + // Navigate on mousedown to shave the press-release delay off tab switches. + if (event.button !== 0) return + if (editing()) return + if (props.suppressNavigation?.()) return + props.onNavigate() + }} onClick={(event) => { - if (!editing()) return event.preventDefault() + // Mouse navigation already happened on mousedown; detail 0 means keyboard activation. + if (event.detail > 0) return + if (editing()) return + if (props.suppressNavigation?.()) return + props.onNavigate() }} - /> - + class="flex h-full min-w-0 flex-1 flex-row items-center gap-1.5 text-[13px] font-medium text-v2-text-text-faint group-data-[active='true']:text-v2-text-text-base group-data-[editing='true']:text-v2-text-text-base [-webkit-user-drag:none]" + > + + + + { + titleEl = el + titleEl.textContent = title() ?? "" + }} + data-slot="tab-title" + data-titlebar-tab-title + class="min-w-0 flex-1 outline-none leading-4" + classList={{ + "overflow-hidden text-clip whitespace-nowrap": !editing(), + "select-text": editing(), + }} + contenteditable={editing() ? true : undefined} + onDblClick={openRename} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { + event.preventDefault() + void closeRename(true) + return + } + if (event.key !== "Escape") return + event.preventDefault() + titleEl.textContent = props.session()?.title ?? "" + void closeRename(false) + }} + onBlur={() => void closeRename(true)} + onPointerDown={(event) => { + if (!editing()) return + event.stopPropagation() + }} + onClick={(event) => { + if (!editing()) return + event.preventDefault() + }} + /> + +
-
+
{ if (event.button !== MIDDLE_MOUSE_BUTTON) return @@ -382,7 +407,7 @@ export function DraftTabItem(props: { {props.title} -
+
{ - test("follows the visible left-to-right order", () => { - expect(adjacentTabKey(["c", "a", "b"], "c", 1)).toBe("a") - expect(adjacentTabKey(["c", "a", "b"], "a", -1)).toBe("c") - }) - - test("skips tabs omitted from the visible order", () => { - expect(adjacentTabKey(["a", "c"], "a", 1)).toBe("c") - expect(adjacentTabKey(["a", "c"], "c", 1)).toBe("a") - }) -}) - -test("merges reordered visible tabs around hidden tabs", () => { - expect(mergeVisibleTabOrder(["a", "hidden", "b", "c"], ["a", "b", "c"], ["c", "a", "b"])).toEqual([ - "c", - "hidden", - "a", - "b", - ]) -}) diff --git a/packages/app/src/components/titlebar-tab-order.ts b/packages/app/src/components/titlebar-tab-order.ts deleted file mode 100644 index a906b1fb32..0000000000 --- a/packages/app/src/components/titlebar-tab-order.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function adjacentTabKey(order: string[], current: string | undefined, offset: -1 | 1) { - if (!current || order.length === 0) return - const index = order.indexOf(current) - if (index === -1) return - return order[(index + offset + order.length) % order.length] -} - -export function mergeVisibleTabOrder(all: string[], current: string[], next: string[]) { - const visible = new Set(current) - const reordered = next.values() - return all.map((key) => (visible.has(key) ? (reordered.next().value ?? key) : key)) -} diff --git a/packages/app/src/components/titlebar-tab-strip.tsx b/packages/app/src/components/titlebar-tab-strip.tsx index 0a4b4da12d..7ef53324cd 100644 --- a/packages/app/src/components/titlebar-tab-strip.tsx +++ b/packages/app/src/components/titlebar-tab-strip.tsx @@ -1,5 +1,4 @@ -import { createEffect, createMemo, createResource, createRoot, For, onCleanup, onMount, Show } from "solid-js" -import { createStore } from "solid-js/store" +import { createEffect, createMemo, createResource, createRoot, For, onCleanup, onMount } from "solid-js" import { createResizeObserver } from "@solid-primitives/resize-observer" import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" import { isSortable, useSortable } from "@dnd-kit/solid/sortable" @@ -16,10 +15,7 @@ import { useCommand } from "@/context/command" import { useTabs } from "@/context/tabs" import { createTabPromptState } from "@/context/prompt" import { base64Encode } from "@opencode-ai/core/util/encode" -import { showToast } from "@/utils/toast" import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture" -import { adjacentTabKey, mergeVisibleTabOrder } from "./titlebar-tab-order" -import type { Session } from "@opencode-ai/sdk/v2" function SessionTabSlot(props: { tab: SessionTab @@ -27,12 +23,12 @@ function SessionTabSlot(props: { index: () => number active: () => boolean forceTruncate: boolean - session: () => Session | undefined - fallbackTitle?: string - onRename: (title: string) => Promise + serverCtx: () => ServerCtx | undefined onNavigate: (element: HTMLDivElement) => void onClose: () => void }) { + const tabs = useTabs() + const language = useLanguage() const sortable = useSortable({ get id() { return props.id @@ -42,47 +38,6 @@ function SessionTabSlot(props: { }, }) let ref!: HTMLDivElement - - return ( -
- { - ref = el - }} - href={tabHref(props.tab)} - server={props.tab.server} - session={props.session} - fallbackTitle={props.fallbackTitle} - onRename={props.onRename} - onNavigate={() => props.onNavigate(ref)} - onClose={props.onClose} - active={props.active()} - forceTruncate={props.forceTruncate} - dragging={sortable.isDragSource()} - /> -
- ) -} - -function SessionTabEntry(props: { - tab: SessionTab - id: string - index: () => number - active: () => boolean - forceTruncate: boolean - serverCtx: () => ServerCtx | undefined - onVisibleChange: (visible: boolean) => void - onNavigate: (element: HTMLDivElement) => void - onClose: () => void -}) { - const tabs = useTabs() - const language = useLanguage() const sdk = createMemo(() => props.serverCtx()?.sdk ?? null) const cachedSession = createMemo(() => props.serverCtx()?.sync.session.peek(props.tab.sessionId)) const persisted = createMemo(() => tabs.info[props.id]) @@ -95,30 +50,8 @@ function SessionTabEntry(props: { ) const session = createMemo(() => cachedSession() ?? loadedSession()) const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session()) - const visible = createMemo(() => !!session() || missingSession() || !!persisted()?.title) let prefetched = false - const rename = async (title: string) => { - const value = session() - const ctx = props.serverCtx() - if (!value || !ctx) return - - ctx.sync.session.remember({ ...value, title }) - try { - await ctx.sdk.api.session.rename({ sessionID: value.id, title }) - } catch (err) { - const current = session() - const currentCtx = props.serverCtx() - if (current && currentCtx) currentCtx.sync.session.remember({ ...current, title: value.title }) - showToast({ - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : undefined, - }) - } - } - - createEffect(() => props.onVisibleChange(visible())) - createEffect(() => { const ctx = props.serverCtx() const value = session() @@ -150,20 +83,39 @@ function SessionTabEntry(props: { }) return ( - - + { + ref = el + }} + href={tabHref(props.tab)} + server={props.tab.server} session={session} fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)} - onRename={rename} - onNavigate={props.onNavigate} + onTitleChange={(title) => { + const value = session() + const ctx = props.serverCtx() + if (value && ctx) ctx.sync.session.remember({ ...value, title }) + }} + onTitleChangeFailed={(title) => { + const value = session() + const ctx = props.serverCtx() + if (value && ctx) ctx.sync.session.remember({ ...value, title }) + }} + onNavigate={() => props.onNavigate(ref)} onClose={props.onClose} + active={props.active()} + forceTruncate={props.forceTruncate} + dragging={sortable.isDragSource()} /> - +
) } @@ -220,39 +172,11 @@ export function TitlebarTabStrip(props: { }) { const global = useGlobal() const language = useLanguage() - const command = useCommand() let scrollRef!: HTMLDivElement let listRef!: HTMLDivElement let resizeFrame: number | undefined - const [visibility, setVisibility] = createStore>({}) - const visibleTabs = createMemo(() => props.tabs.filter((tab) => tab.type === "draft" || visibility[tabKey(tab)])) - const visibleTabIds = () => visibleTabs().map(tabKey) - command.register("titlebar-tab-cycle", () => [ - { - id: `tab.prev`, - category: "tab", - title: "", - keybind: `mod+option+ArrowLeft,ctrl+shift+tab`, - hidden: true, - onSelect: () => selectAdjacentTab(-1), - }, - { - id: `tab.next`, - category: "tab", - title: "", - keybind: `mod+option+ArrowRight,ctrl+tab`, - hidden: true, - onSelect: () => selectAdjacentTab(1), - }, - ]) - - function selectAdjacentTab(offset: -1 | 1) { - const current = props.currentTab() - const key = adjacentTabKey(visibleTabIds(), current ? tabKey(current) : undefined, offset) - const next = props.tabs.find((tab) => tabKey(tab) === key) - if (next) props.onNavigate(next) - } + const tabIds = () => props.tabs.map(tabKey) function refreshOverflow() { if (!scrollRef) return @@ -280,7 +204,7 @@ export function TitlebarTabStrip(props: { createEffect(() => { props.tabs.length - visibleTabIds() + tabIds() refreshOverflow() }) @@ -316,29 +240,22 @@ export function TitlebarTabStrip(props: { props.onNavigate(tab, tabEl ?? undefined) }} onDragEnd={(event) => { - const current = visibleTabIds() + const current = tabIds() const source = event.operation.source if (event.canceled || !isSortable(source)) return const { initialIndex, index } = source if (initialIndex !== index) { - props.onReorder( - mergeVisibleTabOrder( - props.tabs.map(tabKey), - current, - arrayMove(current, source.initialIndex, source.index), - ), - ) + props.onReorder(arrayMove(current, source.initialIndex, source.index)) } }} >
- {(tab) => { + {(tab, index) => { const id = tabKey(tab) let ref!: HTMLDivElement - const visibleIndex = () => visibleTabs().findIndex((item) => tabKey(item) === id) - useTabShortcut(visibleIndex, () => props.onNavigate(tab, ref)) + useTabShortcut(index, () => props.onNavigate(tab, ref)) const serverCtx = createMemo(() => { if (tab.type !== "session") return const conn = global.servers.list().find((item) => ServerConnection.key(item) === tab.server) @@ -347,14 +264,13 @@ export function TitlebarTabStrip(props: { if (tab.type === "session") { return ( - props.currentTab() === tab} forceTruncate={props.forceTruncate} serverCtx={serverCtx} - onVisibleChange={(visible) => setVisibility(id, visible)} onNavigate={(element) => { ref = element props.onNavigate(tab, element) @@ -368,7 +284,7 @@ export function TitlebarTabStrip(props: { props.currentTab() === tab} title={language.t("command.session.new")} onNavigate={(element) => { @@ -402,7 +318,7 @@ function useTabShortcut(index: () => number, onSelect: () => void) { command.register(() => { const number = index() + 1 - if (number < 1 || number > 9) return [] + if (number > 9) return [] return [ { id: `tab.${number}`, diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 649a8f5777..4578656107 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -5,6 +5,7 @@ import { IconButton } from "@opencode-ai/ui/icon-button" import { Icon } from "@opencode-ai/ui/icon" import { Button } from "@opencode-ai/ui/button" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" +import { useTheme } from "@opencode-ai/ui/theme/context" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" @@ -27,13 +28,32 @@ import { tabKey, useTabs } from "@/context/tabs" import type { PromptSession } from "@/context/prompt" import "./titlebar.css" import { newTabTooltipKeybind } from "./command-tooltip-keybind" -import { normalizeSessionInfo } from "@/utils/session" +type TauriDesktopWindow = { + startDragging?: () => Promise + toggleMaximize?: () => Promise +} + +type TauriThemeWindow = { + setTheme?: (theme?: "light" | "dark" | null) => Promise +} + +type TauriApi = { + window?: { + getCurrentWindow?: () => TauriDesktopWindow + } + webviewWindow?: { + getCurrentWebviewWindow?: () => TauriThemeWindow + } +} + +const tauriApi = () => (window as unknown as { __TAURI__?: TauriApi }).__TAURI__ +const currentDesktopWindow = () => tauriApi()?.window?.getCurrentWindow?.() +const currentThemeWindow = () => tauriApi()?.webviewWindow?.getCurrentWebviewWindow?.() const legacyTitlebarHeight = 40 const v2TitlebarHeight = 36 const minTitlebarZoom = 0.25 const windowsControlsBaseWidth = 138 // 3 native Windows caption buttons at 46px each. -const macTrafficLightsBaseWidth = 84 export type TitlebarUpdate = { version: () => string | undefined @@ -53,6 +73,7 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl const command = useCommand() const language = useLanguage() const settings = useSettings() + const theme = useTheme() const server = useServer() const navigate = useNavigate() const location = useLocation() @@ -63,9 +84,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl const mac = createMemo(() => platform.platform === "desktop" && platform.os === "macos") const windows = createMemo(() => platform.platform === "desktop" && platform.os === "windows") + const electronWindows = createMemo(() => windows() && !tauriApi()) const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux") const web = createMemo(() => platform.platform === "web") - const macTrafficLights = createMemo(() => mac() && !platform.windowFullscreen?.()) const zoom = () => platform.webviewZoom?.() ?? 1 const titlebarZoom = () => (windows() ? Math.max(zoom(), minTitlebarZoom) : zoom()) const counterZoom = () => (windows() && titlebarZoom() < 1 ? 1 / titlebarZoom() : 1) @@ -154,6 +175,56 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl }, ]) + const getWin = () => { + if (platform.platform !== "desktop") return + return currentDesktopWindow() + } + + createEffect(() => { + if (platform.platform !== "desktop") return + + const scheme = theme.colorScheme() + const value = scheme === "system" ? null : scheme + + const win = currentThemeWindow() + if (!win?.setTheme) return + + void win.setTheme(value).catch(() => undefined) + }) + + const interactive = (target: EventTarget | null) => { + if (!(target instanceof Element)) return false + + const selector = + "button, a, input, textarea, select, option, [role='button'], [role='menuitem'], [contenteditable='true'], [contenteditable='']" + + return !!target.closest(selector) + } + + const drag = (e: MouseEvent) => { + if (platform.platform !== "desktop") return + if (e.buttons !== 1) return + if (interactive(e.target)) return + + const win = getWin() + if (!win?.startDragging) return + + e.preventDefault() + void win.startDragging().catch(() => undefined) + } + + const maximize = (e: MouseEvent) => { + if (platform.platform !== "desktop") return + if (interactive(e.target)) return + if (e.target instanceof Element && e.target.closest("[data-tauri-decorum-tb]")) return + + const win = getWin() + if (!win?.toggleMaximize) return + + e.preventDefault() + void win.toggleMaximize().catch(() => undefined) + } + return (
@@ -192,9 +267,9 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined }, ({ route, sdk }) => - sdk.api.session + sdk.client.session .get({ sessionID: route.sessionId }) - .then(normalizeSessionInfo) + .then((x) => x.data) .catch(() => {}), ) @@ -338,6 +413,40 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl keybind: "mod+shift+t", onSelect: () => tabsStoreActions.reopenClosedTab(), }, + { + id: `tab.prev`, + category: "tab", + title: "", + keybind: `mod+option+ArrowLeft,ctrl+shift+tab`, + hidden: true, + onSelect: () => { + let index = tabsStore.findIndex((tab) => tab === currentTab()) + if (index === -1) return + + index -= 1 + if (index === -1) index = tabsStore.length - 1 + + const next = tabsStore[index] + if (next) tabs.select(next) + }, + }, + { + id: `tab.next`, + category: "tab", + title: "", + keybind: `mod+option+ArrowRight,ctrl+tab`, + hidden: true, + onSelect: () => { + let index = tabsStore.findIndex((tab) => tab === currentTab()) + if (index === -1) return + + index += 1 + if (index === tabsStore.length) index = 0 + + const next = tabsStore[index] + if (next) tabs.select(next) + }, + }, ].filter((v) => v !== undefined) }) @@ -349,8 +458,8 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl classList={{ "pt-2": !bottom(), "pb-2": bottom(), - "md:pl-2": macTrafficLights(), - "md:pl-4": !macTrafficLights(), + "md:pl-2": mac(), + "md:pl-4": !mac(), }} > @@ -395,27 +504,32 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl }} onReorder={(keys) => tabsStoreActions.reorder(keys)} /> - - {language.t("command.session.new")} - - - } - > - } - onClick={openNewTab} - aria-label={language.t("command.session.new")} - /> - + + + {language.t("command.session.new")} + + + } + > + } + onClick={openNewTab} + aria-label={language.t("command.session.new")} + /> + +
+ +
+
) }} @@ -428,13 +542,14 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
+ {/*
*/}
-
+ {!tauriApi() &&
} +
diff --git a/packages/app/src/components/windows-app-menu.tsx b/packages/app/src/components/windows-app-menu.tsx index e41c2a584a..017ac4cf8d 100644 --- a/packages/app/src/components/windows-app-menu.tsx +++ b/packages/app/src/components/windows-app-menu.tsx @@ -43,7 +43,7 @@ export function WindowsAppMenu(props: { runAction(entry.action) return } - if (entry.href) props.platform.openExternal(entry.href) + if (entry.href) props.platform.openLink(entry.href) } return ( diff --git a/packages/app/src/context/command.test.ts b/packages/app/src/context/command.test.ts index e0d9057511..cf98ff3825 100644 --- a/packages/app/src/context/command.test.ts +++ b/packages/app/src/context/command.test.ts @@ -1,11 +1,5 @@ import { describe, expect, test } from "bun:test" -import { - activeCommandRegistrations, - addCommandRegistration, - commandPaletteOptions, - resolveKeybindOption, - type CommandOption, -} from "./command" +import { commandPaletteOptions, resolveKeybindOption, upsertCommandRegistration, type CommandOption } from "./command" const paletteOptions: CommandOption[] = [ { id: "settings.open", title: "Open settings" }, @@ -21,31 +15,22 @@ describe("commandPaletteOptions", () => { }) }) -describe("command registrations", () => { - test("shadows keyed registrations while retaining the previous owner", () => { +describe("upsertCommandRegistration", () => { + test("replaces keyed registrations", () => { const one = () => [{ id: "one", title: "One" }] const two = () => [{ id: "two", title: "Two" }] - const registrations = addCommandRegistration([{ key: "layout", options: one }], { - key: "layout", - options: two, - }) - const active = activeCommandRegistrations(registrations) + const next = upsertCommandRegistration([{ key: "layout", options: one }], { key: "layout", options: two }) - expect(registrations).toHaveLength(2) - expect(active).toHaveLength(1) - expect(active[0]?.options).toBe(two) - - const restored = activeCommandRegistrations(registrations.filter((entry) => entry.options !== two)) - expect(restored).toHaveLength(1) - expect(restored[0]?.options).toBe(one) + expect(next).toHaveLength(1) + expect(next[0]?.options).toBe(two) }) test("keeps unkeyed registrations additive", () => { const one = () => [{ id: "one", title: "One" }] const two = () => [{ id: "two", title: "Two" }] - const next = activeCommandRegistrations(addCommandRegistration([{ options: one }], { options: two })) + const next = upsertCommandRegistration([{ options: one }], { options: two }) expect(next).toHaveLength(2) expect(next[0]?.options).toBe(two) diff --git a/packages/app/src/context/command.tsx b/packages/app/src/context/command.tsx index 054b99e806..f88033bd7e 100644 --- a/packages/app/src/context/command.tsx +++ b/packages/app/src/context/command.tsx @@ -114,18 +114,9 @@ export type CommandRegistration = { options: Accessor } -export function addCommandRegistration(registrations: CommandRegistration[], entry: CommandRegistration) { - return [entry, ...registrations] -} - -export function activeCommandRegistrations(registrations: CommandRegistration[]) { - const keys = new Set() - return registrations.filter((entry) => { - if (entry.key === undefined) return true - if (keys.has(entry.key)) return false - keys.add(entry.key) - return true - }) +export function upsertCommandRegistration(registrations: CommandRegistration[], entry: CommandRegistration) { + if (entry.key === undefined) return [entry, ...registrations] + return [entry, ...registrations.filter((x) => x.key !== entry.key)] } export function parseKeybind(config: string): Keybind[] { @@ -290,7 +281,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex const seen = new Set() const all: CommandOption[] = [] - for (const reg of activeCommandRegistrations(store.registrations)) { + for (const reg of store.registrations) { for (const opt of reg.options()) { if (seen.has(opt.id)) { if (import.meta.env.DEV && !warnedDuplicates.has(opt.id)) { @@ -434,7 +425,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex key: id, options, } - setStore("registrations", (arr) => addCommandRegistration(arr, entry)) + setStore("registrations", (arr) => upsertCommandRegistration(arr, entry)) onCleanup(() => { setStore("registrations", (arr) => arr.filter((x) => x !== entry)) }) diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts index befd5b61e0..68e6b19cef 100644 --- a/packages/app/src/context/directory-sync.ts +++ b/packages/app/src/context/directory-sync.ts @@ -5,7 +5,6 @@ import { produce, reconcile, type SetStoreFunction } from "solid-js/store" import type { createServerSdkContext } from "./server-sdk" import type { createServerSyncContextInner } from "./server-sync" import type { State } from "./global-sync/types" -import { normalizeSessionInfo } from "@/utils/session" const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const sessionFields = new Set([ @@ -16,7 +15,6 @@ const sessionFields = new Set([ "permission", "question", "message", - "session_message", "part", "part_text_accum_delta", ]) @@ -116,6 +114,7 @@ export const createDirSyncContext = ( await serverSync.session.sync(sessionID, options) index(sessionID) }, + diff: serverSync.session.diff, todo: serverSync.session.todo, history: serverSync.session.history, evict(sessionID: string) { @@ -124,9 +123,9 @@ export const createDirSyncContext = ( fetch: async (count = 10) => { const [store, setStore] = current() setStore("limit", (value) => value + count) - const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" }) - const sessions = response.data - .map(normalizeSessionInfo) + const response = await client.session.list() + const sessions = (response.data ?? []) + .filter((session) => !!session?.id) .sort((a, b) => cmp(a.id, b.id)) .slice(0, store.limit) sessions.forEach(serverSync.session.remember) @@ -134,8 +133,7 @@ export const createDirSyncContext = ( }, more: createMemo(() => current()[0].session.length >= current()[0].limit), archive: async (sessionID: string) => { - if ((await serverSDK.protocol) !== "v1") return - await serverSDK.client.session.update({ sessionID, directory, time: { archived: Date.now() } }) + await serverSDK.client.session.update({ sessionID, time: { archived: Date.now() } }) current()[1]( "session", produce((draft) => { diff --git a/packages/app/src/context/file.tsx b/packages/app/src/context/file.tsx index fbbef3a2a8..6032b81dde 100644 --- a/packages/app/src/context/file.tsx +++ b/packages/app/src/context/file.tsx @@ -204,18 +204,10 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ } const search = (query: string, dirs: "true" | "false", options?: { limit?: number; signal?: AbortSignal }) => - serverSDK() - .api.file.find( - { - location: { directory: sdk().directory }, - query, - type: dirs === "true" ? "directory" : "file", - limit: options?.limit, - }, - { signal: options?.signal }, - ) + sdk() + .client.find.files({ query, dirs, limit: options?.limit }, { signal: options?.signal }) .then( - (x) => x.data.map((entry) => path.normalize(entry.path)), + (x) => (x.data ?? []).map(path.normalize), (error) => { if (options?.signal?.aborted) throw error return [] diff --git a/packages/app/src/context/file/path.test.ts b/packages/app/src/context/file/path.test.ts index 99dd88ae0e..feef6d466e 100644 --- a/packages/app/src/context/file/path.test.ts +++ b/packages/app/src/context/file/path.test.ts @@ -21,29 +21,6 @@ describe("file path helpers", () => { expect(path.normalize("c:\\repo\\src\\app.ts")).toBe("src\\app.ts") }) - test("normalizes Windows directory separators", () => { - const path = createPathHelpers(() => "C:\\repo") - expect(path.normalizeDir("frontend\\")).toBe("frontend") - expect(path.normalizeDir("frontend\\src\\")).toBe("frontend/src") - expect(path.normalizeDir("C:\\repo\\frontend\\")).toBe("frontend") - }) - - test("normalizes separators for Windows roots written with forward slashes", () => { - const path = createPathHelpers(() => "C:/repo") - expect(path.normalizeDir("frontend\\src\\")).toBe("frontend/src") - }) - - test("normalizes separators for Windows UNC roots", () => { - const path = createPathHelpers(() => "\\\\server\\share") - expect(path.normalizeDir("\\\\server\\share\\frontend\\")).toBe("frontend") - }) - - test("preserves backslashes in POSIX directory names", () => { - const path = createPathHelpers(() => "/repo") - expect(path.normalizeDir("literal\\name\\")).toBe("literal\\name\\") - expect(path.normalizeDir("literal\\name/")).toBe("literal\\name") - }) - test("keeps query/hash stripping behavior stable", () => { expect(stripQueryAndHash("a/b.ts#L12?x=1")).toBe("a/b.ts") expect(stripQueryAndHash("a/b.ts?x=1#L12")).toBe("a/b.ts") diff --git a/packages/app/src/context/file/path.ts b/packages/app/src/context/file/path.ts index 2bc4bde5e9..53f072b6cb 100644 --- a/packages/app/src/context/file/path.ts +++ b/packages/app/src/context/file/path.ts @@ -140,12 +140,7 @@ export function createPathHelpers(scope: () => string) { return normalize(tabValue) } - const normalizeDir = (input: string) => { - const path = normalize(input) - const root = scope() - const windows = /^[A-Za-z]:/.test(root) || root.startsWith("\\\\") - return (windows ? path.replace(/\\/g, "/") : path).replace(/\/+$/, "") - } + const normalizeDir = (input: string) => normalize(input).replace(/\/+$/, "") return { normalize, diff --git a/packages/app/src/context/global-sync/bootstrap.test.ts b/packages/app/src/context/global-sync/bootstrap.test.ts index a95706a3db..40735fb822 100644 --- a/packages/app/src/context/global-sync/bootstrap.test.ts +++ b/packages/app/src/context/global-sync/bootstrap.test.ts @@ -1,42 +1,14 @@ import { describe, expect, test } from "bun:test" import { createStore } from "solid-js/store" import { QueryClient } from "@tanstack/solid-query" -import type { Config, OpencodeClient, Project } from "@opencode-ai/sdk/v2/client" -import type { AgentApi, CatalogApi, CommandApi, ReferenceApi } from "@opencode-ai/client/promise" +import type { Config, OpencodeClient, Project, Session } from "@opencode-ai/sdk/v2/client" import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" -import { - bootstrapDirectory, - loadAgentsQuery, - loadCommands, - loadGlobalConfigQuery, - loadPathQuery, - loadProjectsQuery, - loadProvidersQuery, - loadReferencesQuery, -} from "./bootstrap" +import { bootstrapDirectory, loadPathQuery, loadProvidersQuery } from "./bootstrap" import type { State, VcsCache } from "./types" +import { createServerSession } from "../server-session" import { ServerScope } from "@/utils/server-scope" -import type { ServerApi } from "@/utils/server" - -type ProjectApi = ServerApi["project"] const provider = { all: new Map(), connected: [], default: {} } satisfies NormalizedProviderListResponse -const api = { - agent: { list: async () => ({ location: {}, data: [] }) }, - provider: { list: async () => ({ location: {}, data: [] }) }, - model: { - list: async () => ({ location: {}, data: [] }), - default: async () => ({ location: {}, data: null }), - }, - permission: { request: { list: async () => ({ location: {}, data: [] }) } }, - project: { - list: async () => [], - current: async () => ({ id: "project", directory: "/project" }), - }, - question: { request: { list: async () => ({ location: {}, data: [] }) } }, - reference: { list: async () => ({ location: {}, data: [] }) }, - vcs: { get: async () => ({ location: {}, data: {} }) }, -} as unknown as ServerApi function directoryState() { return createStore({ @@ -69,22 +41,20 @@ function directoryState() { vcs: undefined, limit: 5, message: {}, - session_message: {}, part: {}, part_text_accum_delta: {}, }) } describe("bootstrapDirectory", () => { - test("uses legacy MCP endpoints while refreshing a v1 directory", async () => { - const legacyConfigReads: string[] = [] + test("marks a loading directory partial during bootstrap and complete after success", async () => { const mcpReads: string[] = [] const [store, setStore] = directoryState() await bootstrapDirectory({ directory: "/project", scope: ServerScope.local, - mcp: true, + mcp: false, global: { config: {} satisfies Config, path: { state: "", config: "", worktree: "/project", directory: "/project", home: "/home" }, @@ -93,12 +63,7 @@ describe("bootstrapDirectory", () => { }, sdk: { app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) }, - config: { - get: async () => { - legacyConfigReads.push("directory") - return { data: {} } - }, - }, + config: { get: async () => ({ data: {} }) }, session: { status: async () => ({ data: {} }) }, vcs: { get: async () => ({ data: undefined }) }, command: { @@ -116,24 +81,14 @@ describe("bootstrapDirectory", () => { return { data: {} } }, }, - experimental: { - resource: { - list: async () => { - mcpReads.push("resource") - return { data: {} } - }, - }, - }, provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) }, } as unknown as OpencodeClient, - api, store, setStore, vcsCache: { setStore() {} } as unknown as VcsCache, loadSessions() {}, translate: (key) => key, queryClient: new QueryClient(), - protocol: Promise.resolve("v1"), }) expect(store.status).toBe("partial") @@ -141,12 +96,39 @@ describe("bootstrapDirectory", () => { await new Promise((resolve) => setTimeout(resolve, 80)) expect(store.status).toBe("complete") - expect(legacyConfigReads).toEqual(["directory"]) - expect(mcpReads.sort()).toEqual(["command", "resource", "status"]) + expect(mcpReads).toEqual([]) }) - test("skips legacy config while refreshing a v2 directory", async () => { + test("seeds session status even while warming session info stalls", async () => { const [store, setStore] = directoryState() + const stalled = Promise.withResolvers() + const client = { + app: { agents: async () => ({ data: [{ name: "build", mode: "primary" }] }) }, + config: { get: async () => ({ data: {} }) }, + session: { + status: async () => ({ data: { ses_busy: { type: "busy" } } }), + get: () => stalled.promise, + }, + vcs: { get: async () => ({ data: undefined }) }, + command: { list: async () => ({ data: [] }) }, + permission: { list: async () => ({ data: [] }) }, + question: { list: async () => ({ data: [] }) }, + v2: { reference: { list: async () => ({ data: { data: [] } }) } }, + mcp: { status: async () => ({ data: {} }) }, + provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) }, + } as unknown as OpencodeClient + const session = createServerSession(client) + const stale: Session = { + id: "ses_stale", + slug: "ses_stale", + projectID: "project", + directory: "/project", + title: "stale", + version: "1", + time: { created: 1, updated: 1 }, + } + session.remember(stale) + session.set("session_status", stale.id, { type: "busy" }) await bootstrapDirectory({ directory: "/project", @@ -158,173 +140,37 @@ describe("bootstrapDirectory", () => { project: [{ id: "project", worktree: "/project" } as Project], provider, }, - sdk: { - config: { - get: async () => { - throw new Error("legacy directory config should not be called") - }, - }, - } as unknown as OpencodeClient, - api, + sdk: client, store, setStore, vcsCache: { setStore() {} } as unknown as VcsCache, loadSessions() {}, translate: (key) => key, queryClient: new QueryClient(), - protocol: Promise.resolve("v2"), + session, }) - expect(store.status).toBe("partial") + const deadline = Date.now() + 500 + while (!session.data.session_working("ses_busy") && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 10)) + } - await new Promise((resolve) => setTimeout(resolve, 80)) - - expect(store.status).toBe("complete") - }) -}) - -describe("config queries", () => { - test("skips legacy global config for v2 servers", async () => { - const sdk = { - global: { - config: { - get: async () => { - throw new Error("legacy global config should not be called") - }, - }, - }, - } as unknown as OpencodeClient - - const result = await new QueryClient().fetchQuery( - loadGlobalConfigQuery(ServerScope.local, sdk, Promise.resolve("v2")), - ) - - expect(result).toEqual({}) - }) - - test("loads legacy global config for v1 servers", async () => { - const calls: string[] = [] - const config = { shell: "zsh" } satisfies Config - const sdk = { - global: { - config: { - get: async () => { - calls.push("global") - return { data: config } - }, - }, - }, - } as unknown as OpencodeClient - - const result = await new QueryClient().fetchQuery( - loadGlobalConfigQuery(ServerScope.local, sdk, Promise.resolve("v1")), - ) - - expect(result).toEqual(config) - expect(calls).toEqual(["global"]) + expect(session.data.session_status["ses_busy"]?.type).toBe("busy") + expect(session.data.session_status[stale.id]).toBeUndefined() }) }) describe("query keys", () => { test("partitions identical directories by server scope", () => { - const client = {} as Parameters[2] - const api = {} as CatalogApi + const client = {} as OpencodeClient const remote = "https://debian.example" as typeof ServerScope.local expect([...loadPathQuery(ServerScope.local, "/repo", client).queryKey]).toEqual(["local", "/repo", "path"]) expect([...loadPathQuery(remote, "/repo", client).queryKey]).toEqual(["https://debian.example", "/repo", "path"]) - expect([...loadProvidersQuery(remote, null, api).queryKey]).toEqual(["https://debian.example", null, "providers"]) - }) - - test("loads the current provider and model catalog", async () => { - const calls: unknown[] = [] - const api = { - provider: { - list: async (input: unknown) => { - calls.push(["provider", input]) - return { location: {}, data: [{ id: "openai", name: "OpenAI", package: "@ai-sdk/openai" }] } - }, - }, - model: { - list: async (input: unknown) => { - calls.push(["model", input]) - return { location: {}, data: [] } - }, - default: async (input: unknown) => { - calls.push(["default", input]) - return { location: {}, data: null } - }, - }, - } as unknown as CatalogApi - - const result = await new QueryClient().fetchQuery(loadProvidersQuery(ServerScope.local, "/repo", api)) - - expect(calls).toEqual([ - ["provider", { location: { directory: "/repo" } }], - ["model", { location: { directory: "/repo" } }], - ["default", { location: { directory: "/repo" } }], + expect([...loadProvidersQuery(remote, null, client).queryKey]).toEqual([ + "https://debian.example", + null, + "providers", ]) - expect(result.connected).toEqual(["openai"]) - }) - - test("loads agents from the current location-scoped endpoint", async () => { - const calls: unknown[] = [] - const api = { - list: async (input: unknown) => { - calls.push(input) - return { location: {}, data: [] } - }, - } as unknown as AgentApi - - const result = await new QueryClient().fetchQuery(loadAgentsQuery(ServerScope.local, "/repo", api)) - - expect(calls).toEqual([{ location: { directory: "/repo" } }]) - expect(result).toEqual([]) - }) - - test("loads commands from the current location-scoped endpoint", async () => { - const calls: unknown[] = [] - const api = { - list: async (input: unknown) => { - calls.push(input) - return { - location: {}, - data: [{ name: "review", template: "Review files" /* source: "command" as const */ }], - } - }, - } as unknown as CommandApi - - const result = await loadCommands("/repo", api) - - expect(calls).toEqual([{ location: { directory: "/repo" } }]) - expect(result).toEqual([{ name: "review", template: "Review files" /* source: "command" */ }]) - }) - - test("loads projects from the current endpoint", async () => { - const api = { - list: async () => [ - { id: "b", worktree: "/b", time: { created: 1, updated: 1 }, sandboxes: [] }, - { id: "a", worktree: "/a", time: { created: 1, updated: 1 }, sandboxes: [] }, - ], - } as unknown as ProjectApi - - const result = await new QueryClient().fetchQuery(loadProjectsQuery(ServerScope.local, api)) - - expect(result.map((project) => project.id)).toEqual(["a", "b"]) - }) - - test("loads references from the current location-scoped endpoint", async () => { - const calls: unknown[] = [] - const api = { - list: async (input: unknown) => { - calls.push(input) - return { location: {}, data: [{ name: "AGENTS.md", path: "/repo/AGENTS.md", source: "instructions" }] } - }, - } as unknown as ReferenceApi - - const result = await new QueryClient().fetchQuery(loadReferencesQuery(ServerScope.local, "/repo", api)) - - expect(calls).toEqual([{ location: { directory: "/repo" } }]) - expect(result).toHaveLength(1) }) }) diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 0f3e473816..c63b702522 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -9,20 +9,6 @@ import type { ReferenceInfo, Session, } from "@opencode-ai/sdk/v2/client" -import type { - AgentListInput, - AgentListOutput, - CatalogApi, - CommandInfo, - CommandListInput, - CommandListOutput, - ProjectCurrentInput, - ProjectCurrentOutput, - ProjectListOutput, - ReferenceListInput, - ReferenceListOutput, - SessionApi, -} from "@opencode-ai/client/promise" import { showToast } from "@/utils/toast" import { getFilename } from "@opencode-ai/core/util/path" import { retry } from "@opencode-ai/core/util/retry" @@ -30,21 +16,12 @@ import { batch } from "solid-js" import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store" import type { State, VcsCache } from "./types" import type { ServerSession } from "../server-session" -import { - cmp, - normalizeAgentList, - normalizePermissionRequest, - normalizeProjectInfo, - normalizeProviderList, -} from "./utils" +import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" import { QueryClient, queryOptions } from "@tanstack/solid-query" import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync" import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" import { ScopedKey, type ServerScope } from "@/utils/server-scope" -import { normalizeSessionInfo } from "@/utils/session" -import type { ServerProtocol } from "@/utils/server-protocol" -import type { ServerApi } from "@/utils/server" type GlobalStore = { ready: boolean @@ -105,35 +82,21 @@ function showErrors(input: { }) } -export const loadGlobalConfigQuery = (scope: ServerScope, sdk: OpencodeClient, protocol?: Promise) => +export const loadGlobalConfigQuery = (scope: ServerScope, sdk: OpencodeClient) => queryOptions({ queryKey: [scope, "config"], - queryFn: async () => { - if ((await protocol) !== "v1") return {} - return retry(() => sdk.global.config.get().then((x) => x.data!)) - }, + queryFn: () => retry(() => sdk.global.config.get().then((x) => x.data!)), }) -type ProjectApi = { - readonly list: () => Promise - readonly current: (input?: ProjectCurrentInput) => Promise -} - -type McpApi = ServerApi["mcp"] -type PermissionApi = ServerApi["permission"] -type QuestionApi = ServerApi["question"] -type VcsApi = ServerApi["vcs"] - -export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) => +export const loadProjectsQuery = (scope: ServerScope, sdk: OpencodeClient) => queryOptions({ queryKey: [scope, "project"], queryFn: () => retry(() => - api.list().then((projects) => { - return projects + sdk.project.list().then((x) => { + return (x.data ?? []) .filter((p) => !!p?.id) .filter((p) => !!p.worktree && !p.worktree.includes("opencode-test")) - .map(normalizeProjectInfo) .slice() .sort((a, b) => cmp(a.id, b.id)) }), @@ -142,8 +105,6 @@ export const loadProjectsQuery = (scope: ServerScope, api: ProjectApi) => export async function bootstrapGlobal(input: { serverSDK: OpencodeClient - serverAPI: CatalogApi & { readonly project: ProjectApi } - protocol?: Promise scope: ServerScope requestFailedTitle: string translate: (key: string, vars?: Record) => string @@ -152,15 +113,12 @@ export async function bootstrapGlobal(input: { queryClient: QueryClient }) { const slow = [ - () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.serverSDK, input.protocol)), - () => - input.queryClient.fetchQuery( - loadProvidersQuery(input.scope, null, input.serverAPI, input.serverSDK, input.protocol), - ), - () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverSDK, input.protocol)), + () => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope, input.serverSDK)), + () => input.queryClient.fetchQuery(loadProvidersQuery(input.scope, null, input.serverSDK)), + () => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverSDK)), () => input.queryClient - .fetchQuery(loadProjectsQuery(input.scope, input.serverAPI.project)) + .fetchQuery(loadProjectsQuery(input.scope, input.serverSDK)) .then((data) => input.setGlobalStore("project", data)), ] await runAll(slow) @@ -204,126 +162,44 @@ function warmSessions(input: { ids: string[] store: Store setStore: SetStoreFunction - api: SessionApi + sdk: OpencodeClient }) { const known = new Set(input.store.session.map((item) => item.id)) const ids = [...new Set(input.ids)].filter((id) => !!id && !known.has(id)) if (ids.length === 0) return Promise.resolve() return Promise.all( ids.map((sessionID) => - retry(() => input.api.get({ sessionID })).then((session) => - mergeSession(input.setStore, normalizeSessionInfo(session)), - ), + retry(() => input.sdk.session.get({ sessionID })).then((x) => { + const session = x.data + if (!session?.id) return + mergeSession(input.setStore, session) + }), ), ).then(() => undefined) } -export const loadProvidersQuery = ( - scope: ServerScope, - directory: string | null, - sdk: CatalogApi, - legacy?: OpencodeClient, - protocol?: Promise, -) => +export const loadProvidersQuery = (scope: ServerScope, directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [scope, directory, "providers"], - queryFn: () => - retry(async () => { - if ((await protocol) === "v1" && legacy) { - const result = await legacy.provider.list() - return normalizeProviderList(result.data!) - } - const location = directory ? { location: { directory } } : undefined - const [providers, models, defaultModel] = await Promise.all([ - sdk.provider.list(location), - sdk.model.list(location), - sdk.model.default(location), - ]) - return normalizeProviderList(providers.data, models.data, defaultModel.data) - }), + queryFn: () => retry(() => sdk.provider.list().then((x) => normalizeProviderList(x.data!))), }) -type AgentListApi = { - readonly list: (input?: AgentListInput) => Promise -} - -type CommandListApi = { - readonly list: (input?: CommandListInput) => Promise -} - -type ReferenceListApi = { - readonly list: (input?: ReferenceListInput) => Promise -} - -export const loadAgentsQuery = ( - scope: ServerScope, - directory: string, - sdk: AgentListApi, - legacy?: OpencodeClient, - protocol?: Promise, -) => +export const loadAgentsQuery = (scope: ServerScope, directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [scope, directory, "agents"], - queryFn: () => - retry(async () => { - if ((await protocol) === "v1" && legacy) return normalizeAgentList((await legacy.app.agents()).data ?? []) - return sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data)) - }), + queryFn: () => retry(() => sdk.app.agents().then((x) => normalizeAgentList(x.data))), }) -export const loadCommands = ( - directory: string, - api: CommandListApi, - legacy?: OpencodeClient, - protocol?: Promise, -): Promise => - retry(async () => { - if ((await protocol) === "v1" && legacy) { - return ((await legacy.command.list()).data ?? []).map((command) => { - const [providerID, id] = command.model?.split("/") ?? [] - return { - name: command.name, - template: command.template, - description: command.description, - agent: command.agent, - model: providerID && id ? { providerID, id } : undefined, - subtask: command.subtask, - // source: command.source === "skill" ? undefined : command.source, - } - }) - } - return api.list({ location: { directory } }).then((result) => result.data) - }) - -export const loadPathQuery = ( - scope: ServerScope, - directory: string | null, - sdk: OpencodeClient, - protocol?: Promise, -) => +export const loadPathQuery = (scope: ServerScope, directory: string | null, sdk: OpencodeClient) => queryOptions({ queryKey: [scope, directory, "path"], - queryFn: async () => { - if ((await protocol) !== "v1") - return { state: "", config: "", worktree: "", directory: directory ?? "", home: "" } - return retry(() => sdk.path.get({ directory: directory ?? undefined }).then((result) => result.data!)) - }, + queryFn: () => retry(() => sdk.path.get().then((x) => x.data!)), }) -export const loadReferencesQuery = ( - scope: ServerScope, - directory: string, - api: ReferenceListApi, - legacy?: OpencodeClient, - protocol?: Promise, -) => +export const loadReferencesQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) => queryOptions({ queryKey: [scope, directory, "references"] as const, - queryFn: () => - retry(async () => { - if ((await protocol) === "v1" && legacy) return (await legacy.v2.reference.list()).data?.data ?? [] - return api.list({ location: { directory } }).then((result) => result.data) - }).catch(() => []), + queryFn: () => retry(() => sdk.v2.reference.list().then((x) => x.data?.data ?? [])).catch(() => []), placeholderData: [], }) @@ -332,17 +208,6 @@ export async function bootstrapDirectory(input: { scope: ServerScope mcp: boolean sdk: OpencodeClient - api: CatalogApi & { - readonly agent: AgentListApi - readonly command: CommandListApi - readonly mcp: McpApi - readonly permission: PermissionApi - readonly project: ProjectApi - readonly question: QuestionApi - readonly reference: ReferenceListApi - readonly session: SessionApi - readonly vcs: VcsApi - } store: Store setStore: SetStoreFunction vcsCache: VcsCache @@ -356,7 +221,6 @@ export async function bootstrapDirectory(input: { } queryClient: QueryClient session?: ServerSession - protocol?: Promise }) { const loading = input.store.status !== "complete" const seededProject = projectID(input.directory, input.global.project) @@ -376,18 +240,13 @@ export async function bootstrapDirectory(input: { () => Promise.resolve(input.loadSessions(input.directory)), () => input.queryClient - .ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.api.agent, input.sdk, input.protocol)) + .ensureQueryData(loadAgentsQuery(input.scope, input.directory, input.sdk)) .then((data) => input.setStore("agent", data)), () => - retry(async () => { - if ((await input.protocol) !== "v1") return - return input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false }))) - }), + retry(() => input.sdk.config.get().then((x) => input.setStore("config", reconcile(x.data!, { merge: false })))), () => retry(() => - (async () => { - if ((await input.protocol) !== "v1") return - const x = await input.sdk.session.status() + input.sdk.session.status().then(async (x) => { if (!input.session) { input.setStore("session_status", x.data!) return @@ -405,57 +264,42 @@ export async function bootstrapDirectory(input: { for (const [sessionID, status] of Object.entries(statuses)) { input.session.set("session_status", sessionID, reconcile(status)) } + // Warm session info only after seeding statuses so a stalled session + // fetch cannot park busy indicators behind it, mirroring how live + // session.status events apply first and resolve info in the background. await Promise.all( Object.keys(statuses).map((sessionID) => input.session!.resolve(sessionID).catch(() => undefined)), ) - })(), + }), ), !seededProject && - (() => - retry(() => input.api.project.current({ location: { directory: input.directory } })).then((project) => - input.setStore("project", project.id), - )), + (() => retry(() => input.sdk.project.current()).then((x) => input.setStore("project", x.data!.id))), !seededPath && (() => - input.queryClient - .ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk, input.protocol)) - .then((data) => { - const next = projectID(data.directory ?? input.directory, input.global.project) - if (next) input.setStore("project", next) - })), - () => - retry(async () => { - if ((await input.protocol) !== "v1") return - return input.sdk.vcs.get().then((result) => { - const next = { branch: result.data?.branch, default_branch: result.data?.default_branch } - input.setStore("vcs", next) - if (next) input.vcsCache.setStore("value", next) - }) - }), - input.mcp && - (() => - loadCommands(input.directory, input.api.command, input.sdk, input.protocol).then((commands) => - input.setStore("command", commands), - )), - () => - input.queryClient.fetchQuery( - loadReferencesQuery(input.scope, input.directory, input.api.reference, input.sdk, input.protocol), - ), + input.queryClient.ensureQueryData(loadPathQuery(input.scope, input.directory, input.sdk)).then((data) => { + const next = projectID(data.directory ?? input.directory, input.global.project) + if (next) input.setStore("project", next) + })), () => retry(() => - (async () => { - if ((await input.protocol) === "v1") return (await input.sdk.permission.list()).data ?? [] - return input.api.permission.request - .list({ location: { directory: input.directory } }) - .then((result) => result.data.map(normalizePermissionRequest)) - })().then((permissions) => { - const ids = permissions.map((permission) => permission.sessionID) + input.sdk.vcs.get().then((x) => { + const next = x.data ?? input.store.vcs + input.setStore("vcs", next) + if (next) input.vcsCache.setStore("value", next) + }), + ), + input.mcp && (() => retry(() => input.sdk.command.list().then((x) => input.setStore("command", x.data ?? [])))), + () => input.queryClient.fetchQuery(loadReferencesQuery(input.scope, input.directory, input.sdk)), + () => + retry(() => + input.sdk.permission.list().then((x) => { + const ids = (x.data ?? []).map((perm) => perm?.sessionID).filter((id): id is string => !!id) const grouped = groupBySession( - permissions.filter((permission) => !!permission.id && !!permission.sessionID), + (x.data ?? []).filter((perm): perm is PermissionRequest => !!perm?.id && !!perm.sessionID), ) const warm = input.session ? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined) - : warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session }) + : warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }) return warm.then(() => batch(() => { const current = input.session?.data.permission ?? input.store.permission @@ -479,19 +323,12 @@ export async function bootstrapDirectory(input: { ), () => retry(() => - (async () => { - if ((await input.protocol) === "v1") return (await input.sdk.question.list()).data ?? [] - return input.api.question.request - .list({ location: { directory: input.directory } }) - .then((result) => result.data) - })().then((questions) => { - const ids = questions.map((question) => question.sessionID) - const grouped = groupBySession( - questions.filter((question) => !!question.id && !!question.sessionID) as QuestionRequest[], - ) + input.sdk.question.list().then((x) => { + const ids = (x.data ?? []).map((question) => question?.sessionID).filter((id): id is string => !!id) + const grouped = groupBySession((x.data ?? []).filter((q): q is QuestionRequest => !!q?.id && !!q.sessionID)) const warm = input.session ? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined) - : warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session }) + : warmSessions({ ids, store: input.store, setStore: input.setStore, sdk: input.sdk }) return warm.then(() => batch(() => { const current = input.session?.data.question ?? input.store.question @@ -514,27 +351,17 @@ export async function bootstrapDirectory(input: { }), ), () => Promise.resolve(input.loadSessions(input.directory)), - input.mcp && - (() => - input.queryClient.fetchQuery( - loadMcpQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol), - )), - input.mcp && - (() => - input.queryClient.fetchQuery( - loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp, input.sdk, input.protocol), - )), + input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.sdk))), + input.mcp && (() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.sdk))), () => - input.queryClient - .fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api, input.sdk, input.protocol)) - .catch((err) => { - const project = getFilename(input.directory) - showToast({ - variant: "error", - title: input.translate("toast.project.reloadFailed.title", { project }), - description: formatServerError(err, input.translate), - }) - }), + input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.sdk)).catch((err) => { + const project = getFilename(input.directory) + showToast({ + variant: "error", + title: input.translate("toast.project.reloadFailed.title", { project }), + description: formatServerError(err, input.translate), + }) + }), ].filter(Boolean) as (() => Promise)[] await waitForPaint() diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 4eaa785789..b36973e7f0 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -255,7 +255,6 @@ export function createChildStoreManager(input: { vcs: vcsStore.value, limit: 5, message: {}, - session_message: {}, part: {}, part_text_accum_delta: {}, }) diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts index b53fb691b3..fb58fc4832 100644 --- a/packages/app/src/context/global-sync/event-reducer.test.ts +++ b/packages/app/src/context/global-sync/event-reducer.test.ts @@ -80,7 +80,6 @@ const baseState = (input: Partial = {}) => vcs: undefined, limit: 10, message: {}, - session_message: {}, part: {}, part_text_accum_delta: {}, ...input, @@ -262,8 +261,8 @@ describe("applyDirectoryEvent", () => { test("cleans session caches when deleted and decrements only root totals", () => { const cases = [ - { info: rootSession({ id: "ses_1" }), expectedTotal: 1, current: false }, - { info: rootSession({ id: "ses_2", parentID: "ses_1" }), expectedTotal: 2, current: true }, + { info: rootSession({ id: "ses_1" }), expectedTotal: 1 }, + { info: rootSession({ id: "ses_2", parentID: "ses_1" }), expectedTotal: 2 }, ] for (const item of cases) { @@ -287,10 +286,7 @@ describe("applyDirectoryEvent", () => { ) applyDirectoryEvent({ - event: { - type: "session.deleted", - properties: item.current ? { sessionID: item.info.id } : { info: item.info }, - }, + event: { type: "session.deleted", properties: { info: item.info } }, store, setStore, push() {}, diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index 8f203715a6..b12df5eb55 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -8,9 +8,9 @@ import type { QuestionRequest, Session, SessionStatus, + SnapshotFileDiff, Todo, } from "@opencode-ai/sdk/v2/client" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { State, VcsCache } from "./types" import { trimSessions } from "./session-trim" import { dropSessionCaches } from "./session-cache" @@ -171,11 +171,8 @@ export function applyDirectoryEvent(input: { break } case "session.deleted": { - const properties = event.properties as { sessionID?: string; info?: Session } - const sessionID = properties.info?.id ?? properties.sessionID - if (!sessionID) break - const result = Binary.search(input.store.session, sessionID, (s) => s.id) - const info = properties.info ?? (result.found ? input.store.session[result.index] : undefined) + const info = (event.properties as { info: Session }).info + const result = Binary.search(input.store.session, info.id, (s) => s.id) if (result.found) { input.setStore( "session", @@ -184,77 +181,14 @@ export function applyDirectoryEvent(input: { }), ) } - cleanupSessionCaches(input.setStore, sessionID, input.setSessionTodo) - if (info?.parentID) break + cleanupSessionCaches(input.setStore, info.id, input.setSessionTodo) + if (info.parentID) break input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) break } - case "session.renamed": { - const properties = event.properties as { sessionID: string; title: string } - const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) - if (!result.found) break - input.setStore("session", result.index, (session) => ({ - ...session, - title: properties.title, - time: { ...session.time, updated: Date.now() }, - })) - break - } - case "session.usage.updated": { - const properties = event.properties as Pick & { sessionID: string } - const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) - if (!result.found) break - input.setStore("session", result.index, (session) => ({ - ...session, - cost: properties.cost, - tokens: properties.tokens, - })) - break - } - // case "session.archived": { - // const properties = event.properties as { sessionID: string } - // const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) - // if (!result.found) break - // const info = input.store.session[result.index] - // input.setStore( - // "session", - // produce((draft) => void draft.splice(result.index, 1)), - // ) - // cleanupSessionCaches(input.setStore, properties.sessionID) - // if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) - // break - // } - case "session.moved": { - const properties = event.properties as { - sessionID: string - location: { directory: string; workspaceID?: string } - projectID?: string - subpath?: string - } - const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id) - if (!result.found) break - if (properties.location.directory === input.directory) { - input.setStore("session", result.index, (session) => ({ - ...session, - projectID: properties.projectID ?? session.projectID, - workspaceID: properties.location.workspaceID, - directory: properties.location.directory, - path: properties.subpath, - time: { ...session.time, updated: Date.now() }, - })) - break - } - const info = input.store.session[result.index] - input.setStore( - "session", - produce((draft) => void draft.splice(result.index, 1)), - ) - if (!info?.parentID) input.setStore("sessionTotal", (value) => Math.max(0, value - 1)) - break - } case "session.diff": { - const props = event.properties as { sessionID: string; diff: FileDiffInfo[] } - input.setStore("session_diff", props.sessionID, reconcile(list(props.diff) as FileDiffInfo[], { key: "file" })) + const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] } + input.setStore("session_diff", props.sessionID, reconcile(list(props.diff), { key: "file" })) break } case "todo.updated": { diff --git a/packages/app/src/context/global-sync/mcp.test.ts b/packages/app/src/context/global-sync/mcp.test.ts index ebfd9738ee..a292d23df9 100644 --- a/packages/app/src/context/global-sync/mcp.test.ts +++ b/packages/app/src/context/global-sync/mcp.test.ts @@ -31,24 +31,4 @@ describe("toggleMcp", () => { await toggleMcp(input("disabled")) expect(calls).toEqual(["connect", "refresh"]) }) - - test("does not toggle a server while its connection is pending", async () => { - const calls: string[] = [] - await toggleMcp({ - status: "pending", - connect: async () => { - calls.push("connect") - }, - disconnect: async () => { - calls.push("disconnect") - }, - authenticate: async () => { - calls.push("authenticate") - }, - refresh: async () => { - calls.push("refresh") - }, - }) - expect(calls).toEqual([]) - }) }) diff --git a/packages/app/src/context/global-sync/mcp.ts b/packages/app/src/context/global-sync/mcp.ts index cd91f396d0..2eeb297b95 100644 --- a/packages/app/src/context/global-sync/mcp.ts +++ b/packages/app/src/context/global-sync/mcp.ts @@ -1,13 +1,12 @@ -import type { McpServer } from "@opencode-ai/client/promise" +import type { McpStatus } from "@opencode-ai/sdk/v2/client" export async function toggleMcp(input: { - status: McpServer["status"]["status"] + status: McpStatus["status"] connect: () => Promise disconnect: () => Promise authenticate: () => Promise refresh: () => Promise }) { - if (input.status === "pending") return await { connected: input.disconnect, needs_auth: input.authenticate, diff --git a/packages/app/src/context/global-sync/session-cache.test.ts b/packages/app/src/context/global-sync/session-cache.test.ts index 45fbe38abe..4b2be505ea 100644 --- a/packages/app/src/context/global-sync/session-cache.test.ts +++ b/packages/app/src/context/global-sync/session-cache.test.ts @@ -1,6 +1,13 @@ import { describe, expect, test } from "bun:test" -import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@opencode-ai/sdk/v2/client" -import type { FileDiffInfo } from "@opencode-ai/client/promise" +import type { + Message, + Part, + PermissionRequest, + QuestionRequest, + SessionStatus, + SnapshotFileDiff, + Todo, +} from "@opencode-ai/sdk/v2/client" import { dropSessionCaches, pickSessionCacheEvictions } from "./session-cache" const msg = (id: string, sessionID: string) => @@ -26,10 +33,9 @@ describe("app session cache", () => { test("dropSessionCaches clears orphaned parts without message rows", () => { const store: { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record - session_message: Record part: Record permission: Record question: Record @@ -39,7 +45,6 @@ describe("app session cache", () => { session_diff: { ses_1: [] }, todo: { ses_1: [] as Todo[] }, message: {}, - session_message: {}, part: { msg_1: [part("prt_1", "ses_1", "msg_1")] }, permission: { ses_1: [] as PermissionRequest[] }, question: { ses_1: [] as QuestionRequest[] }, @@ -62,10 +67,9 @@ describe("app session cache", () => { const m = msg("msg_1", "ses_1") const store: { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record - session_message: Record part: Record permission: Record question: Record @@ -75,7 +79,6 @@ describe("app session cache", () => { session_diff: {}, todo: {}, message: { ses_1: [m] }, - session_message: {}, part: { [m.id]: [part("prt_1", "ses_1", m.id)] }, permission: {}, question: {}, diff --git a/packages/app/src/context/global-sync/session-cache.ts b/packages/app/src/context/global-sync/session-cache.ts index 7d684a5a1a..05cdc84643 100644 --- a/packages/app/src/context/global-sync/session-cache.ts +++ b/packages/app/src/context/global-sync/session-cache.ts @@ -1,15 +1,20 @@ -import type { Message, Part, PermissionRequest, QuestionRequest, SessionStatus, Todo } from "@opencode-ai/sdk/v2/client" -import type { FileDiffInfo } from "@opencode-ai/client/promise" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" +import type { + Message, + Part, + PermissionRequest, + QuestionRequest, + SessionStatus, + SnapshotFileDiff, + Todo, +} from "@opencode-ai/sdk/v2/client" export const SESSION_CACHE_LIMIT = 40 type SessionCache = { session_status: Record - session_diff: Record + session_diff: Record todo: Record message: Record - session_message: Record part: Record permission: Record question: Record @@ -32,7 +37,6 @@ export function dropSessionCaches(store: SessionCache, sessionIDs: Iterable; directory: string; limit: number }) { - const result = await input.api.list({ - directory: input.directory, - parentID: null, - limit: input.limit, - order: "desc", - }) - return { - data: result.data.map(normalizeSessionInfo), - limit: input.limit, - limited: true, - } as const -} - -export async function loadRootSessionsV1(input: { client: OpencodeClient; directory: string; limit: number }) { +export async function loadRootSessionsWithFallback(input: RootLoadArgs) { try { - const result = await input.client.session.list({ directory: input.directory, roots: true, limit: input.limit }) - return { data: result.data, limit: input.limit, limited: true } as const + const result = await input.list({ directory: input.directory, roots: true, limit: input.limit }) + return { + data: result.data, + limit: input.limit, + limited: true, + } as const } catch { - const result = await input.client.session.list({ directory: input.directory, roots: true }) - return { data: result.data, limit: input.limit, limited: false } as const + const result = await input.list({ directory: input.directory, roots: true }) + return { + data: result.data, + limit: input.limit, + limited: false, + } as const } } diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 74191e10b0..86b489cd09 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -1,7 +1,10 @@ import type { Agent, + Command, Config, LspStatus, + McpResource, + McpStatus, Message, Part, Path, @@ -10,12 +13,11 @@ import type { ReferenceInfo, Session, SessionStatus, + SnapshotFileDiff, Todo, VcsInfo, } from "@opencode-ai/sdk/v2/client" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" -import type { CommandInfo, McpResource, McpServer, SessionMessageInfo } from "@opencode-ai/client/promise" import type { Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" @@ -33,7 +35,7 @@ export type ProjectMeta = { export type State = { status: "loading" | "partial" | "complete" agent: Agent[] - command: CommandInfo[] + command: Command[] reference: ReferenceInfo[] project: string projectMeta: ProjectMeta | undefined @@ -49,7 +51,7 @@ export type State = { } session_working(id: string): boolean session_diff: { - [sessionID: string]: FileDiffInfo[] + [sessionID: string]: SnapshotFileDiff[] } todo: { [sessionID: string]: Todo[] @@ -62,7 +64,7 @@ export type State = { } mcp_ready: boolean mcp: { - [name: string]: McpServer["status"] + [name: string]: McpStatus } mcp_resource: { [key: string]: McpResource @@ -74,9 +76,6 @@ export type State = { message: { [sessionID: string]: Message[] } - session_message: { - [sessionID: string]: SessionMessageInfo[] - } part: { [messageID: string]: Part[] } @@ -129,6 +128,18 @@ export type DisposeCheck = { loadingSessions: boolean } +export type RootLoadArgs = { + directory: string + limit: number + list: (query: { directory: string; roots: true; limit?: number }) => Promise<{ data?: Session[] }> +} + +export type RootLoadResult = { + data?: Session[] + limit: number + limited: boolean +} + export const MAX_DIR_STORES = 30 export const DIR_IDLE_TTL_MS = 20 * 60 * 1000 export const SESSION_RECENT_WINDOW = 4 * 60 * 60 * 1000 diff --git a/packages/app/src/context/global-sync/utils.test.ts b/packages/app/src/context/global-sync/utils.test.ts index 69ca494992..406c0f124e 100644 --- a/packages/app/src/context/global-sync/utils.test.ts +++ b/packages/app/src/context/global-sync/utils.test.ts @@ -1,117 +1,36 @@ import { describe, expect, test } from "bun:test" -import type { - AgentListOutput, - ModelDefaultOutput, - ModelListOutput, - ProviderListOutput, -} from "@opencode-ai/client/promise" -import { directoryKey, normalizeAgentList, normalizePermissionRequest, normalizeProviderList } from "./utils" +import type { Agent } from "@opencode-ai/sdk/v2/client" +import { directoryKey, normalizeAgentList } from "./utils" + +const agent = (name = "build") => + ({ + name, + mode: "primary", + permission: {}, + options: {}, + }) as Agent describe("normalizeAgentList", () => { - test("adapts current agents to the app agent shape", () => { - const result = normalizeAgentList([ - { - id: "build", - name: "Build", - mode: "primary", - hidden: false, - color: "primary", - model: { id: "gpt-5", providerID: "openai", variant: "high" }, - request: { settings: { temperature: 0.2, topP: 0.9 }, headers: {}, body: {} }, - system: "Build software", - permissions: [{ action: "read", resource: "*", effect: "allow" }], - }, - ] as AgentListOutput["data"]) - - expect(result).toEqual([ - { - name: "build", - description: undefined, - mode: "primary", - hidden: false, - temperature: 0.2, - topP: 0.9, - color: "primary", - permission: [{ permission: "read", pattern: "*", action: "allow" }], - model: { providerID: "openai", modelID: "gpt-5" }, - variant: "high", - prompt: "Build software", - options: { temperature: 0.2, topP: 0.9 }, - steps: undefined, - }, - ]) + test("keeps array payloads", () => { + expect(normalizeAgentList([agent("build"), agent("docs")])).toEqual([agent("build"), agent("docs")]) }) -}) -describe("normalizePermissionRequest", () => { - test("adapts the current permission request to app state", () => { + test("wraps a single agent payload", () => { + expect(normalizeAgentList(agent("docs"))).toEqual([agent("docs")]) + }) + + test("extracts agents from keyed objects", () => { expect( - normalizePermissionRequest({ - id: "permission-1", - sessionID: "session-1", - action: "read", - resources: ["README.md"], - save: ["*.md"], - metadata: { path: "README.md" }, - source: { type: "tool", messageID: "message-1", callID: "call-1" }, + normalizeAgentList({ + build: agent("build"), + docs: agent("docs"), }), - ).toEqual({ - id: "permission-1", - sessionID: "session-1", - permission: "read", - patterns: ["README.md"], - always: ["*.md"], - metadata: { path: "README.md" }, - tool: { messageID: "message-1", callID: "call-1" }, - }) + ).toEqual([agent("build"), agent("docs")]) }) -}) -describe("normalizeProviderList", () => { - test("groups current models into the app provider catalog", () => { - const result = normalizeProviderList( - [{ id: "openai", name: "OpenAI", package: "@ai-sdk/openai" }] as ProviderListOutput["data"], - [ - { - id: "gpt-5", - modelID: "gpt-5", - providerID: "openai", - name: "GPT-5", - capabilities: { tools: true, input: ["text", "image"], output: ["text"] }, - variants: [{ id: "high" }], - time: { released: 1 }, - cost: [{ input: 1, output: 2, cache: { read: 0.1, write: 0.2 } }], - status: "active", - enabled: true, - limit: { context: 128_000, output: 8_192 }, - }, - { - id: "gpt-old", - modelID: "gpt-old", - providerID: "openai", - name: "GPT Old", - capabilities: { tools: false, input: ["text"], output: ["text"] }, - variants: [], - time: { released: 0 }, - cost: [], - status: "deprecated", - enabled: true, - limit: { context: 1, output: 1 }, - }, - ] as ModelListOutput["data"], - { id: "gpt-5", providerID: "openai" } as ModelDefaultOutput["data"], - ) - - expect(result.connected).toEqual(["openai"]) - expect(result.default).toEqual({ openai: "gpt-5" }) - expect(result.all.get("openai")?.models["gpt-old"]).toBeUndefined() - expect(result.all.get("openai")?.models["gpt-5"]).toMatchObject({ - id: "gpt-5", - providerID: "openai", - capabilities: { toolcall: true, attachment: true }, - cost: { input: 1, output: 2 }, - variants: { high: {} }, - }) + test("drops invalid payloads", () => { + expect(normalizeAgentList({ name: "AbortError" })).toEqual([]) + expect(normalizeAgentList([{ name: "build" }, agent("docs")])).toEqual([agent("docs")]) }) }) diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index 59632e53c9..e54bc88d4d 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -1,152 +1,39 @@ -import type { - AgentListOutput, - ModelDefaultOutput, - ModelListOutput, - PermissionV2Request, - ProviderListOutput, -} from "@opencode-ai/client/promise" -import type { Agent, PermissionRequest, Project, Provider, ProviderListResponse } from "@opencode-ai/sdk/v2/client" -import type { Project as CurrentProject } from "@opencode-ai/client/promise" +import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" import { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context" export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key" export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) -export function normalizeAgentList(input: AgentListOutput["data"] | Agent[]): Agent[] { - if (input.every((agent) => !("request" in agent))) return input as Agent[] - return (input as AgentListOutput["data"]).map((agent) => ({ - name: agent.id, - description: agent.description, - mode: agent.mode, - hidden: agent.hidden, - temperature: - typeof agent.request.settings.temperature === "number" ? agent.request.settings.temperature : undefined, - topP: typeof agent.request.settings.topP === "number" ? agent.request.settings.topP : undefined, - color: agent.color, - permission: agent.permissions.map((rule) => ({ - permission: rule.action, - pattern: rule.resource, - action: rule.effect, - })), - model: agent.model && { providerID: agent.model.providerID, modelID: agent.model.id }, - variant: agent.model?.variant, - prompt: agent.system, - options: agent.request.settings, - steps: agent.steps, - })) +function isAgent(input: unknown): input is Agent { + if (!input || typeof input !== "object") return false + const item = input as { name?: unknown; mode?: unknown } + if (typeof item.name !== "string") return false + return item.mode === "subagent" || item.mode === "primary" || item.mode === "all" } -export function normalizePermissionRequest(input: PermissionV2Request | PermissionRequest): PermissionRequest { - if ("permission" in input) return input +export function normalizeAgentList(input: unknown): Agent[] { + if (Array.isArray(input)) return input.filter(isAgent) + if (isAgent(input)) return [input] + if (!input || typeof input !== "object") return [] + return Object.values(input).filter(isAgent) +} + +export function normalizeProviderList(input: ProviderListResponse): NormalizedProviderListResponse { return { - id: input.id, - sessionID: input.sessionID, - permission: input.action, - patterns: input.resources, - always: input.save ?? [], - metadata: input.metadata ?? {}, - tool: - input.source?.type === "tool" ? { messageID: input.source.messageID, callID: input.source.callID } : undefined, - } -} - -export function normalizeProviderList( - providers: ProviderListOutput["data"] | ProviderListResponse, - models?: ModelListOutput["data"], - defaultModel?: ModelDefaultOutput["data"], -): NormalizedProviderListResponse { - if (!Array.isArray(providers)) { - return { - ...providers, - all: new Map( - providers.all.map((provider) => [ - provider.id, - { - ...provider, - models: Object.fromEntries( - Object.entries(provider.models).filter(([, model]) => model.status !== "deprecated"), - ), - }, - ]), + ...input, + all: new Map( + input.all.map( + (provider) => + [ + provider.id, + { + ...provider, + models: Object.fromEntries( + Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"), + ), + }, + ] as const, ), - } - } - const all = new Map() - - for (const provider of providers) { - all.set(provider.id, { - id: provider.id, - name: provider.name, - source: "custom", - env: [], - options: provider.settings ?? {}, - models: {}, - }) - } - - for (const model of models ?? []) { - const provider = all.get(model.providerID) - if (!provider || model.status === "deprecated") continue - const cost = model.cost.find((item) => item.tier === undefined) ?? model.cost[0] - provider.models[model.id] = { - id: model.id, - providerID: model.providerID, - api: { - id: model.modelID, - url: "", - npm: model.package ?? provider.id, - }, - name: model.name, - family: model.family, - capabilities: { - temperature: false, - reasoning: false, - attachment: model.capabilities.input.some((item) => item !== "text"), - toolcall: model.capabilities.tools, - input: { - text: model.capabilities.input.includes("text"), - audio: model.capabilities.input.includes("audio"), - image: model.capabilities.input.includes("image"), - video: model.capabilities.input.includes("video"), - pdf: model.capabilities.input.includes("pdf"), - }, - output: { - text: model.capabilities.output.includes("text"), - audio: model.capabilities.output.includes("audio"), - image: model.capabilities.output.includes("image"), - video: model.capabilities.output.includes("video"), - pdf: model.capabilities.output.includes("pdf"), - }, - interleaved: false, - }, - cost: { - input: cost?.input ?? 0, - output: cost?.output ?? 0, - cache: { - read: cost?.cache.read ?? 0, - write: cost?.cache.write ?? 0, - }, - }, - limit: model.limit, - status: model.status, - options: model.settings ?? {}, - headers: model.headers ?? {}, - release_date: new Date(model.time.released).toISOString().slice(0, 10), - variants: Object.fromEntries(model.variants.map((variant) => [variant.id, variant.settings ?? {}])), - } - } - - return { - all, - connected: providers.map((provider) => provider.id), - default: Object.fromEntries( - providers.flatMap((provider) => { - const model = - defaultModel?.providerID === provider.id - ? defaultModel - : models?.find((item) => item.providerID === provider.id && item.status !== "deprecated") - return model ? [[provider.id, model.id]] : [] - }), ), } } @@ -162,10 +49,3 @@ export function sanitizeProject(project: Project) { }, } } - -export function normalizeProjectInfo(project: Project | CurrentProject): Project { - return { - ...project, - vcs: project.vcs === "git" ? "git" : undefined, - } -} diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 6235f35c45..7ac248dc1d 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -8,7 +8,6 @@ import { useServerSDK } from "./server-sdk" import { RECENTLY_CLOSED_DISPLAY_LIMIT, ServerConnection, useServer } from "./server" import { usePlatform } from "./platform" import { Project } from "@opencode-ai/sdk/v2" -import { normalizeProjectInfo } from "./global-sync/utils" import { Persist, persisted, removePersisted } from "@/utils/persist" import { pathKey } from "@/utils/path-key" import { decode64 } from "@/utils/base64" @@ -127,7 +126,7 @@ const normalizeStoredSessionTabs = (key: string, tabs: SessionTabs) => { } } -export const currentRoute = (pathname: string, search: string): LayoutRoute => { +const currentRoute = (pathname: string, search: string): LayoutRoute => { const parts = pathname.split("/").filter(Boolean) if (parts.length === 0) return { type: "home" } @@ -570,22 +569,11 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( continue } - const projectID = project.id - void (async () => { - const sdk = serverSdk() - if ((await sdk.protocol) !== "v1") return - return sdk.client.project - .update({ projectID, directory: worktree, icon: { color } }) - .then((response) => response.data) - .then((result) => { - if (!result) return - serverSync().set("project", (items) => - items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), - ) - }) - })().catch(() => { - if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) - }) + void serverSdk() + .client.project.update({ projectID: project.id, directory: worktree, icon: { color } }) + .catch(() => { + if (colorRequested.get(worktree) === color) colorRequested.delete(worktree) + }) } }) diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx index a80cf2e580..736dc3a7c5 100644 --- a/packages/app/src/context/models.tsx +++ b/packages/app/src/context/models.tsx @@ -26,7 +26,7 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( name: "Models", gate: false, init: (props: { directory?: Accessor } = {}) => { - const providers = useProviders(() => props.directory?.()) + const providers = useProviders(props.directory) const [store, setStore, _, ready] = persisted( Persist.global("model", ["model.v1"]), diff --git a/packages/app/src/context/notification.tsx b/packages/app/src/context/notification.tsx index 799c4713dd..e12f8f1250 100644 --- a/packages/app/src/context/notification.tsx +++ b/packages/app/src/context/notification.tsx @@ -1,6 +1,6 @@ import { createStore, reconcile } from "solid-js/store" import { type Accessor, batch, createEffect, createMemo, createRoot, getOwner, onCleanup } from "solid-js" -import { useNavigate, useParams, useSearchParams } from "@solidjs/router" +import { useParams, useSearchParams } from "@solidjs/router" import { createSimpleContext } from "@opencode-ai/ui/context" import type { ServerSDK } from "./server-sdk" import type { ServerSync } from "./server-sync" @@ -118,7 +118,6 @@ export const { use: useNotification, provider: NotificationProvider } = createSi const global = useGlobal() const server = useServer() const tabs = useTabs() - const navigate = useNavigate() const platform = usePlatform() const settings = useSettings() const language = useLanguage() @@ -154,7 +153,6 @@ export const { use: useNotification, provider: NotificationProvider } = createSi platform, settings, language, - navigate, }), }), owner ?? undefined, @@ -178,14 +176,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi onCleanup(() => states.forEach((value) => value.dispose())) - const selected = () => { - const list = global.servers.list() - const key = activeServer() - if (list.some((conn) => ServerConnection.key(conn) === key)) return ensure(key) - const conn = list.find((conn) => ServerConnection.key(conn) === server.key) ?? list[0] - if (!conn) throw new Error("Notification server not found") - return ensure(ServerConnection.key(conn)) - } + const selected = () => ensure(activeServer()) return { ready: () => selected().ready(), @@ -219,7 +210,6 @@ function createServerNotificationState(input: { platform: ReturnType settings: ReturnType language: ReturnType - navigate: (href: string) => void }) { const serverSDK = () => input.sdk const serverSync = () => input.sync @@ -356,9 +346,7 @@ function createServerNotificationState(input: { const href = `/${base64Encode(directory)}/session/${sessionID}` if (settings.notifications.agent()) { - void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, () => - input.navigate(href), - ) + void platform.notify(language.t("notification.session.responseReady.title"), session.title ?? sessionID, href) } }) } @@ -391,7 +379,7 @@ function createServerNotificationState(input: { (typeof error === "string" ? error : language.t("notification.session.error.fallbackDescription")) const href = sessionID ? `/${base64Encode(directory)}/session/${sessionID}` : `/${base64Encode(directory)}` if (settings.notifications.errors()) { - void platform.notify(language.t("notification.session.error.title"), description, () => input.navigate(href)) + void platform.notify(language.t("notification.session.error.title"), description, href) } }) } diff --git a/packages/app/src/context/permission.tsx b/packages/app/src/context/permission.tsx index d6d8019262..496d1ab4a2 100644 --- a/packages/app/src/context/permission.tsx +++ b/packages/app/src/context/permission.tsx @@ -13,7 +13,6 @@ import { type DraftTab, useTabs } from "./tabs" import { useSettings } from "./settings" import { requireServerKey } from "@/utils/session-route" import type { ServerScope } from "@/utils/server-scope" -import { normalizePermissionRequest } from "./global-sync/utils" import { acceptKey, directoryAcceptKey, @@ -212,7 +211,6 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync } ) function enableConfiguredDirectory(directory: string) { - if (input.sdk.protocolKind() !== "v1") return if (meta.disposed || !ready()) return const [childStore] = input.sync.child(directory) if (childStore.config.permission !== "allow") return @@ -245,25 +243,9 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync } const respond: PermissionRespondFn = (request) => { if (meta.disposed) return - input.sdk.api.permission - .reply({ - sessionID: request.sessionID, - requestID: request.permissionID, - reply: request.response, - location: request.directory ? { directory: request.directory } : undefined, - }) - .catch(() => { - responded.delete(request.permissionID) - }) - } - - const list = async (directory: string) => { - if ((await input.sdk.protocol) === "v1") { - return (await input.sdk.client.permission.list({ directory })).data ?? [] - } - return input.sdk.api.permission.request - .list({ location: { directory } }) - .then((result) => result.data.map(normalizePermissionRequest)) + input.sdk.client.permission.respond(request).catch(() => { + responded.delete(request.permissionID) + }) } function respondOnce(permission: PermissionRequest, directory?: string) { @@ -361,12 +343,14 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync } }), ) - list(directory) - .then((permissions) => { + input.sdk.client.permission + .list({ directory }) + .then((x) => { if (meta.disposed) return if (!isAutoAcceptingDirectory(directory)) return - for (const permission of permissions) { - void respondPending(permission, directory, () => isAutoAcceptingDirectory(directory)) + for (const perm of x.data ?? []) { + if (!perm?.id) continue + void respondPending(perm, directory, () => isAutoAcceptingDirectory(directory)) } }) .catch(() => undefined) @@ -393,14 +377,16 @@ function createServerPermissionState(input: { sdk: ServerSDK; sync: ServerSync } }), ) - list(directory) - .then((permissions) => { + input.sdk.client.permission + .list({ directory }) + .then((x) => { if (meta.disposed) return if (enableVersion.get(key) !== version) return if (!isAutoAccepting(sessionID, directory)) return - for (const permission of permissions) { + for (const perm of x.data ?? []) { + if (!perm?.id) continue void respondPending( - permission, + perm, directory, () => enableVersion.get(key) === version && isAutoAccepting(sessionID, directory), ) diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx index 0408b233ff..cf343bfa7e 100644 --- a/packages/app/src/context/platform.tsx +++ b/packages/app/src/context/platform.tsx @@ -5,7 +5,6 @@ import type { DesktopMenuAction } from "../desktop-menu" import { ServerConnection } from "./server" import type { WslServersPlatform } from "../wsl/types" import type { UpdaterPlatform } from "../updater" -import type { DraftStore } from "@/utils/draft-store" type PickerPaths = string | string[] | null type OpenDirectoryPickerOptions = { title?: string; multiple?: boolean } @@ -32,23 +31,26 @@ type PlatformBase = { /** App version */ version?: string - /** Open a web or mail URL in the default system application */ - openExternal(url: string): void + /** Open a URL in the default browser */ + openLink(url: string): void /** Open a local path in a local app (desktop only) */ openPath?(path: string, app?: string): Promise - /** Open a local file URL in its default app (desktop only) */ - openLocalFile?(url: string): void - /** Reveal a local path in the system file manager; false when the path does not exist (desktop only) */ revealPath?(path: string): Promise /** Restart the app */ restart(): Promise - /** Send a system notification */ - notify(title: string, description?: string, onClick?: () => void): Promise + /** Navigate back in history */ + back(): void + + /** Navigate forward in history */ + forward(): void + + /** Send a system notification (optional deep link) */ + notify(title: string, description?: string, href?: string): Promise /** Open a native attachment picker and read selected files sequentially (desktop only) */ openAttachmentPickerDialog?( @@ -65,9 +67,6 @@ type PlatformBase = { /** Storage mechanism, defaults to localStorage */ storage?: (name?: string) => SyncStorage | AsyncStorage - /** Prompt drafts, history, and their blobs. */ - draftStore?: DraftStore - /** Stable platform window identity for window-scoped persistence */ windowID?: string @@ -92,12 +91,12 @@ type PlatformBase = { /** Set the preferred display backend (desktop only) */ setDisplayBackend?(backend: DisplayBackend): Promise + /** Parse markdown to HTML using native parser (desktop only, returns unprocessed code blocks) */ + parseMarkdown?(markdown: string): Promise + /** Webview zoom level (desktop only) */ webviewZoom?: Accessor - /** Whether the native desktop window is fullscreen */ - windowFullscreen?: Accessor - /** Get whether native pinch/Ctrl-scroll zoom gestures are enabled (desktop only) */ getPinchZoomEnabled?(): Promise | boolean diff --git a/packages/app/src/context/prompt-state.ts b/packages/app/src/context/prompt-state.ts index ea639a5bdd..73196289e2 100644 --- a/packages/app/src/context/prompt-state.ts +++ b/packages/app/src/context/prompt-state.ts @@ -5,7 +5,6 @@ import { createStore, type SetStoreFunction } from "solid-js/store" import type { FileSelection } from "@/context/file" import { Persist, persisted } from "@/utils/persist" import type { ServerScope } from "@/utils/server-scope" -import type { BlobReference } from "@/utils/draft-store" interface PartBase { content: string @@ -38,7 +37,7 @@ export interface ImageAttachmentPart { filename: string sourcePath?: string mime: string - blob: BlobReference + dataUrl: string } export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart @@ -169,9 +168,9 @@ function createPromptActions(setStore: SetStoreFunction) { } function promptTarget(serverScope: ServerScope, scope: PromptScope) { - if ("draftID" in scope) return Persist.prompt(Persist.draft(scope.draftID, "prompt")) + if ("draftID" in scope) return Persist.draft(scope.draftID, "prompt") const legacy = `${scope.dir}/prompt${scope.id ? "/" + scope.id : ""}.v2` - return Persist.prompt(Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt", [legacy])) + return Persist.serverScoped(serverScope, scope.dir, scope.id, "prompt", [legacy]) } function promptStore(initial?: InitialPrompt): PromptStore { @@ -246,7 +245,7 @@ export function createPromptSession(serverScope: ServerScope, scope: PromptScope } export function createDraftPromptSession(draftID: string, initial?: InitialPrompt) { - return createPersistedPrompt(Persist.prompt(Persist.draft(draftID, "prompt")), initial) + return createPersistedPrompt(Persist.draft(draftID, "prompt"), initial) } export type PromptSession = ReturnType diff --git a/packages/app/src/context/server-sdk.test.ts b/packages/app/src/context/server-sdk.test.ts index 57e1cd86f3..7b592178fa 100644 --- a/packages/app/src/context/server-sdk.test.ts +++ b/packages/app/src/context/server-sdk.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test" -import { adaptServerEvent, coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk" -import type { OpenCodeEvent } from "@opencode-ai/client/promise" +import { coalesceServerEvents, enqueueServerEvent, resumeStreamAfterPageShow } from "./server-sdk" import type { Event } from "@opencode-ai/sdk/v2/client" describe("resumeStreamAfterPageShow", () => { @@ -15,23 +14,6 @@ describe("resumeStreamAfterPageShow", () => { }) }) -describe("adaptServerEvent", () => { - test("preserves V2 events while adapting permission requests for existing consumers", () => { - const current = { - id: "evt_1", - created: 1, - type: "permission.v2.asked", - data: { id: "perm_1", sessionID: "ses_1", action: "read", resources: ["src/**"] }, - } as OpenCodeEvent - - expect(adaptServerEvent(current)).toMatchObject({ - type: "permission.asked", - properties: { id: "perm_1", sessionID: "ses_1", permission: "read", patterns: ["src/**"] }, - current, - }) - }) -}) - describe("coalesceServerEvents", () => { const delta = (value: string, field = "text", partID = "part") => ({ directory: "/repo", @@ -52,24 +34,6 @@ describe("coalesceServerEvents", () => { expect(result[0]?.payload).toMatchObject({ id: "second", properties: { delta: "hello world" } }) }) - test("merges adjacent current text deltas", () => { - const current = (id: string, value: string) => - adaptServerEvent({ - id, - created: 1, - type: "session.text.delta", - location: { directory: "/repo" }, - data: { sessionID: "ses", assistantMessageID: "msg", ordinal: 0, delta: value }, - } as OpenCodeEvent) - const result = coalesceServerEvents([ - { directory: "/repo", payload: current("evt_1", "hello ") }, - { directory: "/repo", payload: current("evt_2", "world") }, - ]) - - expect(result).toHaveLength(1) - expect(result[0]?.payload.current).toMatchObject({ id: "evt_2", data: { delta: "hello world" } }) - }) - test("preserves event boundaries and distinct fields", () => { const status = { directory: "/repo", diff --git a/packages/app/src/context/server-sdk.tsx b/packages/app/src/context/server-sdk.tsx index 7dd2a6e59e..06597e56e7 100644 --- a/packages/app/src/context/server-sdk.tsx +++ b/packages/app/src/context/server-sdk.tsx @@ -1,60 +1,21 @@ -import type { OpenCodeEvent } from "@opencode-ai/client/promise" import type { Event } from "@opencode-ai/sdk/v2/client" import { createSimpleContext } from "@opencode-ai/ui/context" import { createGlobalEmitter } from "@solid-primitives/event-bus" import { makeEventListener } from "@solid-primitives/event-listener" -import { type Accessor, batch, createMemo, createResource, onCleanup, onMount } from "solid-js" -import { createApiForServer, createSdkForServer, type ServerApi } from "@/utils/server" +import { type Accessor, batch, createMemo, onCleanup, onMount } from "solid-js" +import { createSdkForServer } from "@/utils/server" import { useLanguage } from "./language" import { usePlatform } from "./platform" import { ServerConnection, useServer } from "./server" import { createRefCountMap } from "@/utils/refcount" import { useGlobal } from "./global" import { ServerScope } from "@/utils/server-scope" -import { detectServerProtocol, type ServerProtocol } from "@/utils/server-protocol" -import { createCompatibleApi, type CompatibleApi } from "@/utils/server-compat" const isAbortError = (error: unknown) => error !== null && typeof error === "object" && "name" in error && error.name === "AbortError" const isStreamClosed = (error: unknown, signal?: AbortSignal) => isAbortError(error) || signal?.aborted === true -export type ServerEvent = Event & { current?: OpenCodeEvent } -type QueuedServerEvent = { directory: string; payload: ServerEvent } -type CurrentDelta = Extract< - OpenCodeEvent, - { type: "session.text.delta" | "session.reasoning.delta" | "session.tool.input.delta" | "session.compaction.delta" } -> - -export function adaptServerEvent(event: OpenCodeEvent): ServerEvent { - if (event.type === "permission.v2.asked") { - return { - id: event.id, - type: "permission.asked", - properties: { - id: event.data.id, - sessionID: event.data.sessionID, - permission: event.data.action, - patterns: event.data.resources, - always: event.data.save ?? [], - metadata: event.data.metadata ?? {}, - tool: - event.data.source?.type === "tool" - ? { messageID: event.data.source.messageID, callID: event.data.source.callID } - : undefined, - }, - current: event, - } as ServerEvent - } - if (event.type === "permission.v2.replied") - return { id: event.id, type: "permission.replied", properties: event.data, current: event } as ServerEvent - if (event.type === "question.v2.asked") - return { id: event.id, type: "question.asked", properties: event.data, current: event } as ServerEvent - if (event.type === "question.v2.replied") - return { id: event.id, type: "question.replied", properties: event.data, current: event } as ServerEvent - if (event.type === "question.v2.rejected") - return { id: event.id, type: "question.rejected", properties: event.data, current: event } as ServerEvent - return { id: event.id, type: event.type, properties: event.data, current: event } as ServerEvent -} +type QueuedServerEvent = { directory: string; payload: Event } const coalescedKey = (event: QueuedServerEvent) => { if (event.payload.type === "lsp.updated") return `lsp.updated:${event.directory}` @@ -79,34 +40,6 @@ export function enqueueServerEvent(queue: QueuedServerEvent[], event: QueuedServ export function coalesceServerEvents(events: QueuedServerEvent[]) { const output: QueuedServerEvent[] = [] events.forEach((event) => { - const current = currentDelta(event.payload.current) - if (current) { - const previous = output[output.length - 1] - const prior = currentDelta(previous?.payload.current) - if ( - previous && - prior && - previous.directory === event.directory && - currentDeltaKey(prior) === currentDeltaKey(current) - ) { - const fragment = currentDeltaFragment(prior) + currentDeltaFragment(current) - const data = - current.type === "session.compaction.delta" - ? { ...current.data, text: fragment } - : { ...current.data, delta: fragment } - output[output.length - 1] = { - directory: event.directory, - payload: { - ...event.payload, - properties: data, - current: { ...current, data } as CurrentDelta, - } as ServerEvent, - } - return - } - output.push(event) - return - } if (event.payload.type !== "message.part.delta") { output.push(event) return @@ -138,53 +71,12 @@ export function coalesceServerEvents(events: QueuedServerEvent[]) { return output } -function currentDelta(event: OpenCodeEvent | undefined): CurrentDelta | undefined { - if ( - event?.type === "session.text.delta" || - event?.type === "session.reasoning.delta" || - event?.type === "session.tool.input.delta" || - event?.type === "session.compaction.delta" - ) - return event -} - -function currentDeltaKey(event: CurrentDelta) { - if (event.type === "session.tool.input.delta") - return `${event.type}:${event.data.sessionID}:${event.data.assistantMessageID}:${event.data.callID}` - if (event.type === "session.compaction.delta") return `${event.type}:${event.data.sessionID}` - return `${event.type}:${event.data.sessionID}:${event.data.assistantMessageID}:${event.data.ordinal}` -} - -function currentDeltaFragment(event: CurrentDelta) { - return event.type === "session.compaction.delta" ? event.data.text : event.data.delta -} - export function resumeStreamAfterPageShow(event: PageTransitionEvent, start: () => unknown) { if (!event.persisted) return start() } -type ServerEventEmitter = ReturnType> -type ServerSDKBase = { - server: ServerConnection.Any - scope: ServerScope - protocol: Promise - protocolKind: Accessor - url: string - client: ReturnType - api: CompatibleApi - currentApi: ServerApi - event: { - on: ServerEventEmitter["on"] - listen: ServerEventEmitter["listen"] - start: () => Promise | undefined - } - createClient: ( - opts: Omit[0], "server" | "fetch">, - ) => ReturnType -} - -function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerScope): ServerSDKBase { +function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerScope) { const platform = usePlatform() const abort = new AbortController() @@ -199,19 +91,13 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS } })() - const eventApi = createApiForServer({ server: server.http, fetch: eventFetch }) const eventSdk = createSdkForServer({ signal: abort.signal, fetch: eventFetch, server: server.http, }) - const protocol = detectServerProtocol(server.http, platform.fetch ?? globalThis.fetch) - const [protocolKind] = createResource( - () => protocol, - (value) => value, - ) const emitter = createGlobalEmitter<{ - [key: string]: ServerEvent + [key: string]: Event }>() type Queued = QueuedServerEvent @@ -256,6 +142,21 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS let run: Promise | undefined let started = false let generation = 0 + const HEARTBEAT_TIMEOUT_MS = 15_000 + let lastEventAt = Date.now() + let heartbeat: ReturnType | undefined + const resetHeartbeat = () => { + lastEventAt = Date.now() + if (heartbeat) clearTimeout(heartbeat) + heartbeat = setTimeout(() => { + attempt?.abort() + }, HEARTBEAT_TIMEOUT_MS) + } + const clearHeartbeat = () => { + if (!heartbeat) return + clearTimeout(heartbeat) + heartbeat = undefined + } const start = () => { if (started) return run @@ -267,24 +168,35 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS // oxlint-disable-next-line no-unmodified-loop-condition -- `started` is set to false by stop() which also aborts; both flags are checked to allow graceful exit while (!abort.signal.aborted && started && generation === active) { attempt = new AbortController() + lastEventAt = Date.now() const onAbort = () => { attempt?.abort() } abort.signal.addEventListener("abort", onAbort) try { - const kind = await protocol - const events = - kind === "v1" - ? (await eventSdk.global.event({ signal: attempt.signal })).stream - : eventApi.event.subscribe({ signal: attempt.signal }) + const events = await eventSdk.global.event({ + signal: attempt.signal, + onSseError: (error) => { + if (isStreamClosed(error, attempt?.signal)) return + if (streamErrorLogged) return + streamErrorLogged = true + console.error("[global-sdk] event stream error", { + url: server.http.url, + fetch: eventFetch ? "platform" : "webview", + error, + }) + }, + }) let yielded = Date.now() - for await (const event of events) { + resetHeartbeat() + for await (const event of events.stream) { + resetHeartbeat() streamErrorLogged = false - const legacy = "payload" in event - if (legacy && event.payload.type === "sync") continue - const directory = legacy ? (event.directory ?? "global") : (event.location?.directory ?? "global") - const payload = legacy ? (event.payload as Event) : adaptServerEvent(event) - if (enqueueServerEvent(queue, { directory, payload })) schedule() + if (event.payload.type !== "sync") { + const directory = event.directory ?? "global" + const payload = event.payload as Event + if (enqueueServerEvent(queue, { directory, payload })) schedule() + } if (Date.now() - yielded < STREAM_YIELD_MS) continue yielded = Date.now() @@ -302,6 +214,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS } finally { abort.signal.removeEventListener("abort", onAbort) attempt = undefined + clearHeartbeat() } if (abort.signal.aborted || !started || generation !== active) return @@ -320,11 +233,18 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS started = false generation++ attempt?.abort() + clearHeartbeat() } onMount(() => { makeEventListener(window, "pagehide", stop) makeEventListener(window, "pageshow", (event) => resumeStreamAfterPageShow(event, start)) + makeEventListener(document, "visibilitychange", () => { + if (document.visibilityState !== "visible") return + if (!started) return + if (Date.now() - lastEventAt < HEARTBEAT_TIMEOUT_MS) return + attempt?.abort() + }) }) onCleanup(() => { @@ -338,25 +258,12 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS fetch: platform.fetch, throwOnError: true, }) - const currentApi: ServerApi = createApiForServer({ server: server.http, fetch: platform.fetch }) - const legacy = (directory?: string) => - createSdkForServer({ - server: server.http, - fetch: platform.fetch, - throwOnError: true, - directory, - }) - const api = createCompatibleApi({ protocol, current: currentApi, legacy }) return { server, scope, - protocol, - protocolKind, url: server.http.url, client: sdk, - api, - currentApi, event: { on: emitter.on.bind(emitter), listen: emitter.listen.bind(emitter), @@ -372,6 +279,7 @@ function createServerSdkContextBase(server: ServerConnection.Any, scope: ServerS } } +type ServerSDKBase = ReturnType export type ServerSDK = ServerSDKBase & { ensureDirSdkContext: (directory: string) => ReturnType } @@ -400,13 +308,8 @@ export const { use: useServerSDK, provider: ServerSDKProvider } = createSimpleCo }, }) -export function useServerProtocol() { - const serverSDK = useServerSDK() - return createMemo(() => serverSDK().protocolKind()) -} - type SDKEventMap = { - [key in Event["type"]]: Extract + [key in Event["type"]]: Extract } function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) { @@ -424,15 +327,8 @@ function createDirSdkContext(directory: string, serverSDK: ServerSDKBase) { return { scope: serverSDK.scope, - protocol: serverSDK.protocol, directory, client, - api: createCompatibleApi({ - protocol: serverSDK.protocol, - current: serverSDK.currentApi, - legacy: (next) => serverSDK.createClient({ directory: next ?? directory, throwOnError: true }), - directory, - }), event: emitter, get url() { return serverSDK.url diff --git a/packages/app/src/context/server-session-v2-reducer.test.ts b/packages/app/src/context/server-session-v2-reducer.test.ts deleted file mode 100644 index 00cc37cf52..0000000000 --- a/packages/app/src/context/server-session-v2-reducer.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { OpenCodeEvent, SessionMessageInfo } from "@opencode-ai/client/promise" -import { createV2SessionReducer } from "./server-session-v2-reducer" - -const event = (input: object) => input as OpenCodeEvent -const base = { created: 1, location: { directory: "/repo" }, durable: { aggregateID: "ses_1", seq: 1, version: 1 } } - -describe("v2 session reducer", () => { - test("projects promoted input and streaming assistant content", () => { - const reducer = createV2SessionReducer() - let messages: SessionMessageInfo[] = [] - const apply = (input: object) => { - const result = reducer.reduce(messages, event(input)) - if (result) messages = result.messages - return result - } - - apply({ - ...base, - id: "evt_admitted", - type: "session.input.admitted", - data: { - sessionID: "ses_1", - inputID: "msg_user", - input: { type: "user", delivery: "steer", data: { text: "hello" } }, - }, - }) - apply({ - ...base, - id: "evt_promoted", - type: "session.input.promoted", - data: { sessionID: "ses_1", inputID: "msg_user" }, - }) - apply({ - ...base, - id: "evt_step", - type: "session.step.started", - data: { - sessionID: "ses_1", - assistantMessageID: "msg_assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - }, - }) - apply({ - ...base, - id: "evt_text_start", - type: "session.text.started", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", ordinal: 0 }, - }) - apply({ - ...base, - id: "evt_text_delta", - type: "session.text.delta", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", ordinal: 0, delta: "hel" }, - }) - apply({ - ...base, - id: "evt_text_end", - type: "session.text.ended", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", ordinal: 0, text: "hello" }, - }) - - expect(messages[0]).toMatchObject({ id: "msg_user", type: "user", text: "hello" }) - expect(messages[1]).toMatchObject({ - id: "msg_assistant", - type: "assistant", - content: [{ type: "text", text: "hello" }], - }) - }) - - test("folds tool, retry, and completion events", () => { - const reducer = createV2SessionReducer() - let messages: SessionMessageInfo[] = [] - const apply = (input: object) => { - const result = reducer.reduce(messages, event(input)) - if (result) messages = result.messages - } - - apply({ - ...base, - id: "evt_step", - type: "session.step.started", - data: { - sessionID: "ses_1", - assistantMessageID: "msg_assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - }, - }) - apply({ - ...base, - id: "evt_tool_start", - type: "session.tool.input.started", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", name: "bash" }, - }) - apply({ - ...base, - id: "evt_tool_delta", - type: "session.tool.input.delta", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", delta: "{}" }, - }) - apply({ - ...base, - id: "evt_tool_called", - type: "session.tool.called", - data: { sessionID: "ses_1", assistantMessageID: "msg_assistant", callID: "call_1", input: {}, executed: true }, - }) - apply({ - ...base, - id: "evt_tool_success", - type: "session.tool.success", - data: { - sessionID: "ses_1", - assistantMessageID: "msg_assistant", - callID: "call_1", - metadata: {}, - content: [{ type: "text", text: "done" }], - executed: true, - }, - }) - apply({ - ...base, - id: "evt_retry", - type: "session.retry.scheduled", - data: { - sessionID: "ses_1", - assistantMessageID: "msg_assistant", - attempt: 2, - at: 10, - error: { type: "ProviderError", message: "retry" }, - }, - }) - apply({ ...base, id: "evt_done", type: "session.execution.succeeded", data: { sessionID: "ses_1" } }) - - expect(messages[0]).toMatchObject({ - type: "assistant", - retry: undefined, - content: [{ type: "tool", id: "call_1", state: { status: "completed", content: [{ text: "done" }] } }], - }) - }) - - test("requests hydration when promotion admission was missed", () => { - const result = createV2SessionReducer().reduce( - [], - event({ - ...base, - id: "evt_promoted", - type: "session.input.promoted", - data: { sessionID: "ses_1", inputID: "msg_user" }, - }), - ) - - expect(result).toMatchObject({ sessionID: "ses_1", missing: "msg_user", touched: [] }) - }) -}) diff --git a/packages/app/src/context/server-session-v2-reducer.ts b/packages/app/src/context/server-session-v2-reducer.ts deleted file mode 100644 index b34ab3985e..0000000000 --- a/packages/app/src/context/server-session-v2-reducer.ts +++ /dev/null @@ -1,509 +0,0 @@ -import type { OpenCodeEvent, SessionMessageInfo, SessionPendingMessage } from "@opencode-ai/client/promise" - -type Assistant = Extract -type Compaction = Extract -type Shell = Extract - -export type V2SessionReduction = { - sessionID: string - messages: SessionMessageInfo[] - touched: string[] - missing?: string -} - -export function createV2SessionReducer() { - const pending = new Map() - - const reduce = (source: readonly SessionMessageInfo[], event: OpenCodeEvent): V2SessionReduction | undefined => { - if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return - const sessionID = event.data.sessionID - const result = (messages: SessionMessageInfo[], touched: string[] = []): V2SessionReduction => ({ - sessionID, - messages, - touched, - }) - const append = (message: SessionMessageInfo) => - result(source.some((item) => item.id === message.id) ? [...source] : [...source, message], [message.id]) - - switch (event.type) { - case "session.input.admitted": - pending.set(key(sessionID, event.data.inputID), event.data.input) - return result([...source]) - case "session.input.promoted": { - const input = pending.get(key(sessionID, event.data.inputID)) - pending.delete(key(sessionID, event.data.inputID)) - if (!input) return { ...result([...source]), missing: event.data.inputID } - if (input.type === "user") - return append({ - id: event.data.inputID, - type: "user", - metadata: input.data.metadata, - text: input.data.text, - files: input.data.files, - agents: input.data.agents, - time: { created: event.created }, - }) - return append({ - id: event.data.inputID, - type: "synthetic", - metadata: input.data.metadata, - text: input.data.text, - description: input.data.description, - time: { created: event.created }, - }) - } - case "session.agent.selected": - return append({ - id: messageID(event.id), - type: "agent-switched", - metadata: event.metadata, - agent: event.data.agent, - time: { created: event.created }, - }) - case "session.model.selected": - return append({ - id: messageID(event.id), - type: "model-switched", - metadata: event.metadata, - model: event.data.model, - previous: source.findLast( - (item): item is Extract => - item.type === "model-switched" || item.type === "assistant", - )?.model, - time: { created: event.created }, - }) - case "session.synthetic": - return append({ - id: messageID(event.id), - type: "synthetic", - metadata: event.data.metadata, - text: event.data.text, - description: event.data.description, - time: { created: event.created }, - }) - case "session.skill.activated": - return append({ - id: messageID(event.id), - type: "skill", - metadata: event.metadata, - skill: event.data.id, - name: event.data.name, - text: event.data.text, - time: { created: event.created }, - }) - case "session.shell.started": - return append({ - id: messageID(event.id), - type: "shell", - metadata: event.metadata, - shellID: event.data.shell.id, - command: event.data.shell.command, - status: event.data.shell.status, - exit: event.data.shell.exit, - time: { created: event.created }, - }) - case "session.shell.ended": - return updateMessage( - source, - (item): item is Shell => item.type === "shell" && item.shellID === event.data.shell.id, - (item) => ({ - ...item, - status: event.data.shell.status, - exit: event.data.shell.exit, - output: event.data.output, - time: { ...item.time, completed: event.created }, - }), - sessionID, - ) - case "session.step.started": { - const current = source.findLast((item): item is Assistant => item.type === "assistant" && !item.time.completed) - const completed = - current && current.id !== event.data.assistantMessageID - ? update(source, current.id, (item) => - item.type === "assistant" - ? { ...item, retry: undefined, time: { ...item.time, completed: event.created } } - : item, - ) - : [...source] - const existing = completed.find((item) => item.id === event.data.assistantMessageID) - if (existing?.type === "assistant") - return result( - update(completed, existing.id, (item) => - item.type === "assistant" - ? { - ...item, - agent: event.data.agent, - model: event.data.model, - retry: undefined, - error: undefined, - finish: undefined, - snapshot: event.data.snapshot ? { ...item.snapshot, start: event.data.snapshot } : item.snapshot, - time: { ...item.time, completed: undefined }, - } - : item, - ), - current && current.id !== existing.id ? [current.id, existing.id] : [existing.id], - ) - return result( - [ - ...completed, - { - id: event.data.assistantMessageID, - type: "assistant", - metadata: event.metadata, - agent: event.data.agent, - model: event.data.model, - content: [], - snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, - time: { created: event.created }, - }, - ], - current ? [current.id, event.data.assistantMessageID] : [event.data.assistantMessageID], - ) - } - case "session.step.ended": - return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ - ...item, - finish: event.data.finish, - cost: event.data.cost, - tokens: event.data.tokens, - snapshot: - event.data.snapshot || event.data.files - ? { ...item.snapshot, end: event.data.snapshot, files: event.data.files } - : item.snapshot, - time: { ...item.time, completed: event.created }, - })) - case "session.step.failed": - return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ - ...item, - finish: "error", - error: event.data.error, - retry: undefined, - cost: event.data.cost ?? item.cost, - tokens: event.data.tokens ?? item.tokens, - snapshot: - event.data.snapshot || event.data.files - ? { ...item.snapshot, end: event.data.snapshot, files: event.data.files } - : item.snapshot, - time: { ...item.time, completed: event.created }, - })) - case "session.text.started": - return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ - ...item, - content: insertOrdinal(item.content, "text", event.data.ordinal, { type: "text", text: "" }), - })) - case "session.text.delta": - return updateContent(source, event.data.assistantMessageID, sessionID, "text", event.data.ordinal, (item) => ({ - ...item, - text: item.text + event.data.delta, - })) - case "session.text.ended": - return updateContent(source, event.data.assistantMessageID, sessionID, "text", event.data.ordinal, (item) => ({ - ...item, - text: event.data.text, - })) - case "session.reasoning.started": - return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ - ...item, - content: insertOrdinal(item.content, "reasoning", event.data.ordinal, { - type: "reasoning", - text: "", - state: event.data.state, - time: { created: event.created }, - }), - })) - case "session.reasoning.delta": - return updateContent( - source, - event.data.assistantMessageID, - sessionID, - "reasoning", - event.data.ordinal, - (item) => ({ - ...item, - text: item.text + event.data.delta, - }), - ) - case "session.reasoning.ended": - return updateContent( - source, - event.data.assistantMessageID, - sessionID, - "reasoning", - event.data.ordinal, - (item) => ({ - ...item, - text: event.data.text, - state: event.data.state ?? item.state, - time: { created: item.time?.created ?? event.created, completed: event.created }, - }), - ) - case "session.tool.input.started": - return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ - ...item, - content: item.content.some((content) => content.type === "tool" && content.id === event.data.callID) - ? item.content - : [ - ...item.content, - { - type: "tool", - id: event.data.callID, - name: event.data.name, - state: { status: "streaming", input: "" }, - time: { created: event.created }, - }, - ], - })) - case "session.tool.input.delta": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => - tool.state.status === "streaming" - ? { ...tool, state: { ...tool.state, input: tool.state.input + event.data.delta } } - : tool, - ) - case "session.tool.input.ended": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => - tool.state.status === "streaming" ? { ...tool, state: { ...tool.state, input: event.data.text } } : tool, - ) - case "session.tool.called": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => ({ - ...tool, - executed: event.data.executed, - providerState: event.data.state, - // structured: {}, content: [] - state: { status: "running", input: event.data.input, metadata: {} }, - time: { ...tool.time, ran: event.created }, - })) - case "session.tool.progress": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => - tool.state.status === "running" - ? { - ...tool, - // state: { ...tool.state, structured: event.data.structured, content: event.data.content }, - state: { ...tool.state, metadata: event.data.metadata }, - } - : tool, - ) - case "session.tool.success": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => { - if (tool.state.status !== "running") return tool - return { - ...tool, - executed: event.data.executed || tool.executed === true, - providerResultState: event.data.resultState, - state: { - status: "completed", - input: tool.state.input, - // structured: event.data.structured, - metadata: event.data.metadata, - content: event.data.content, - // result: event.data.result, - }, - time: { ...tool.time, completed: event.created }, - } - }) - case "session.tool.failed": - return updateTool(source, event.data.assistantMessageID, event.data.callID, sessionID, (tool) => { - if (tool.state.status !== "streaming" && tool.state.status !== "running") return tool - return { - ...tool, - executed: event.data.executed || tool.executed === true, - providerResultState: event.data.resultState, - state: { - status: "error", - input: typeof tool.state.input === "string" ? {} : tool.state.input, - // structured: tool.state.status === "running" ? tool.state.structured : {}, - metadata: event.data.metadata ?? (tool.state.status === "running" ? tool.state.metadata : {}), - content: event.data.content, - error: event.data.error, - // result: event.data.result, - }, - time: { ...tool.time, completed: event.created }, - } - }) - case "session.retry.scheduled": - return updateAssistant(source, event.data.assistantMessageID, sessionID, (item) => ({ - ...item, - retry: { attempt: event.data.attempt, at: event.data.at, error: event.data.error }, - })) - case "session.execution.succeeded": - case "session.execution.failed": - case "session.execution.interrupted": { - const current = source.findLast((item): item is Assistant => item.type === "assistant" && !item.time.completed) - if (!current?.retry) return result([...source]) - return updateAssistant(source, current.id, sessionID, (item) => ({ ...item, retry: undefined })) - } - case "session.compaction.started": - return append({ - id: event.data.inputID ?? messageID(event.id), - type: "compaction", - status: "running", - metadata: event.metadata, - reason: event.data.reason, - summary: "", - recent: event.data.recent, - time: { created: event.created }, - }) - case "session.compaction.delta": - return updateMessage>( - source, - (item): item is Extract => - item.type === "compaction" && item.status === "running", - (item) => ({ - ...item, - summary: item.summary + event.data.text, - }), - sessionID, - ) - case "session.compaction.ended": { - const current = source.findLast( - (item): item is Extract => - item.type === "compaction" && item.status === "running", - ) - if (!current) - return append({ - id: messageID(event.id), - type: "compaction", - status: "completed", - metadata: event.metadata, - reason: event.data.reason, - summary: event.data.text, - recent: event.data.recent, - time: { created: event.created }, - }) - return result( - update(source, current.id, () => ({ - ...current, - status: "completed", - reason: event.data.reason, - summary: event.data.text, - recent: event.data.recent, - })), - [current.id], - ) - } - case "session.compaction.failed": { - const current = source.findLast( - (item): item is Extract => - item.type === "compaction" && item.status === "running", - ) - const failed: Extract = { - id: current?.id ?? event.data.inputID ?? messageID(event.id), - type: "compaction", - status: "failed", - metadata: current?.metadata ?? event.metadata, - reason: event.data.reason, - error: event.data.error, - time: current?.time ?? { created: event.created }, - } - if (!current) return append(failed) - return result( - update(source, current.id, () => failed), - [failed.id], - ) - } - default: - return - } - } - - return { - reduce, - clear(sessionID: string) { - for (const id of pending.keys()) { - if (id.startsWith(`${sessionID}:`)) pending.delete(id) - } - }, - } -} - -function key(sessionID: string, inputID: string) { - return `${sessionID}:${inputID}` -} - -function messageID(eventID: string) { - return eventID.replace(/^evt_/, "msg_") -} - -function update( - source: readonly SessionMessageInfo[], - id: string, - apply: (item: SessionMessageInfo) => SessionMessageInfo, -) { - return source.map((item) => (item.id === id ? apply(item) : item)) -} - -function updateMessage( - source: readonly SessionMessageInfo[], - matches: (item: SessionMessageInfo) => item is T, - apply: (item: T) => T, - sessionID: string, -): V2SessionReduction { - const current = source.findLast(matches) - if (!current) return { sessionID, messages: [...source], touched: [] } - return { - sessionID, - messages: update(source, current.id, (item) => (matches(item) ? apply(item) : item)), - touched: [current.id], - } -} - -function updateAssistant( - source: readonly SessionMessageInfo[], - id: string, - sessionID: string, - apply: (item: Assistant) => Assistant, -): V2SessionReduction { - return { - sessionID, - messages: update(source, id, (item) => (item.type === "assistant" ? apply(item) : item)), - touched: source.some((item) => item.id === id && item.type === "assistant") ? [id] : [], - } -} - -function updateContent( - source: readonly SessionMessageInfo[], - messageID: string, - sessionID: string, - type: T, - ordinal: number, - apply: ( - item: Extract, - ) => Extract, -) { - return updateAssistant(source, messageID, sessionID, (assistant) => { - let index = -1 - return { - ...assistant, - content: assistant.content.map((item) => { - if (item.type !== type || ++index !== ordinal) return item - return apply(item as Extract) - }), - } - }) -} - -function updateTool( - source: readonly SessionMessageInfo[], - messageID: string, - callID: string, - sessionID: string, - apply: ( - item: Extract, - ) => Extract, -) { - return updateAssistant(source, messageID, sessionID, (assistant) => ({ - ...assistant, - content: assistant.content.map((item) => (item.type === "tool" && item.id === callID ? apply(item) : item)), - })) -} - -function insertOrdinal( - source: Assistant["content"], - type: T, - ordinal: number, - item: Extract, -) { - const matches = source.filter((content) => content.type === type) - if (matches[ordinal]) return source - return [...source, item] -} diff --git a/packages/app/src/context/server-session.test.ts b/packages/app/src/context/server-session.test.ts index e9595e1376..38f7ff71e5 100644 --- a/packages/app/src/context/server-session.test.ts +++ b/packages/app/src/context/server-session.test.ts @@ -1,11 +1,7 @@ import { describe, expect, test } from "bun:test" import type { retry } from "@opencode-ai/core/util/retry" -import type { OpenCodeEvent, SessionApi } from "@opencode-ai/client/promise" import type { Message, OpencodeClient, Part, Session } from "@opencode-ai/sdk/v2/client" import { createServerSession } from "./server-session" -import type { ServerApi } from "@/utils/server" - -type MessageApi = ServerApi["message"] const session = (id: string, parentID?: string): Session => ({ id, @@ -162,57 +158,6 @@ function setup(sessions: Record) { } describe("server session", () => { - test("projects V2 session events into current and legacy message state", () => { - const ctx = setup({ child: session("child") }) - ctx.store.remember(session("child")) - ctx.store.set("session_message", "child", [ - { - id: "msg_1_user", - type: "user", - text: "hello", - time: { created: 1 }, - }, - ]) - const apply = (input: object) => ctx.store.applyV2(input as OpenCodeEvent) - - apply({ - id: "evt_step", - created: 2, - type: "session.step.started", - durable: { aggregateID: "child", seq: 1, version: 1 }, - location: { directory: "/repo" }, - data: { - sessionID: "child", - assistantMessageID: "msg_2_assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - }, - }) - apply({ - id: "evt_text_start", - created: 3, - type: "session.text.started", - durable: { aggregateID: "child", seq: 2, version: 1 }, - location: { directory: "/repo" }, - data: { sessionID: "child", assistantMessageID: "msg_2_assistant", ordinal: 0 }, - }) - apply({ - id: "evt_text_delta", - created: 4, - type: "session.text.delta", - location: { directory: "/repo" }, - data: { sessionID: "child", assistantMessageID: "msg_2_assistant", ordinal: 0, delta: "world" }, - }) - - expect(ctx.store.data.session_message.child?.at(-1)).toMatchObject({ - id: "msg_2_assistant", - type: "assistant", - content: [{ type: "text", text: "world" }], - }) - expect(ctx.store.data.message.child?.map((message) => message.id)).toEqual(["msg_1_user", "msg_2_assistant"]) - expect(ctx.store.data.part.msg_2_assistant).toMatchObject([{ type: "text", text: "world" }]) - }) - test("resolves lineage by session ID without directory", async () => { const ctx = setup({ child: session("child", "root"), root: session("root") }) @@ -233,116 +178,6 @@ describe("server session", () => { expect(ctx.store.data.message.root).toEqual([]) }) - test("loads current session content through the current message API", async () => { - const requests: unknown[] = [] - const user = { id: "msg_z_user", type: "user", text: "hello", time: { created: 1 } } - const assistant = { - id: "msg_a_assistant", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "hi" }], - time: { created: 2, completed: 3 }, - } - const client = { - session: { - messages: () => { - throw new Error("legacy message endpoint called") - }, - }, - } as unknown as OpencodeClient - const messageApi = { - list: async (input: unknown) => { - requests.push(input) - return { data: [assistant, user], cursor: { previous: null, next: null } } - }, - } as unknown as MessageApi - const store = createServerSession(client, {} as SessionApi, messageApi) - store.remember(session("root")) - - await store.sync("root") - - expect(requests).toEqual([{ sessionID: "root", limit: 20, order: "desc" }]) - expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id]) - }) - - test("extends a current page to include the user for split assistant turns", async () => { - const user = { id: "msg_1_user", type: "user", text: "hello", time: { created: 1 } } as const - const assistant = (id: string, created: number) => ({ - id, - type: "assistant" as const, - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text" as const, text: id }], - time: { created, completed: created }, - }) - const assistants = [ - assistant("msg_2_assistant", 2), - assistant("msg_3_assistant", 3), - assistant("msg_4_assistant", 4), - ] - const pages = [ - { data: assistants.slice(1).toReversed(), cursor: { previous: null, next: "older" } }, - { data: [assistants[0], user], cursor: { previous: null, next: null } }, - ] - const requests: unknown[] = [] - const messageApi = { - list: async (input: unknown) => { - requests.push(input) - return pages.shift()! - }, - } as unknown as MessageApi - const store = createServerSession({} as OpencodeClient, {} as SessionApi, messageApi) - store.remember(session("root")) - - await store.sync("root") - - expect(requests).toEqual([ - { sessionID: "root", limit: 20, order: "desc" }, - { sessionID: "root", limit: 20, cursor: "older" }, - ]) - expect(store.data.message.root.map((message) => message.id)).toEqual([ - user.id, - ...assistants.map((item) => item.id), - ]) - expect(assistants.map((item) => store.data.part[item.id]?.[0]?.type)).toEqual(["text", "text", "text"]) - }) - - test("indexes V1 messages for the current timeline projection", async () => { - const user = userMessage("message-1", { sessionID: "root" }) - const assistant = assistantMessage("message-2", user.id, { sessionID: "root" }) - const client = messageClient( - response([ - { info: user, parts: [textPart(user.id, { sessionID: "root" })] }, - { info: assistant, parts: [textPart(assistant.id, { sessionID: "root" })] }, - ]), - ) - const messageApi = { - list: () => { - throw new Error("current message endpoint called") - }, - } as unknown as MessageApi - const store = createServerSession(client, {} as SessionApi, messageApi, { - protocol: Promise.resolve("v1"), - }) - store.remember(session("root")) - - await store.sync("root") - - expect(store.data.message.root.map((message) => message.id)).toEqual([user.id, assistant.id]) - expect(store.data.session_message.root).toMatchObject([ - { id: user.id, type: "user", text: "text" }, - { id: assistant.id, type: "assistant" }, - ]) - - const next = userMessage("message-3", { sessionID: "root" }) - store.apply({ type: "message.updated", properties: { info: next } }) - expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id, next.id]) - - store.apply({ type: "message.removed", properties: { sessionID: "root", messageID: next.id } }) - expect(store.data.session_message.root.map((message) => message.id)).toEqual([user.id, assistant.id]) - }) - test("backfills an assistant-only initial page through its user root", async () => { const user = userMessage("message-1") const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)] @@ -365,49 +200,6 @@ describe("server session", () => { expect(store.history.more("child")).toBe(true) }) - test("keeps assistant history when its deleted parent cannot be backfilled", async () => { - const missing = Promise.withResolvers() - const assistant = assistantMessage("message-2", "message-missing") - const client = rootMessageClient([response([{ info: assistant, parts: [] }], "older")], [missing.promise]) - const store = createServerSession(client) - const loading = store.sync("child") - await client.rootRequested(1) - - missing.reject(new Error("Message not found: message-missing", { cause: { status: 404 } })) - await loading - - expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }]) - expect(store.data.message.child).toEqual([assistant]) - expect(store.history.more("child")).toBe(true) - }) - - test("drops a cached parent when a forced refresh confirms it was deleted", async () => { - const missing = Promise.withResolvers() - const parent = userMessage("message-1") - const part = textPart(parent.id) - const assistant = assistantMessage("message-2", parent.id) - const client = rootMessageClient( - [ - response([ - { info: parent, parts: [part] }, - { info: assistant, parts: [] }, - ]), - response([{ info: assistant, parts: [] }], "older"), - ], - [missing.promise], - ) - const store = createServerSession(client) - await store.sync("child") - const loading = store.sync("child", { force: true }) - await client.rootRequested(1) - - missing.reject(new Error(`Message not found: ${parent.id}`, { cause: { status: 404 } })) - await loading - - expect(store.data.message.child).toEqual([assistant]) - expect(store.data.part[parent.id]).toBeUndefined() - }) - test("does not let an optimistic user suppress initial root backfill", async () => { const user = userMessage("message-1") const part = textPart(user.id) diff --git a/packages/app/src/context/server-session.ts b/packages/app/src/context/server-session.ts index 69bec61ba2..a440aab38f 100644 --- a/packages/app/src/context/server-session.ts +++ b/packages/app/src/context/server-session.ts @@ -1,6 +1,5 @@ import { Binary } from "@opencode-ai/core/util/binary" import { retry } from "@opencode-ai/core/util/retry" -import type { OpenCodeEvent, SessionApi, SessionMessageInfo } from "@opencode-ai/client/promise" import type { Message, OpencodeClient, @@ -9,21 +8,15 @@ import type { QuestionRequest, Session, SessionStatus, + SnapshotFileDiff, Todo, } from "@opencode-ai/sdk/v2/client" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { batch } from "solid-js" import { createStore, produce, reconcile } from "solid-js/store" -import { message as cleanMessage } from "@/utils/diffs" +import { diffs as cleanDiffs, message as cleanMessage } from "@/utils/diffs" import { sessionNotFoundError } from "@/utils/server-errors" import { rootSession } from "@/utils/session-route" -import { normalizeSessionInfo } from "@/utils/session" -import { normalizeSessionMessages } from "@/utils/session-message" import { dropSessionCaches, pickSessionCacheEvictions, SESSION_CACHE_LIMIT } from "./global-sync/session-cache" -import { createV2SessionReducer, type V2SessionReduction } from "./server-session-v2-reducer" -import type { ServerApi } from "@/utils/server" - -type MessageApi = ServerApi["message"] const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) const cmpMessage = (a: Message, b: Message) => a.time.created - b.time.created || cmp(a.id, b.id) @@ -33,17 +26,6 @@ const historyMessagePageSize = 200 const sessionInfoLimit = 2_048 const emptyIDs: ReadonlySet = new Set() -function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) { - const boundary = source.find( - (message) => - message.type === "user" || - message.type === "shell" || - message.type === "assistant" || - (message.type === "synthetic" && message.description?.trim()), - ) - return boundary?.type === "assistant" -} - type OptimisticItem = { message: Message parts: Part[] @@ -54,37 +36,10 @@ type OptimisticItem = { type MessagePage = { session: Message[] part: { id: string; part: Part[] }[] - source?: SessionMessageInfo[] - sourceMode?: "latest" | "older" - projectSource?: boolean cursor?: string complete: boolean } -function legacyMessageSource(items: { info: Message; parts: Part[] }[]): SessionMessageInfo[] { - return items - .slice() - .sort((a, b) => cmp(a.info.id, b.info.id)) - .map((item) => { - if (item.info.role === "user") { - return { - id: item.info.id, - type: "user" as const, - text: item.parts.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n"), - time: item.info.time, - } - } - return { - id: item.info.id, - type: "assistant" as const, - agent: item.info.agent ?? item.info.mode, - model: { id: item.info.modelID, providerID: item.info.providerID, variant: item.info.variant }, - content: [], - time: item.info.time, - } - }) -} - // Most markers describe the current HTTP attempt; deltaParts persists non-durable stream state across retries. type MessageLoadState = { touchedMessages: Set @@ -97,7 +52,6 @@ type MessageLoadState = { optimisticParts: Map> orphanParents: Set clearedMessageParts: Set - touchedSource: Set } type MessageLoadBaseline = Pick< @@ -156,7 +110,6 @@ function reconcileFetched( options: { touched?: ReadonlySet retained?: ReadonlySet - removed?: ReadonlySet preserveUnfetched?: boolean | ((item: T) => boolean) } = {}, ) { @@ -179,29 +132,18 @@ function reconcileFetched( if (item) result.set(id, item) if (!item) result.delete(id) } - for (const id of options.removed ?? emptyIDs) result.delete(id) return [...result.values()].sort((a, b) => cmp(a.id, b.id)) } -type ServerSessionOptions = { retry?: typeof retry; protocol?: Promise<"v1" | "v2"> } - -export function createServerSession( - client: OpencodeClient, - sessionApiOrOptions?: SessionApi | ServerSessionOptions, - messageApi?: MessageApi, - currentOptions?: ServerSessionOptions, -) { - const sessionApi = messageApi ? (sessionApiOrOptions as SessionApi) : undefined - const options = messageApi ? currentOptions : (sessionApiOrOptions as ServerSessionOptions | undefined) +export function createServerSession(client: OpencodeClient, options?: { retry?: typeof retry }) { const [data, setData] = createStore({ info: {} as Record, session_status: {} as Record, - session_diff: {} as Record, + session_diff: {} as Record, todo: {} as Record, permission: {} as Record, question: {} as Record, message: {} as Record, - session_message: {} as Record, part: {} as Record, part_text_accum_delta: {} as Record, session_working(id: string) { @@ -210,9 +152,9 @@ export function createServerSession( }) const requests = new Map>() const inflight = new Map>() + const inflightDiff = new Map>() const inflightTodo = new Map>() const optimistic = new Map>() - const v2 = createV2SessionReducer() const messageLoads = new Map() const pendingParts = new Map>>() const orphanParts = new Map>() @@ -247,16 +189,6 @@ export function createServerSession( at: {} as Record, }) - const indexLegacyMessage = (message: Message) => { - const current = data.session_message[message.sessionID] ?? [] - if (current.some((item) => item.id === message.id)) return - setData( - "session_message", - message.sessionID, - reconcile([...current, ...legacyMessageSource([{ info: message, parts: [] }])]), - ) - } - const remember = (session: Session) => { setData("info", session.id, reconcile(session)) infoSeen.delete(session.id) @@ -266,6 +198,7 @@ export function createServerSession( ...pinned.keys(), ...requests.keys(), ...inflight.keys(), + ...inflightDiff.keys(), ...inflightTodo.keys(), ...messageLoads.keys(), ...optimistic.keys(), @@ -307,31 +240,27 @@ export function createServerSession( const pending = requests.get(sessionID) if (pending) return pending const active = generation(sessionID) - const request = sessionApi - ? sessionApi.get({ sessionID }).then(normalizeSessionInfo) - : client.session.get({ sessionID }).then((result) => { - if (!result.data) throw sessionNotFoundError(sessionID) - return result.data - }) - const resolved = request.then((result) => { - if (generations.get(sessionID) !== active) return result - return remember(result) + const request = client.session.get({ sessionID }).then((result) => { + if (!result.data) throw sessionNotFoundError(sessionID) + if (generations.get(sessionID) !== active) return result.data + return remember(result.data) }) - requests.set(sessionID, resolved) + requests.set(sessionID, request) const cleanup = () => { - if (requests.get(sessionID) === resolved) requests.delete(sessionID) + if (requests.get(sessionID) === request) requests.delete(sessionID) if ( generations.get(sessionID) === active && !data.info[sessionID] && !requests.has(sessionID) && !messageLoads.has(sessionID) && !inflight.has(sessionID) && + !inflightDiff.has(sessionID) && !inflightTodo.has(sessionID) ) generations.delete(sessionID) } - void resolved.then(cleanup, cleanup) - return resolved + void request.then(cleanup, cleanup) + return request } const peekLineage = (sessionID: string) => { @@ -488,9 +417,9 @@ export function createServerSession( clearOptimistic(sessionID) requests.delete(sessionID) inflight.delete(sessionID) + inflightDiff.delete(sessionID) inflightTodo.delete(sessionID) messageLoads.delete(sessionID) - v2.clear(sessionID) pendingParts.delete(sessionID) orphanParts.delete(sessionID) removedMessages.delete(sessionID) @@ -518,6 +447,7 @@ export function createServerSession( ...pinned.keys(), ...requests.keys(), ...inflight.keys(), + ...inflightDiff.keys(), ...inflightTodo.keys(), ...messageLoads.keys(), ...optimistic.keys(), @@ -538,34 +468,6 @@ export function createServerSession( ) const fetchMessages = async (sessionID: string, limit: number, before?: string, onAttempt?: () => void) => { - if (messageApi && (await options?.protocol) !== "v1") { - const request = (cursor?: string) => - (options?.retry ?? retry)(() => { - onAttempt?.() - return messageApi.list(cursor ? { sessionID, limit, cursor } : { sessionID, limit, order: "desc" }) - }) - const first = await request(before) - const pages = [first] - while (pages.at(-1)?.cursor.next && needsOlderTurnRoot(pages.flatMap((page) => page.data).toReversed())) { - const response = await request(pages.at(-1)!.cursor.next ?? undefined) - pages.push(response) - if (!response.data.length) break - } - const response = pages.at(-1)! - const source = pages.flatMap((page) => page.data).toReversed() - const normalized = normalizeSessionMessages(sessionID, source) - return { - session: normalized.messages.sort((a, b) => cmp(a.id, b.id)), - part: [...normalized.parts.entries()] - .map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) })) - .sort((a, b) => cmp(a.id, b.id)), - source, - sourceMode: before ? ("older" as const) : ("latest" as const), - projectSource: true, - cursor: response.cursor.next ?? undefined, - complete: response.data.length === 0, - } - } const response = await (options?.retry ?? retry)(() => { onAttempt?.() return client.session.messages({ sessionID, limit, before }) @@ -577,24 +479,12 @@ export function createServerSession( id: item.info.id, part: item.parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)), })), - source: legacyMessageSource(items), - sourceMode: before ? ("older" as const) : ("latest" as const), cursor: response.response.headers.get("x-next-cursor") ?? undefined, complete: !response.response.headers.get("x-next-cursor"), } } const fetchMessage = async (sessionID: string, messageID: string, onAttempt?: () => void) => { - if (sessionApi && (await options?.protocol) !== "v1") { - const response = await (options?.retry ?? retry)(() => { - onAttempt?.() - return sessionApi.message({ sessionID, messageID }) - }) - const normalized = normalizeSessionMessages(sessionID, [response]) - const message = normalized.messages[0] - if (!message) throw new Error(`Message not found: ${messageID}`) - return { message, parts: normalized.parts.get(messageID) ?? [] } - } const response = await (options?.retry ?? retry)(() => { onAttempt?.() return client.session.message({ sessionID, messageID }) @@ -679,31 +569,7 @@ export function createServerSession( preserveUnfetched: boolean | ((message: Message) => boolean), cleanupOrphans: boolean, ) => { - const source = page.source - ? (() => { - const incoming = new Map(page.source.map((message) => [message.id, message])) - const existing = data.session_message[sessionID] ?? [] - const current = existing.filter((message) => !incoming.has(message.id)) - const live = new Map(existing.map((message) => [message.id, message])) - return (page.sourceMode === "older" ? [...page.source, ...current] : [...current, ...page.source]).map( - (message) => (load?.touchedSource.has(message.id) ? (live.get(message.id) ?? message) : message), - ) - })() - : undefined - const projected = - page.projectSource && source - ? (() => { - const normalized = normalizeSessionMessages(sessionID, source) - return { - ...page, - session: normalized.messages.sort((a, b) => cmp(a.id, b.id)), - part: [...normalized.parts.entries()] - .map(([id, part]) => ({ id, part: part.sort((a, b) => cmp(a.id, b.id)) })) - .sort((a, b) => cmp(a.id, b.id)), - } - })() - : page - const merged = mergeOptimisticPage(projected, [...(optimistic.get(sessionID)?.values() ?? [])]) + const merged = mergeOptimisticPage(page, [...(optimistic.get(sessionID)?.values() ?? [])]) merged.observed.forEach((item) => { if (!load?.clearedMessageParts.has(item.messageID)) confirmOptimistic(sessionID, item.messageID, item.parts) }) @@ -711,11 +577,9 @@ export function createServerSession( const messages = reconcileFetched(merged.session, data.message[sessionID] ?? [], { touched: touchedMessages, retained: load?.retainedMessages, - removed: load?.removedMessages, preserveUnfetched, }) batch(() => { - if (source) setData("session_message", sessionID, reconcile(source)) const messageIDs = replaceMessages(sessionID, messages) replaceParts(sessionID, merged.part, messageIDs, load) const orphans = orphanParts.get(sessionID) @@ -746,7 +610,6 @@ export function createServerSession( optimisticParts: new Map(), orphanParents: new Set(), clearedMessageParts: new Set(), - touchedSource: new Set(), } messageLoads.set(sessionID, load) setMeta("loading", sessionID, true) @@ -782,15 +645,7 @@ export function createServerSession( if (generations.get(sessionID) !== active) break const parent = await fetchMessage(sessionID, parentID, () => resetMessageLoad(sessionID, load, messageLoadBaseline(load, parentID)), - ).catch((error) => { - const cause = error instanceof Error && typeof error.cause === "object" ? error.cause : undefined - if (cause && "status" in cause && cause.status === 404) { - load.removedMessages.add(parentID) - return - } - throw error - }) - if (!parent) continue + ) if (parent.message.role !== "user") throw new Error(`Assistant parent is not a user message: ${parentID}`) parents.push(parent) } @@ -881,109 +736,6 @@ export function createServerSession( return properties.part.sessionID } - const projectV2 = (reduction: V2SessionReduction) => { - reduction.touched.forEach((messageID) => messageLoads.get(reduction.sessionID)?.touchedSource.add(messageID)) - setData("session_message", reduction.sessionID, reconcile(reduction.messages)) - if (reduction.touched.length === 0) return - - const touched = new Set(reduction.touched) - let parentID: string | undefined - for (const message of reduction.messages) { - if (message.type === "user" || (message.type === "synthetic" && message.description?.trim())) - parentID = message.id - if (message.type === "shell") { - if (touched.has(message.id)) touched.add(`${message.id}:assistant`) - parentID = undefined - } - if (message.type === "assistant" && touched.has(message.id) && parentID) touched.add(parentID) - if (message.type === "compaction" && touched.has(message.id) && parentID) touched.add(parentID) - } - - const normalized = normalizeSessionMessages(reduction.sessionID, reduction.messages) - batch(() => { - for (const message of normalized.messages) { - if (!touched.has(message.id)) continue - apply({ type: "message.updated", properties: { sessionID: reduction.sessionID, info: message } }) - } - for (const messageID of touched) { - const next = normalized.parts.get(messageID) ?? [] - const nextIDs = new Set(next.map((part) => part.id)) - for (const part of next) { - apply({ type: "message.part.updated", properties: { sessionID: reduction.sessionID, part } }) - } - for (const part of data.part[messageID] ?? []) { - if (nextIDs.has(part.id)) continue - apply({ - type: "message.part.removed", - properties: { sessionID: reduction.sessionID, messageID, partID: part.id }, - }) - } - } - }) - } - - const hydrateV2Message = (sessionID: string, messageID: string) => { - if (!sessionApi) return - void sessionApi - .message({ sessionID, messageID }) - .then((message) => { - const current = data.session_message[sessionID] ?? [] - const messages = [...current.filter((item) => item.id !== message.id), message].sort((a, b) => cmp(a.id, b.id)) - projectV2({ sessionID, messages, touched: [message.id] }) - }) - .catch(() => {}) - } - - const applyV2 = (event: OpenCodeEvent) => { - if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return - const sessionID = event.data.sessionID - const reduction = v2.reduce(data.session_message[sessionID] ?? [], event) - if (reduction) { - projectV2(reduction) - if (reduction.missing) hydrateV2Message(sessionID, reduction.missing) - } - - const info = data.info[sessionID] - if (event.type === "session.renamed" && info) - remember({ ...info, title: event.data.title, time: { ...info.time, updated: event.created } }) - if (event.type === "session.moved" && info) - remember({ - ...info, - projectID: event.data.projectID ?? info.projectID, - workspaceID: event.data.location.workspaceID, - directory: event.data.location.directory, - path: event.data.subpath, - time: { ...info.time, updated: event.created }, - }) - if (event.type === "session.usage.updated" && info) - remember({ ...info, cost: event.data.cost, tokens: event.data.tokens }) - // if (event.type === "session.archived") { - // if (info) remember({ ...info, time: { ...info.time, archived: event.created, updated: event.created } }) - // evict([sessionID]) - // } - if (event.type === "session.execution.started") setData("session_status", sessionID, { type: "busy" }) - if ( - event.type === "session.execution.succeeded" || - event.type === "session.execution.failed" || - event.type === "session.execution.interrupted" - ) - setData("session_status", sessionID, { type: "idle" }) - if (event.type === "session.retry.scheduled") - setData("session_status", sessionID, { - type: "retry", - attempt: event.data.attempt, - message: event.data.error.message, - next: event.data.at, - }) - if (event.type === "session.forked") void resolve(sessionID, { force: true }).catch(() => {}) - if ( - event.type === "session.revert.staged" || - event.type === "session.revert.cleared" || - event.type === "session.revert.committed" - ) - void resolve(sessionID, { force: true }).catch(() => {}) - } - const apply = (event: { type: string; properties?: unknown }) => { const eventID = eventSessionID(event) if (eventID) { @@ -1007,9 +759,7 @@ export function createServerSession( return } case "session.deleted": { - const properties = event.properties as { sessionID?: string; info?: Session } - const sessionID = properties.info?.id ?? properties.sessionID - if (!sessionID) return + const sessionID = (event.properties as { info: Session }).info.id infoSeen.delete(sessionID) setData( "info", @@ -1018,6 +768,11 @@ export function createServerSession( evict([sessionID]) return } + case "session.diff": { + const props = event.properties as { sessionID: string; diff: SnapshotFileDiff[] } + setData("session_diff", props.sessionID, reconcile(cleanDiffs(props.diff), { key: "file" })) + return + } case "todo.updated": { const props = event.properties as { sessionID: string; todos: Todo[] } setData("todo", props.sessionID, reconcile(props.todos, { key: "id" })) @@ -1030,7 +785,6 @@ export function createServerSession( } case "message.updated": { const info = cleanMessage((event.properties as { info: Message }).info) - indexLegacyMessage(info) const load = messageLoads.get(info.sessionID) load?.touchedMessages.add(info.id) load?.removedMessages.delete(info.id) @@ -1063,9 +817,6 @@ export function createServerSession( } case "message.removed": { const props = event.properties as { sessionID: string; messageID: string } - setData("session_message", props.sessionID, (messages) => - messages?.filter((message) => message.id !== props.messageID), - ) const load = messageLoads.get(props.sessionID) load?.touchedMessages.add(props.messageID) load?.removedMessages.add(props.messageID) @@ -1378,16 +1129,23 @@ export function createServerSession( setData(produce((draft) => deleteMessageParts(draft, input.messageID))) }, }, - async todo(sessionID: string, request?: { force?: boolean }) { + diff(sessionID: string, options?: { force?: boolean }) { touch(sessionID) - if (data.todo[sessionID] !== undefined && !request?.force) return - if ((await options?.protocol) === "v2") { - setData("todo", sessionID, []) - return - } + if (data.session_diff[sessionID] !== undefined && !options?.force) return Promise.resolve() + return runInflight(inflightDiff, sessionID, () => { + const active = generation(sessionID) + return retry(() => client.session.diff({ sessionID })).then((result) => { + if (generations.get(sessionID) !== active) return + setData("session_diff", sessionID, reconcile(cleanDiffs(result.data), { key: "file" })) + }) + }) + }, + todo(sessionID: string, options?: { force?: boolean }) { + touch(sessionID) + if (data.todo[sessionID] !== undefined && !options?.force) return Promise.resolve() return runInflight(inflightTodo, sessionID, () => { const active = generation(sessionID) - return (options?.retry ?? retry)(() => client.session.todo({ sessionID })).then((result) => { + return retry(() => client.session.todo({ sessionID })).then((result) => { if (generations.get(sessionID) !== active) return setData("todo", sessionID, reconcile(result.data ?? [], { key: "id" })) }) @@ -1421,7 +1179,6 @@ export function createServerSession( if (count && count > 1) pinned.set(sessionID, count - 1) }, apply, - applyV2, } } diff --git a/packages/app/src/context/server-sync.test.ts b/packages/app/src/context/server-sync.test.ts index 7821a09e20..93e9c41755 100644 --- a/packages/app/src/context/server-sync.test.ts +++ b/packages/app/src/context/server-sync.test.ts @@ -1,106 +1,6 @@ import { describe, expect, test } from "bun:test" -import type { OpencodeClient } from "@opencode-ai/sdk/v2/client" -import type { - McpListInput, - McpResourceCatalogInput, - SessionApi, - SessionInfo, - SessionListInput, -} from "@opencode-ai/client/promise" -import { QueryClient } from "@tanstack/solid-query" import { canDisposeDirectory, pickDirectoriesToEvict } from "./global-sync/eviction" -import { estimateRootSessionTotal, loadRootSessions } from "./global-sync/session-load" -import { loadActiveSessionsQuery, loadMcpQuery, loadMcpResourcesQuery, seedActiveSessionStatuses } from "./server-sync" -import { ServerScope } from "@/utils/server-scope" -import { createServerSession } from "./server-session" -import type { ServerApi } from "@/utils/server" - -type McpApi = ServerApi["mcp"] - -describe("MCP queries", () => { - test("loads current servers for the requested location", async () => { - const calls: unknown[] = [] - const queryClient = new QueryClient() - const result = await queryClient.fetchQuery( - loadMcpQuery(ServerScope.local, "/project", { - list: async (input: McpListInput = {}) => { - calls.push(input) - return { - location: { directory: "/project", project: { id: "project", directory: "/project" } }, - data: [ - { name: "docs", status: { status: "connected" } }, - { name: "search", status: { status: "pending" } }, - ], - } - }, - } as unknown as McpApi), - ) - - expect(calls).toEqual([{ location: { directory: "/project" } }]) - expect(result).toEqual({ docs: { status: "connected" }, search: { status: "pending" } }) - }) - - test("loads and keys the current resource catalog", async () => { - const calls: unknown[] = [] - const queryClient = new QueryClient() - const result = await queryClient.fetchQuery( - loadMcpResourcesQuery(ServerScope.local, "/project", { - resource: { - catalog: async (input: McpResourceCatalogInput = {}) => { - calls.push(input) - return { - location: { directory: "/project", project: { id: "project", directory: "/project" } }, - data: { - resources: [{ server: "docs", name: "Guide", uri: "docs://guide" }], - templates: [], - }, - } - }, - }, - } as unknown as McpApi), - ) - - expect(calls).toEqual([{ location: { directory: "/project" } }]) - expect(result).toEqual({ "docs:docs://guide": { server: "docs", name: "Guide", uri: "docs://guide" } }) - }) -}) - -describe("active session query", () => { - test("loads active sessions immediately and once per server cache", async () => { - let calls = 0 - const queryClient = new QueryClient() - const options = loadActiveSessionsQuery(ServerScope.local, { - active: async () => { - calls++ - return { ses_running: { type: "running" } } - }, - }) - - expect(await queryClient.fetchQuery(options)).toEqual({ ses_running: { type: "running" } }) - expect(await queryClient.fetchQuery(options)).toEqual({ ses_running: { type: "running" } }) - expect(calls).toBe(1) - expect(options.enabled).toBe(true) - expect([...options.queryKey]).toEqual([ServerScope.local, "activeSessions"]) - }) - - test("does not overwrite statuses already written by events", () => { - const session = createServerSession({} as OpencodeClient) - session.set("session_status", "ses_retry", { type: "retry", attempt: 2, message: "retrying", next: 10 }) - - seedActiveSessionStatuses(session, { - ses_running: { type: "running" }, - ses_retry: { type: "running" }, - }) - - expect(session.data.session_status.ses_running).toEqual({ type: "busy" }) - expect(session.data.session_status.ses_retry).toEqual({ - type: "retry", - attempt: 2, - message: "retrying", - next: 10, - }) - }) -}) +import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load" describe("pickDirectoriesToEvict", () => { test("keeps pinned stores and evicts idle stores", () => { @@ -123,57 +23,46 @@ describe("pickDirectoriesToEvict", () => { }) }) -describe("loadRootSessions", () => { - test("loads and normalizes a limited page of root sessions", async () => { - const calls: SessionListInput[] = [] +describe("loadRootSessionsWithFallback", () => { + test("uses limited roots query when supported", async () => { + const calls: Array<{ directory: string; roots: true; limit?: number }> = [] - const result = await loadRootSessions({ - api: { - list: async (query = {}) => { - calls.push(query) - return { data: [sessionInfo("session-1")], cursor: {} } - }, - } satisfies Pick, + const result = await loadRootSessionsWithFallback({ directory: "dir", limit: 10, + list: async (query) => { + calls.push(query) + return { data: [] } + }, }) - expect(result.data).toEqual([ - expect.objectContaining({ id: "session-1", directory: "dir", slug: "session-1", version: "" }), - ]) + expect(result.data).toEqual([]) expect(result.limited).toBe(true) - expect(calls).toEqual([{ directory: "dir", parentID: null, limit: 10, order: "desc" }]) + expect(calls).toEqual([{ directory: "dir", roots: true, limit: 10 }]) }) - test("propagates list failures", () => { - expect( - loadRootSessions({ - api: { - list: async () => { - throw new Error("failed") - }, - } satisfies Pick, - directory: "dir", - limit: 25, - }), - ).rejects.toThrow("failed") + test("falls back to full roots query on limited-query failure", async () => { + const calls: Array<{ directory: string; roots: true; limit?: number }> = [] + + const result = await loadRootSessionsWithFallback({ + directory: "dir", + limit: 25, + list: async (query) => { + calls.push(query) + if (query.limit) throw new Error("unsupported") + return { data: [] } + }, + }) + + expect(result.data).toEqual([]) + expect(result.limited).toBe(false) + expect(calls).toEqual([ + { directory: "dir", roots: true, limit: 25 }, + { directory: "dir", roots: true }, + ]) }) }) -function sessionInfo(id: string) { - return { - id, - projectID: "project-1", - agent: "build", - model: { id: "model-1", providerID: "provider-1" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1, updated: 1 }, - title: id, - location: { directory: "dir" }, - } as SessionInfo -} - describe("estimateRootSessionTotal", () => { test("keeps exact total for full fetches", () => { expect(estimateRootSessionTotal({ count: 42, limit: 10, limited: false })).toBe(42) diff --git a/packages/app/src/context/server-sync.tsx b/packages/app/src/context/server-sync.tsx index 13a0b74bc6..05806fba54 100644 --- a/packages/app/src/context/server-sync.tsx +++ b/packages/app/src/context/server-sync.tsx @@ -1,10 +1,10 @@ import type { Config, + McpResource, OpencodeClient, Path, Project, ProviderAuthResponse, - SessionStatus, } from "@opencode-ai/sdk/v2/client" import { showToast } from "@/utils/toast" import { getFilename } from "@opencode-ai/core/util/path" @@ -18,7 +18,6 @@ import { bootstrapGlobal, clearProviderRev, loadAgentsQuery, - loadCommands, loadGlobalConfigQuery, loadPathQuery, loadProjectsQuery, @@ -27,13 +26,12 @@ import { } from "./global-sync/bootstrap" import { createChildStoreManager } from "./global-sync/child-store" import { applyDirectoryEvent, applyGlobalEvent } from "./global-sync/event-reducer" -import { estimateRootSessionTotal, loadRootSessions, loadRootSessionsV1 } from "./global-sync/session-load" +import { estimateRootSessionTotal, loadRootSessionsWithFallback } from "./global-sync/session-load" import { trimSessions } from "./global-sync/session-trim" import type { ProjectMeta } from "./global-sync/types" import { SESSION_RECENT_LIMIT } from "./global-sync/types" import { formatServerError } from "@/utils/server-errors" import { queryOptions, useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/solid-query" -import type { SolidQueryOptions } from "@tanstack/solid-query" import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" import { PathKey } from "@/utils/path-key" @@ -47,18 +45,8 @@ import { retry } from "@opencode-ai/core/util/retry" import type { ServerScope } from "@/utils/server-scope" import { createHomeSessionIndexCache } from "./global-sync/home-session-index" import { persisted } from "@/utils/persist" -import type { ServerApi } from "@/utils/server" -import type { - McpListInput, - McpListOutput, - McpResource, - McpResourceCatalogInput, - McpResourceCatalogOutput, - McpServer, - SessionActiveOutput, -} from "@opencode-ai/client/promise" import { toggleMcp } from "./global-sync/mcp" -import { createServerSession, type ServerSession } from "./server-session" +import { createServerSession } from "./server-session" type GlobalStore = { ready: boolean @@ -71,76 +59,16 @@ type GlobalStore = { reload: undefined | "pending" | "complete" } -type McpListApi = { - readonly list: (input?: McpListInput) => Promise -} - -type McpResourceApi = { - readonly resource: { - readonly catalog: (input?: McpResourceCatalogInput) => Promise - } -} - -type ApiQueryOptions = SolidQueryOptions & { - initialData?: undefined - queryKey: K -} - -type SessionActiveApi = { - readonly active: () => Promise -} - -export const loadMcpQuery = ( - scope: ServerScope, - directory: string, - api: McpListApi, - legacy?: OpencodeClient, - protocol?: Promise<"v1" | "v2">, -): ApiQueryOptions, readonly [ServerScope, string, "mcp"]> => - queryOptions< - Record, - Error, - Record, - readonly [ServerScope, string, "mcp"] - >({ +export const loadMcpQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) => + queryOptions({ queryKey: [scope, directory, "mcp"] as const, - queryFn: async () => { - if ((await protocol) === "v1" && legacy) return (await legacy.mcp.status()).data ?? {} - return api - .list({ location: { directory } }) - .then((result) => Object.fromEntries(result.data.map((server) => [server.name, server.status]))) - }, + queryFn: () => sdk.mcp.status().then((r) => r.data ?? {}), }) -export const loadMcpResourcesQuery = ( - scope: ServerScope, - directory: string, - api: McpResourceApi, - legacy?: OpencodeClient, - protocol?: Promise<"v1" | "v2">, -): ApiQueryOptions, readonly [ServerScope, string, "mcpResources"]> => - queryOptions< - Record, - Error, - Record, - readonly [ServerScope, string, "mcpResources"] - >({ +export const loadMcpResourcesQuery = (scope: ServerScope, directory: string, sdk: OpencodeClient) => + queryOptions>({ queryKey: [scope, directory, "mcpResources"] as const, - queryFn: async () => { - if ((await protocol) === "v1" && legacy) { - return Object.fromEntries( - Object.entries((await legacy.experimental.resource.list()).data ?? {}).map(([key, resource]) => [ - key, - { ...resource, server: resource.client }, - ]), - ) - } - return api.resource - .catalog({ location: { directory } }) - .then((result) => - Object.fromEntries(result.data.resources.map((resource) => [`${resource.server}:${resource.uri}`, resource])), - ) - }, + queryFn: () => sdk.experimental.resource.list().then((r) => r.data ?? {}), placeholderData: {}, }) @@ -150,52 +78,22 @@ export const loadLspQuery = (scope: ServerScope, directory: string, sdk: Opencod queryFn: () => sdk.lsp.status().then((r) => r.data ?? []), }) -export const loadActiveSessionsQuery = ( - scope: ServerScope, - api: SessionActiveApi, -): ApiQueryOptions => - queryOptions({ - queryKey: [scope, "activeSessions"] as const, - queryFn: () => api.active(), - enabled: true, - staleTime: Number.POSITIVE_INFINITY, - gcTime: Number.POSITIVE_INFINITY, - refetchOnMount: false, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - }) - -export function seedActiveSessionStatuses( - session: Pick, - active: SessionActiveOutput | Record, -) { - for (const sessionID of Object.keys(active)) { - if (session.data.session_status[sessionID] !== undefined) continue - const status = active[sessionID] - session.set("session_status", sessionID, status?.type === "running" ? { type: "busy" } : status) - } -} - function makeQueryOptionsApi( scope: ServerScope, serverSDK: () => OpencodeClient, - serverAPI: ServerApi, sdkFor: (dir: PathKey) => OpencodeClient, - protocol: Promise<"v1" | "v2">, ) { return { - globalConfig: () => loadGlobalConfigQuery(scope, serverSDK(), protocol), - projects: () => loadProjectsQuery(scope, serverAPI.project), + globalConfig: () => loadGlobalConfigQuery(scope, serverSDK()), + projects: () => loadProjectsQuery(scope, serverSDK()), providers: (directory: PathKey | null) => - loadProvidersQuery(scope, directory, serverAPI, directory ? sdkFor(directory) : serverSDK(), protocol), + loadProvidersQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)), path: (directory: PathKey | null) => - loadPathQuery(scope, directory, directory ? sdkFor(directory) : serverSDK(), protocol), - agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent, sdkFor(directory), protocol), - references: (directory: PathKey) => - loadReferencesQuery(scope, directory, serverAPI.reference, sdkFor(directory), protocol), - mcp: (directory: PathKey) => loadMcpQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol), - mcpResources: (directory: PathKey) => - loadMcpResourcesQuery(scope, directory, serverAPI.mcp, sdkFor(directory), protocol), + loadPathQuery(scope, directory, directory === null ? serverSDK() : sdkFor(directory)), + agents: (directory: PathKey) => loadAgentsQuery(scope, directory, sdkFor(directory)), + references: (directory: PathKey) => loadReferencesQuery(scope, directory, sdkFor(directory)), + mcp: (directory: PathKey) => loadMcpQuery(scope, directory, sdkFor(directory)), + mcpResources: (directory: PathKey) => loadMcpResourcesQuery(scope, directory, sdkFor(directory)), lsp: (directory: PathKey) => loadLspQuery(scope, directory, sdkFor(directory)), sessions: (directory: PathKey) => ({ queryKey: [scope, directory, "loadSessions"] as const }), } @@ -224,44 +122,11 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { return sdk } - const session = createServerSession(serverSDK.client, serverSDK.api.session, serverSDK.api.message, { - protocol: serverSDK.protocol, - }) - const queryOptionsApi = makeQueryOptionsApi( - serverSDK.scope, - () => serverSDK.client, - serverSDK.api, - sdkFor, - serverSDK.protocol, - ) + const queryOptionsApi = makeQueryOptionsApi(serverSDK.scope, () => serverSDK.client, sdkFor) const [configQuery, providerQuery, pathQuery] = useQueries(() => ({ queries: [queryOptionsApi.globalConfig(), queryOptionsApi.providers(null), queryOptionsApi.path(null)], })) - const activeSessionsQuery = useQuery(() => - loadActiveSessionsQuery(serverSDK.scope, { - active: async () => { - if ((await serverSDK.protocol) === "v1") { - const statuses = (await serverSDK.client.session.status()).data ?? {} - seedActiveSessionStatuses(session, statuses) - for (const sessionID of Object.keys(statuses)) { - void session.resolve(sessionID).catch(() => undefined) - } - return Object.fromEntries( - Object.entries(statuses).flatMap(([sessionID, status]) => - status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], - ), - ) - } - const active = await serverSDK.api.session.active() - seedActiveSessionStatuses(session, active) - for (const sessionID of Object.keys(active)) { - void session.resolve(sessionID).catch(() => undefined) - } - return active - }, - }), - ) const [globalStore, setGlobalStore] = createStore({ get ready() { @@ -290,10 +155,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { const queryClient = useQueryClient() const homeSessions = createHomeSessionIndexCache(queryClient, ServerConnection.key(serverSDK.server)) - const refreshProviders = () => - queryClient.refetchQueries({ - predicate: (query) => query.queryKey[0] === serverSDK.scope && query.queryKey[2] === "providers", - }) let bootedAt = 0 let bootingRoot = false @@ -322,8 +183,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { queryFn: async () => { await bootstrapGlobal({ serverSDK: serverSDK.client, - serverAPI: serverSDK.api, - protocol: serverSDK.protocol, scope: serverSDK.scope, requestFailedTitle: language.t("common.requestFailed"), translate: language.t, @@ -353,6 +212,8 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { bootstrapInstance, }) + const session = createServerSession(serverSDK.client) + const children = createChildStoreManager({ owner, scope: serverSDK.scope, @@ -363,15 +224,17 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { void bootstrapInstance(directory) }, onMcp: (directory, setStore) => { - void loadCommands(directory, serverSDK.api.command, sdkFor(directory), serverSDK.protocol) - .then((commands) => setStore("command", commands)) - .catch((err) => { - showToast({ - variant: "error", - title: language.t("toast.project.reloadFailed.title", { project: getFilename(directory) }), - description: formatServerError(err, language.t), - }) + void retry(() => + sdkFor(directory) + .command.list() + .then((x) => setStore("command", x.data ?? [])), + ).catch((err) => { + showToast({ + variant: "error", + title: language.t("toast.project.reloadFailed.title", { project: getFilename(directory) }), + description: formatServerError(err, language.t), }) + }) }, onDispose: (directory) => { const key = directoryKey(directory) @@ -416,12 +279,11 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { .fetchQuery({ ...queryOptionsApi.sessions(key), queryFn: () => - serverSDK.protocol - .then((protocol) => - protocol === "v1" - ? loadRootSessionsV1({ client: sdkFor(directory), directory, limit }) - : loadRootSessions({ api: serverSDK.api.session, directory, limit }), - ) + loadRootSessionsWithFallback({ + directory, + limit, + list: (query) => serverSDK.client.session.list(query), + }) .then((x) => { const nonArchived = (x.data ?? []) .filter((s) => !!s?.id) @@ -491,7 +353,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { provider: globalStore.provider, }, sdk, - api: serverSDK.api, store: child[0], setStore: child[1], vcsCache: cache, @@ -499,7 +360,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { translate: language.t, queryClient, session, - protocol: serverSDK.protocol, }) }) @@ -511,41 +371,19 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { return promise } - const indexSession = (info: Parameters[0]) => { - const key = directoryKey(info.directory) - const existing = children.children[key] - if (!existing) return - applyDirectoryEvent({ - event: { type: "session.created", properties: { info } }, - directory: key, - store: existing[0], - setStore: existing[1], - push: queue.push, - retainedLimit: sessionMeta.get(key)?.limit, - sessionContent: false, - permission: session.data.permission, - loadLsp() {}, - }) - } - const unsub = serverSDK.event.listen((e) => { const directory = e.name const key = directoryKey(directory) const event = e.details - const eventType: string = event.type const recent = bootingRoot || Date.now() - bootedAt < 1500 - if (event.current) session.applyV2(event.current) session.apply(event) if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted") { homeSessions.apply(event) } homeSessions.refresh(event.type) - if (eventType === "integration.connection.updated") void refreshProviders() if (directory === "global") { - if (eventType === "server.connected" && activeSessionsQuery.data === undefined && !activeSessionsQuery.isFetching) - void activeSessionsQuery.refetch() applyGlobalEvent({ event, project: globalStore.project, @@ -555,14 +393,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { }, setGlobalProject: setProjects, }) - if ( - eventType === "config.updated" || - eventType === "catalog.updated" || - eventType === "agent.updated" || - eventType === "project.directories.updated" - ) - bootstrap.refetch() - if (eventType === "server.connected" || eventType === "global.disposed") { + if (event.type === "server.connected" || event.type === "global.disposed") { if (recent) return for (const directory of Object.keys(children.children)) { if (!children.active(directory)) continue @@ -572,30 +403,9 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { return } - if (event.current?.type === "session.moved") { - const info = session.get(event.current.data.sessionID) - if (info) indexSession(info) - } - if (event.current?.type === "session.forked") - void session - .resolve(event.current.data.sessionID, { force: true }) - .then(indexSession) - .catch(() => {}) - const existing = children.children[key] if (!existing) return children.mark(key) - if ( - event.current?.type === "session.moved" || - // event.current?.type === "session.archived" || - event.current?.type === "session.forked" || - eventType === "command.updated" || - eventType === "config.updated" || - eventType === "agent.updated" - ) - queue.push(key) - if (eventType === "mcp.status.changed") void queryClient.invalidateQueries(queryOptionsApi.mcp(key)) - if (eventType === "mcp.resources.changed") void queryClient.invalidateQueries(queryOptionsApi.mcpResources(key)) const [store, setStore] = existing applyDirectoryEvent({ event, @@ -683,7 +493,6 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { peek: children.peek, disableMcp: children.disableMcp, queryOptions: queryOptionsApi, - refreshProviders, // bootstrap, updateConfig: updateConfigMutation.mutateAsync, project: projectApi, @@ -693,23 +502,14 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) { toggle: async (directory: string, name: string) => { const key = directoryKey(directory) const sdk = sdkFor(key) - const status = children.child(key, { bootstrap: false })[0].mcp[name]?.status - if (!status) return + const status = children.child(key, { bootstrap: false })[0].mcp[name].status await toggleMcp({ status, connect: async () => { - if ((await serverSDK.protocol) === "v1") { - await sdk.mcp.connect({ name }) - return - } - await serverSDK.api.mcp.connect({ server: name, location: { directory: key } }) + await sdk.mcp.connect({ name }) }, disconnect: async () => { - if ((await serverSDK.protocol) === "v1") { - await sdk.mcp.disconnect({ name }) - return - } - await serverSDK.api.mcp.disconnect({ server: name, location: { directory: key } }) + await sdk.mcp.disconnect({ name }) }, authenticate: async () => { await sdk.mcp.auth.authenticate({ name }) diff --git a/packages/app/src/context/settings.test.ts b/packages/app/src/context/settings.test.ts index 51b35eacbb..ba0161a6cd 100644 --- a/packages/app/src/context/settings.test.ts +++ b/packages/app/src/context/settings.test.ts @@ -1,7 +1,5 @@ import { describe, expect, test } from "bun:test" import { - hasExistingWebState, - initialAgentVisibility, isAppUpgrade, layoutTransitionState, maximumSunsetTimeout, @@ -12,22 +10,6 @@ import { shouldEnableNewLayout, } from "./settings" -describe("agent visibility", () => { - test("shows the picker for existing profiles and hides it for first-time installs", () => { - expect(initialAgentVisibility(undefined, true)).toBe(true) - expect(initialAgentVisibility(undefined, false)).toBe(false) - }) - - test("shows the picker when updating from a recent release", () => { - expect(initialAgentVisibility(undefined, false, "1.18.8")).toBe(true) - }) - - test("preserves the preference after initialization", () => { - expect(initialAgentVisibility(true, true, "1.18.8")).toBeUndefined() - expect(initialAgentVisibility(true, false)).toBeUndefined() - }) -}) - describe("layout transition", () => { test("blank profiles default to the new layout", () => { expect(newLayoutDesignsDefault).toBe(true) @@ -41,12 +23,6 @@ describe("layout transition", () => { expect(layoutTransitionState(true, true, false, false)).toEqual({ available: true, notice: false }) }) - test("classifies web profiles from existing settings or a recorded version", () => { - expect(hasExistingWebState("{}", undefined)).toBe(true) - expect(hasExistingWebState(null, "1.17.19")).toBe(true) - expect(hasExistingWebState(null, undefined)).toBe(false) - }) - test("preserves explicit and default layout preferences", () => { expect(resolveNewLayoutDesigns(false, false, true)).toBe(false) expect(resolveNewLayoutDesigns(false, undefined, false)).toBe(false) diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx index 1a118b6544..c2b5680418 100644 --- a/packages/app/src/context/settings.tsx +++ b/packages/app/src/context/settings.tsx @@ -1,5 +1,5 @@ import { createStore, reconcile } from "solid-js/store" -import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { createEffect, createMemo, createSignal, onCleanup } from "solid-js" import { createSimpleContext } from "@opencode-ai/ui/context" import { persisted } from "@/utils/persist" import { usePlatform } from "@/context/platform" @@ -36,7 +36,6 @@ export interface Settings { mobileTitlebarPosition: "top" | "bottom" newLayoutDesigns?: boolean layoutTransitionEligible?: boolean - agentVisibilityInitialized?: boolean newInterfaceNoticeDismissed?: boolean shouldDisplayTabsToast?: boolean } @@ -90,15 +89,6 @@ export function shouldDisplayTabsToast( return isAppUpgrade(previous, current) || (!previous && existingInstall) } -export function hasExistingWebState(settings: Promise | string | null, previousVersion: string | undefined) { - return settings !== null || previousVersion !== undefined -} - -export function initialAgentVisibility(initialized: boolean | undefined, existing: boolean, previousVersion?: string) { - if (initialized === true) return - return existing || previousVersion !== undefined -} - export function shouldEnableNewLayout(previous: string | undefined, current: string | undefined) { if (!current) return false const currentComparison = compareVersions(current, newLayoutDesignsUpgradeCutoff) @@ -230,7 +220,7 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont gate: false, init: () => { const platform = usePlatform() - const [store, setStore, settingsInit, ready] = persisted("settings.v3", createStore(defaultSettings)) + const [store, setStore, _, ready] = persisted("settings.v3", createStore(defaultSettings)) const [launch, setLaunch, , launchReady] = persisted( "app-version.v1", createStore<{ version?: string }>({ version: undefined }), @@ -277,14 +267,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont ) }) const visible = (preference: () => boolean) => createMemo(() => !newLayoutDesigns() || preference()) - const initializeAgentVisibility = (existing: boolean) => { - const initial = initialAgentVisibility(store.general?.agentVisibilityInitialized, existing, launchState.previous) - if (initial === undefined) return - batch(() => { - setStore("general", "showCustomAgents", initial) - setStore("general", "agentVisibilityInitialized", true) - }) - } if (sunset && !oldInterfaceRetired()) { const timeout = { current: undefined as ReturnType | undefined } @@ -311,13 +293,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont setLaunch("version", platform.version) }) - createEffect(() => { - if (!ready() || !launchState.classified || platform.platform !== "web") return - const existing = hasExistingWebState(settingsInit, launchState.previous) - if (!layoutTransitionClassified()) setStore("general", "layoutTransitionEligible", existing) - initializeAgentVisibility(existing) - }) - createEffect(() => { if (!ready() || !launchState.classified || launchState.migrationApplied) return if (layoutUpgrade() && store.general?.newLayoutDesigns !== true) { @@ -441,7 +416,6 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont if (typeof current === "boolean") return setStore("general", "layoutTransitionEligible", eligible) }, - initializeAgentVisibility, layoutTransitionAvailable: createMemo(() => ready() && layoutTransition().available), newInterfaceNoticeVisible: createMemo(() => ready() && layoutTransition().notice), dismissNewInterfaceNotice() { diff --git a/packages/app/src/context/tab-migration.ts b/packages/app/src/context/tab-migration.ts deleted file mode 100644 index 04c1cb6804..0000000000 --- a/packages/app/src/context/tab-migration.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { ServerConnection } from "./server" -import type { Tab } from "./tabs" - -export function migrateTabs(value: unknown, fallback: ServerConnection.Key): Tab[] { - if (!Array.isArray(value)) return [] - return value.flatMap((tab) => { - if (!tab || typeof tab !== "object") return [] - if ("server" in tab && typeof tab.server !== "string") return [] - const server = ("server" in tab ? tab.server : fallback) as ServerConnection.Key - if (tab.type === "session" && typeof tab.sessionId === "string") { - return [{ type: tab.type, server, sessionId: tab.sessionId }] - } - if ( - tab.type === "draft" && - typeof tab.draftID === "string" && - typeof tab.directory === "string" && - (tab.worktree === undefined || typeof tab.worktree === "string") - ) { - return [{ type: tab.type, server, draftID: tab.draftID, directory: tab.directory, worktree: tab.worktree }] - } - return [] - }) -} diff --git a/packages/app/src/context/tabs.test.ts b/packages/app/src/context/tabs.test.ts index abd62a4dae..b56ea2dfcf 100644 --- a/packages/app/src/context/tabs.test.ts +++ b/packages/app/src/context/tabs.test.ts @@ -3,7 +3,6 @@ import { createRoot, getOwner, onCleanup } from "solid-js" import { createTabMemory } from "./tab-memory" import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" import type { SessionTab, Tab } from "./tabs" -import { migrateTabs } from "./tab-migration" import type { ServerConnection } from "./server" const server = "local\nhttp://localhost:4096" as ServerConnection.Key @@ -12,23 +11,6 @@ function sessionTab(sessionId: string): SessionTab { return { type: "session", server, sessionId } } -describe("tab migration", () => { - test("drops null and malformed persisted tabs", () => { - expect( - migrateTabs([null, sessionTab("a"), { type: "session", server }, { type: "unknown", server }, "invalid"], server), - ).toEqual([sessionTab("a")]) - }) - - test("adds the fallback server to valid legacy tabs", () => { - expect(migrateTabs([{ type: "session", sessionId: "a", dirBase64: "legacy" }], server)).toEqual([sessionTab("a")]) - }) - - test("replaces invalid top-level persisted data", () => { - expect(migrateTabs(null, server)).toEqual([]) - expect(migrateTabs({}, server)).toEqual([]) - }) -}) - describe("tab memory", () => { test("keeps state until its tab is removed", () => { createRoot((dispose) => { diff --git a/packages/app/src/context/tabs.tsx b/packages/app/src/context/tabs.tsx index b6608e8681..75191d54cf 100644 --- a/packages/app/src/context/tabs.tsx +++ b/packages/app/src/context/tabs.tsx @@ -12,7 +12,6 @@ import { sessionHref } from "@/utils/session-route" import { createTabMemory } from "./tab-memory" import { nextTabAfterClose, pushClosedTab, removeClosedTabs, takeClosedTab, type ClosedTab } from "./closed-tabs" import { createDraftPromptSession, type PromptModel } from "./prompt-state" -import { migrateTabs } from "./tab-migration" export type SessionTab = { type: "session" @@ -60,7 +59,13 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ const [store, setStore, _, ready] = persisted( { ...Persist.window("tabs"), - migrate: (value: unknown) => migrateTabs(value, fallback), + migrate: (value: unknown) => { + if (!Array.isArray(value)) return value + return value.map((tab) => { + if (!tab || typeof tab !== "object" || "server" in tab) return tab + return { ...tab, server: fallback } + }) + }, }, createStore([]), ) @@ -101,10 +106,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({ } const removeDraftPersisted = (draftID: string) => { - for (const key of draftPersistedKeys()) { - const target = Persist.draft(draftID, key) - removePersisted(key === "prompt" ? Persist.prompt(target) : target, platform) - } + for (const key of draftPersistedKeys()) removePersisted(Persist.draft(draftID, key), platform) } const removeInfo = (key: string) => { diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx index df575b71f7..d2d616248d 100644 --- a/packages/app/src/context/terminal.tsx +++ b/packages/app/src/context/terminal.tsx @@ -149,7 +149,6 @@ function createWorkspaceTerminalSession( scope: ServerScopeValue, legacySessionID?: string, ) { - const location = { directory: sdk.directory } const legacy = scope === ServerScope.local ? getLegacyTerminalStorageKeys(dir, legacySessionID) : [] const [store, setStore, _, ready] = persisted( @@ -241,63 +240,47 @@ function createWorkspaceTerminalSession( }) onCleanup(unsub) - const update = (pty: Partial & { id: string }) => { + const update = (client: DirectorySDK["client"], pty: Partial & { id: string }) => { const index = store.all.findIndex((x) => x.id === pty.id) const previous = index >= 0 ? store.all[index] : undefined if (index >= 0) { setStore("all", index, (item) => ({ ...item, ...pty })) } - const doUpdate = async () => { - if ((await sdk.protocol) === "v1") { - await sdk.client.pty.update({ - ptyID: pty.id, - title: pty.title, - size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, - }) - } else { - await sdk.api.pty.update({ - ptyID: pty.id, - location, - title: pty.title, - size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, - }) - } - } - doUpdate().catch((error: unknown) => { - if (previous) { - const currentIndex = store.all.findIndex((item) => item.id === pty.id) - if (currentIndex >= 0) setStore("all", currentIndex, previous) - } - console.error("Failed to update terminal", error) - }) + client.pty + .update({ + ptyID: pty.id, + title: pty.title, + size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined, + }) + .catch((error: unknown) => { + if (previous) { + const currentIndex = store.all.findIndex((item) => item.id === pty.id) + if (currentIndex >= 0) setStore("all", currentIndex, previous) + } + console.error("Failed to update terminal", error) + }) } - const clone = async (id: string) => { + const clone = async (client: DirectorySDK["client"], id: string) => { const index = store.all.findIndex((x) => x.id === id) const pty = store.all[index] if (!pty) return - const data = await (async () => { - if ((await sdk.protocol) === "v1") { - return (await sdk.client.pty.create({ title: pty.title })).data - } - return ( - await sdk.api.pty.create({ - location, - title: pty.title, - }) - ).data - })().catch((error: unknown) => { - console.error("Failed to clone terminal", error) - return undefined - }) - if (!data?.id) return + const next = await client.pty + .create({ + title: pty.title, + }) + .catch((error: unknown) => { + console.error("Failed to clone terminal", error) + return undefined + }) + if (!next?.data) return const active = store.active === pty.id batch(() => { setStore("all", index, { - id: data.id, - title: data.title ?? pty.title, + id: next.data.id, + title: next.data.title ?? pty.title, titleNumber: pty.titleNumber, buffer: undefined, cursor: undefined, @@ -306,7 +289,7 @@ function createWorkspaceTerminalSession( cols: undefined, }) if (active) { - setStore("active", data.id) + setStore("active", next.data.id) } }) } @@ -325,22 +308,17 @@ function createWorkspaceTerminalSession( const nextNumber = pickNextTerminalNumber() const focusRequest = options?.focus ? requestFocus(undefined, true) : undefined - const doCreate = async () => { - if ((await sdk.protocol) === "v1") { - return (await sdk.client.pty.create({ title: defaultTitle(nextNumber) })).data - } - return (await sdk.api.pty.create({ location, title: defaultTitle(nextNumber) })).data - } - doCreate() - .then((data) => { - const id = data?.id + sdk.client.pty + .create({ title: defaultTitle(nextNumber) }) + .then((pty: { data?: { id?: string; title?: string } }) => { + const id = pty.data?.id if (!id) { if (focusRequest !== undefined) cancelFocus(focusRequest) return } const newTerminal = { id, - title: data?.title ?? defaultTitle(nextNumber), + title: pty.data?.title ?? defaultTitle(nextNumber), titleNumber: nextNumber, } batch(() => { @@ -357,7 +335,7 @@ function createWorkspaceTerminalSession( }) }, update(pty: Partial & { id: string }) { - update(pty) + update(sdk.client, pty) }, trim(id: string) { const index = store.all.findIndex((x) => x.id === id) @@ -372,9 +350,10 @@ function createWorkspaceTerminalSession( }) }, async clone(id: string) { - await clone(id) + await clone(sdk.client, id) }, bind() { + const client = sdk.client return { trim(id: string) { const index = store.all.findIndex((x) => x.id === id) @@ -382,10 +361,10 @@ function createWorkspaceTerminalSession( setStore("all", index, (pty) => trimTerminal(pty)) }, update(pty: Partial & { id: string }) { - update(pty) + update(client, pty) }, async clone(id: string) { - await clone(id) + await clone(client, id) }, } }, @@ -433,11 +412,7 @@ function createWorkspaceTerminalSession( }) } - const removePromise = - (await sdk.protocol) === "v1" - ? sdk.client.pty.remove({ ptyID: id }) - : sdk.api.pty.remove({ ptyID: id, location }) - await removePromise.catch((error: unknown) => { + await sdk.client.pty.remove({ ptyID: id }).catch((error: unknown) => { console.error("Failed to close terminal", error) }) }, diff --git a/packages/app/src/entry.tsx b/packages/app/src/entry.tsx index 9b71d6567e..c41c1bb4b6 100644 --- a/packages/app/src/entry.tsx +++ b/packages/app/src/entry.tsx @@ -4,9 +4,9 @@ import * as Sentry from "@sentry/solid" import { render } from "solid-js/web" import { AppBaseProviders, AppInterface } from "@/app" import { type Platform, PlatformProvider } from "@/context/platform" -import { createBrowserDraftStore } from "@/utils/draft-store" import { dict as en } from "@/i18n/en" import { dict as zh } from "@/i18n/zh" +import { handleNotificationClick } from "@/utils/notification-click" import { authFromToken } from "@/utils/server" import pkg from "../package.json" import { ServerConnection } from "./context/server" @@ -54,7 +54,7 @@ const setStorage = (key: string, value: string | null) => { const readDefaultServerUrl = () => getStorage(DEFAULT_SERVER_URL_KEY) const writeDefaultServerUrl = (url: string | null) => setStorage(DEFAULT_SERVER_URL_KEY, url) -const notify: Platform["notify"] = async (title, description, onClick) => { +const notify: Platform["notify"] = async (title, description, href) => { if (!("Notification" in window)) return const permission = @@ -73,17 +73,21 @@ const notify: Platform["notify"] = async (title, description, onClick) => { }) notification.onclick = () => { - window.focus() - onClick?.() + handleNotificationClick(href) notification.close() } } -const openExternal: Platform["openExternal"] = (value) => { - if (!URL.canParse(value)) return - const url = new URL(value) - if (url.protocol !== "http:" && url.protocol !== "https:" && url.protocol !== "mailto:") return - window.open(url.href, "_blank", "noopener,noreferrer") +const openLink: Platform["openLink"] = (url) => { + window.open(url, "_blank") +} + +const back: Platform["back"] = () => { + window.history.back() +} + +const forward: Platform["forward"] = () => { + window.history.forward() } const restart: Platform["restart"] = async () => { @@ -117,9 +121,10 @@ const clearAuthToken = () => { const platform: Platform = { platform: "web", - draftStore: createBrowserDraftStore(), version: pkg.version, - openExternal, + openLink, + back, + forward, restart, notify, getDefaultServer: async () => { diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index 60ad01c867..982c26ca97 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -2,7 +2,7 @@ import { useServerSync } from "@/context/server-sync" import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" import { Iterable, pipe } from "effect" -import { createEffect, createMemo, type Accessor } from "solid-js" +import type { Accessor } from "solid-js" import { selectProviderCatalog } from "./provider-catalog" export const popularProviders = [ @@ -17,14 +17,14 @@ export const popularProviders = [ ] const popularProviderSet = new Set(popularProviders) -export function useProviders(directory: Accessor) { +export function useProviders(directory?: Accessor) { const serverSync = useServerSync() const params = useParams() const dir = () => (directory ? directory() : decode64(params.dir)) const providers = () => { const value = dir() const projectStore = value ? serverSync().child(value)[0] : undefined - if (value) + if (directory) return selectProviderCatalog({ explicit: true, directory: value, @@ -37,7 +37,6 @@ export function useProviders(directory: Accessor) { global: serverSync().data.provider, }) } - return { all: () => providers().all, default: () => providers().default, @@ -59,7 +58,7 @@ export function useProviders(directory: Accessor) { }, paid: () => { const connected = new Set(providers().connected) - const paid = [ + return [ ...Iterable.filter( providers().all, ([id]) => @@ -67,7 +66,6 @@ export function useProviders(directory: Accessor) { (id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)), ), ] - return paid }, } } diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index 89fa394637..320738a6b0 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -277,7 +277,6 @@ export const dict = { "prompt.action.send": "إرسال", "prompt.action.stop": "توقف", "prompt.toast.pasteUnsupported.title": "مرفق غير مدعوم", - "prompt.toast.attachmentDuplicate.title": "تم تحميل هذا الملف بالفعل", "prompt.toast.pasteUnsupported.description": "يمكن إرفاق الصور أو ملفات PDF أو الملفات النصية فقط هنا.", "prompt.toast.modelAgentRequired.title": "حدد وكيلاً ونموذجاً", "prompt.toast.modelAgentRequired.description": "اختر وكيلاً ونموذجاً قبل إرسال الموجه.", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index f6e5d8df9d..9d6765ec94 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -277,7 +277,6 @@ export const dict = { "prompt.action.send": "Enviar", "prompt.action.stop": "Parar", "prompt.toast.pasteUnsupported.title": "Anexo não suportado", - "prompt.toast.attachmentDuplicate.title": "Este arquivo já foi enviado", "prompt.toast.pasteUnsupported.description": "Apenas imagens, PDFs ou arquivos de texto podem ser anexados aqui.", "prompt.toast.modelAgentRequired.title": "Selecione um agente e modelo", "prompt.toast.modelAgentRequired.description": "Escolha um agente e modelo antes de enviar um prompt.", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index 3343df7a2b..915313c3e2 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -298,7 +298,6 @@ export const dict = { "prompt.action.stop": "Zaustavi", "prompt.toast.pasteUnsupported.title": "Nepodržan prilog", - "prompt.toast.attachmentDuplicate.title": "Ova datoteka je već učitana", "prompt.toast.pasteUnsupported.description": "Ovdje se mogu priložiti samo slike, PDF-ovi ili tekstualne datoteke.", "prompt.toast.modelAgentRequired.title": "Odaberi agenta i model", "prompt.toast.modelAgentRequired.description": "Odaberi agenta i model prije slanja upita.", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index 52bfb642c3..22fcdf39ce 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -296,7 +296,6 @@ export const dict = { "prompt.action.stop": "Stop", "prompt.toast.pasteUnsupported.title": "Ikke understøttet vedhæftning", - "prompt.toast.attachmentDuplicate.title": "Denne fil er allerede uploadet", "prompt.toast.pasteUnsupported.description": "Kun billeder, PDF'er eller tekstfiler kan vedhæftes her.", "prompt.toast.modelAgentRequired.title": "Vælg en agent og model", "prompt.toast.modelAgentRequired.description": "Vælg en agent og model før du sender en forespørgsel.", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index 8619546079..bc2ac9be38 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -282,7 +282,6 @@ export const dict = { "prompt.action.send": "Senden", "prompt.action.stop": "Stopp", "prompt.toast.pasteUnsupported.title": "Nicht unterstützter Anhang", - "prompt.toast.attachmentDuplicate.title": "Diese Datei wurde bereits hochgeladen", "prompt.toast.pasteUnsupported.description": "Hier können nur Bilder, PDFs oder Textdateien angehängt werden.", "prompt.toast.modelAgentRequired.title": "Wählen Sie einen Agenten und ein Modell", "prompt.toast.modelAgentRequired.description": diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 76623f4468..f03147f151 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -301,7 +301,6 @@ export const dict = { "prompt.toast.pasteUnsupported.title": "Unsupported attachment", "prompt.toast.pasteUnsupported.description": "Only images, PDFs, or text files can be attached here.", - "prompt.toast.attachmentDuplicate.title": "This file has already been uploaded", "prompt.toast.modelAgentRequired.title": "Select an agent and model", "prompt.toast.modelAgentRequired.description": "Choose an agent and model before sending a prompt.", "prompt.toast.worktreeCreateFailed.title": "Failed to create worktree", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 043c997c04..00dca6704b 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -297,7 +297,6 @@ export const dict = { "prompt.action.stop": "Detener", "prompt.toast.pasteUnsupported.title": "Adjunto no compatible", - "prompt.toast.attachmentDuplicate.title": "Este archivo ya se ha subido", "prompt.toast.pasteUnsupported.description": "Solo se pueden adjuntar imágenes, PDFs o archivos de texto aquí.", "prompt.toast.modelAgentRequired.title": "Selecciona un agente y modelo", "prompt.toast.modelAgentRequired.description": "Elige un agente y modelo antes de enviar un prompt.", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index fc5c5bbb34..d1487ff61e 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -277,7 +277,6 @@ export const dict = { "prompt.action.send": "Envoyer", "prompt.action.stop": "Arrêter", "prompt.toast.pasteUnsupported.title": "Pièce jointe non prise en charge", - "prompt.toast.attachmentDuplicate.title": "Ce fichier a déjà été téléversé", "prompt.toast.pasteUnsupported.description": "Seules les images, les PDF ou les fichiers texte peuvent être joints ici.", "prompt.toast.modelAgentRequired.title": "Sélectionnez un agent et un modèle", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index 56c63e0e70..6a3b1ebcb0 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -276,7 +276,6 @@ export const dict = { "prompt.action.send": "送信", "prompt.action.stop": "停止", "prompt.toast.pasteUnsupported.title": "サポートされていない添付ファイル", - "prompt.toast.attachmentDuplicate.title": "このファイルはすでにアップロードされています", "prompt.toast.pasteUnsupported.description": "画像、PDF、またはテキストファイルのみ添付できます。", "prompt.toast.modelAgentRequired.title": "エージェントとモデルを選択", "prompt.toast.modelAgentRequired.description": "プロンプトを送信する前にエージェントとモデルを選択してください。", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 02d9a5e0e1..3c262ce3b9 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -264,7 +264,6 @@ export const dict = { "prompt.action.send": "전송", "prompt.action.stop": "중지", "prompt.toast.pasteUnsupported.title": "지원되지 않는 첨부 파일", - "prompt.toast.attachmentDuplicate.title": "이 파일은 이미 업로드되었습니다", "prompt.toast.pasteUnsupported.description": "이미지, PDF 또는 텍스트 파일만 첨부할 수 있습니다.", "prompt.toast.modelAgentRequired.title": "에이전트 및 모델 선택", "prompt.toast.modelAgentRequired.description": "프롬프트를 보내기 전에 에이전트와 모델을 선택하세요.", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index 959e4c68cb..a4e8efb79d 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -288,7 +288,6 @@ export const dict = { "prompt.action.stop": "Stopp", "prompt.toast.pasteUnsupported.title": "Ikke støttet vedlegg", - "prompt.toast.attachmentDuplicate.title": "Denne filen er allerede lastet opp", "prompt.toast.pasteUnsupported.description": "Kun bilder, PDF-er eller tekstfiler kan legges ved her.", "prompt.toast.modelAgentRequired.title": "Velg en agent og modell", "prompt.toast.modelAgentRequired.description": "Velg en agent og modell før du sender en forespørsel.", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index 7517f16b81..f2e32ffeb8 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -278,7 +278,6 @@ export const dict = { "prompt.action.send": "Wyślij", "prompt.action.stop": "Zatrzymaj", "prompt.toast.pasteUnsupported.title": "Nieobsługiwany załącznik", - "prompt.toast.attachmentDuplicate.title": "Ten plik został już przesłany", "prompt.toast.pasteUnsupported.description": "Można tutaj załączać tylko obrazy, pliki PDF lub pliki tekstowe.", "prompt.toast.modelAgentRequired.title": "Wybierz agenta i model", "prompt.toast.modelAgentRequired.description": "Wybierz agenta i model przed wysłaniem zapytania.", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 17d6ba58a9..fa6fb04d04 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -297,7 +297,6 @@ export const dict = { "prompt.action.stop": "Остановить", "prompt.toast.pasteUnsupported.title": "Неподдерживаемое вложение", - "prompt.toast.attachmentDuplicate.title": "Этот файл уже загружен", "prompt.toast.pasteUnsupported.description": "Здесь можно прикрепить только изображения, PDF или текстовые файлы.", "prompt.toast.modelAgentRequired.title": "Выберите агента и модель", "prompt.toast.modelAgentRequired.description": "Выберите агента и модель перед отправкой запроса.", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 2c0b7d081c..0ae67ee34e 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -297,7 +297,6 @@ export const dict = { "prompt.action.stop": "หยุด", "prompt.toast.pasteUnsupported.title": "ไฟล์แนบที่ไม่รองรับ", - "prompt.toast.attachmentDuplicate.title": "ไฟล์นี้ถูกอัปโหลดแล้ว", "prompt.toast.pasteUnsupported.description": "แนบได้เฉพาะรูปภาพ, PDF หรือไฟล์ข้อความเท่านั้น", "prompt.toast.modelAgentRequired.title": "เลือกเอเจนต์และโมเดล", "prompt.toast.modelAgentRequired.description": "เลือกเอเจนต์และโมเดลก่อนส่งพร้อมท์", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index 7c123c69aa..d401519af9 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -302,7 +302,6 @@ export const dict = { "prompt.action.stop": "Durdur", "prompt.toast.pasteUnsupported.title": "Desteklenmeyen ek", - "prompt.toast.attachmentDuplicate.title": "Bu dosya zaten yüklendi", "prompt.toast.pasteUnsupported.description": "Buraya yalnızca resimler, PDF'ler veya metin dosyaları eklenebilir.", "prompt.toast.modelAgentRequired.title": "Bir ajan ve model seçin", "prompt.toast.modelAgentRequired.description": "Komut göndermeden önce bir ajan ve model seçin.", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index d8c9ae8e43..c949f41de2 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -299,7 +299,6 @@ export const dict = { "prompt.action.stop": "Зупинити", "prompt.toast.pasteUnsupported.title": "Непідтримуване вкладення", - "prompt.toast.attachmentDuplicate.title": "Цей файл уже завантажено", "prompt.toast.pasteUnsupported.description": "Сюди можна прикріплювати лише зображення, PDF або текстові файли.", "prompt.toast.modelAgentRequired.title": "Виберіть агента та модель", "prompt.toast.modelAgentRequired.description": "Виберіть агента та модель перед надсиланням запиту.", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 724bcc1175..82d73ac3a6 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -316,7 +316,6 @@ export const dict = { "prompt.action.send": "发送", "prompt.action.stop": "停止", "prompt.toast.pasteUnsupported.title": "不支持的附件", - "prompt.toast.attachmentDuplicate.title": "此文件已上传", "prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。", "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", "prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 6f66822165..fbe80d95ee 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -297,7 +297,6 @@ export const dict = { "prompt.action.stop": "停止", "prompt.toast.pasteUnsupported.title": "不支援的附件", - "prompt.toast.attachmentDuplicate.title": "此檔案已上傳", "prompt.toast.pasteUnsupported.description": "此處僅能附加圖片、PDF 或文字檔案。", "prompt.toast.modelAgentRequired.title": "請選擇代理程式和模型", "prompt.toast.modelAgentRequired.description": "傳送提示前請先選擇代理程式和模型。", diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index 8d36086d9f..d18e05fc9c 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -27,4 +27,4 @@ export { type WslServersState, } from "./wsl/types" export { ServerConnection } from "./context/server" -export { createDraftStore, type DraftStore } from "./utils/draft-store" +export { handleNotificationClick } from "./utils/notification-click" diff --git a/packages/app/src/pages/directory-layout.tsx b/packages/app/src/pages/directory-layout.tsx index a0b3576396..77c120e57b 100644 --- a/packages/app/src/pages/directory-layout.tsx +++ b/packages/app/src/pages/directory-layout.tsx @@ -63,7 +63,6 @@ export function DirectoryDataProvider( navigate(href(sessionID))} onSessionHref={href} > diff --git a/packages/app/src/pages/error.tsx b/packages/app/src/pages/error.tsx index 2fc6095412..86a997b39d 100644 --- a/packages/app/src/pages/error.tsx +++ b/packages/app/src/pages/error.tsx @@ -354,7 +354,7 @@ export const ErrorPage: Component = (props) => { + +
+ ) +} + +function HomeServerRow(props: { + server: ServerConnection.Any + selected: boolean + collapsed: boolean + health: ServerHealth | undefined + controller: ReturnType + focusServer: (server: ServerConnection.Any) => void + chooseProject: (server: ServerConnection.Any) => void + openEdit: (server: ServerConnection.Http) => void + toggleCollapsed: () => void + language: ReturnType +}) { + const global = useGlobal() + const [state, setState] = createStore({ menuOpen: false }) + const healthy = () => !!props.health?.healthy + const canToggle = () => healthy() && global.ensureServerCtx(props.server).projects.list().length > 0 + return ( +
+ +
+ setState("menuOpen", open)} + /> + + } + aria-label={props.language.t("home.project.add")} + disabled={props.health?.healthy === false} + onClick={() => props.chooseProject(props.server)} + /> + +
+
+ ) +} + +function HomeProjectList(props: { + server: ServerConnection.Any + projects: LocalProject[] + selected: HomeProjectSelection + selectProject: (server: ServerConnection.Any, directory: string) => void + openNewSession: (server: ServerConnection.Any, directory: string) => void + editProject: (server: ServerConnection.Any, project: LocalProject) => void + closeProject: (server: ServerConnection.Any, directory: string) => void + clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void + unseenCount: (server: ServerConnection.Any, project: LocalProject) => number + language: ReturnType +}) { + return ( +
+ + {(project) => ( + + )} + +
+ ) +} + +function HomeProjectEmpty(props: { + server: ServerConnection.Any + recentlyClosed: LocalProject[] + homedir: string + chooseProject: (server: ServerConnection.Any) => void + openRecentProject: (server: ServerConnection.Any, directory: string) => void + language: ReturnType +}) { + const global = useGlobal() + const unreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false + return ( +
+ + 0}> +
+
{props.language.t("home.recentlyClosed")}
+
+ + {(project) => ( + + )} + +
+
+ ) +} + +function HomeRecentlyClosedRow(props: { + project: LocalProject + server: ServerConnection.Any + homedir: string + openRecentProject: (server: ServerConnection.Any, directory: string) => void + language: ReturnType +}) { + const global = useGlobal() + const unreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false + const path = () => { + const home = props.homedir + const worktree = props.project.worktree + if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}` + return worktree + } + return ( + + + + ) +} + +function HomeProjectRow(props: { + project: LocalProject + server: ServerConnection.Any + selected: boolean + unseenCount: number + selectProject: (server: ServerConnection.Any, directory: string) => void + openNewSession: (server: ServerConnection.Any, directory: string) => void + editProject: (server: ServerConnection.Any, project: LocalProject) => void + closeProject: (server: ServerConnection.Any, directory: string) => void + clearNotifications: (server: ServerConnection.Any, project: LocalProject) => void + language: ReturnType +}) { + const global = useGlobal() + const platform = usePlatform() + const serverUnreachable = () => global.servers.health[ServerConnection.key(props.server)]?.healthy === false + const [state, setState] = createStore({ menuOpen: false }) + const canRevealInFileManager = () => + platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(props.server) + const fileManagerActionLabel = () => + props.language.t( + fileManagerApp(platform.platform === "desktop" ? (platform.os ?? "unknown") : "unknown").actionLabel, + ) + const revealInFileManager = () => { + if (!platform.openPath) return + platform.openPath(props.project.worktree).catch((err: unknown) => + showToast({ + title: props.language.t("common.requestFailed"), + description: errorMessage(err, props.language.t("common.requestFailed")), + }), + ) + } + return ( +
+ +
+ setState("menuOpen", open)} + > + } + aria-label={props.language.t("common.moreOptions")} + /> + + + props.openNewSession(props.server, props.project.worktree)}> + {props.language.t("command.session.new")} + + props.editProject(props.server, props.project)}> + {props.language.t("dialog.project.edit.title")} + + + {fileManagerActionLabel()} + + props.clearNotifications(props.server, props.project)} + > + {props.language.t("sidebar.project.clearNotifications")} + + + props.closeProject(props.server, props.project.worktree)}> + {props.language.t("common.close")} + + + + + } + aria-label={props.language.t("command.session.new")} + onClick={() => props.openNewSession(props.server, props.project.worktree)} + /> +
+
+ ) +} + +function HomeProjectAvatar(props: { project: LocalProject; outline?: boolean }) { + const name = createMemo(() => displayName(props.project)) + return ( + + ) +} + +function HomeSessionLeading(props: { + project: LocalProject + session: Session + server: ServerConnection.Key + revealProjectOnHover: boolean +}) { + const tabs = useTabs() + const hasOpenTab = createMemo(() => sessionHasOpenTab(tabs.store, props.server, props.session)) + return ( +
+ + + +
+ ) +} + +function HomeSessionSearch(props: { + value: string + placeholder: string + open: boolean + loading: boolean + results: HomeSessionRecord[] + showProjectName: boolean + server: ServerConnection.Key + noResultsLabel: string + bindFocus: (focus: () => void) => void + onInput: (value: string) => void + onFocus: () => void + onClose: () => void + onSelect: (session: Session, options?: OpenSessionOptions) => void +}) { + const language = useLanguage() + const [store, setStore] = createStore({ active: "" }) + let root: HTMLDivElement | undefined + let input: HTMLInputElement | undefined + let listRef: HTMLDivElement | undefined + + const focusInput = () => { + input?.focus() + props.onFocus() + } + + onMount(() => { + props.bindFocus(focusInput) + }) + + const syncActive = (results: HomeSessionRecord[]) => { + if (results.length === 0) { + setStore("active", "") + return + } + if (!results.some((record) => homeSessionSearchKey(record) === store.active)) { + setStore("active", homeSessionSearchKey(results[0])) + } + } + + createEffect(() => syncActive(props.results)) + + createEffect( + on( + () => props.value, + () => syncActive(props.results), + ), + ) + + const scrollActiveIntoView = () => { + const key = store.active + if (!key || !listRef) return + const element = listRef.querySelector(`[data-key="${key}"]`) + element?.scrollIntoView({ block: "nearest" }) + } + + const moveActive = (delta: number) => { + const results = props.results + if (results.length === 0) return + const index = results.findIndex((record) => homeSessionSearchKey(record) === store.active) + const start = index === -1 ? 0 : index + const next = (start + delta + results.length) % results.length + setStore("active", homeSessionSearchKey(results[next])) + scrollActiveIntoView() + } + + const selectActive = () => { + const record = props.results.find((item) => homeSessionSearchKey(item) === store.active) + if (!record) return + props.onSelect(record.session) + } + + onCleanup( + makeEventListener(document, "pointerdown", (event) => { + if (!props.open) return + const target = event.target + if (!(target instanceof Node)) return + if (root?.contains(target)) return + props.onClose() + }), + ) + + return ( +
+
+ +
+
+
+ + +
+ } + > + 0} + fallback={ +

+ {props.noResultsLabel} +

+ } + > +
+

+ {language.t("home.sessions.search.sessions")} +

+ (listRef = el)}> +
+ + {(record) => ( + setStore("active", homeSessionSearchKey(record))} + onSelect={(session, options) => props.onSelect(session, options)} + /> + )} + +
+
+
+
+ +
+
+
+ + +
+
+ ) +} + +function HomeSessionSearchResultRow(props: { + record: HomeSessionRecord + showProjectName: boolean + server: ServerConnection.Key + selected: boolean + onHighlight: () => void + onSelect: (session: Session, options?: OpenSessionOptions) => void +}) { + const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.showProjectName && props.record.projectName + + const key = () => homeSessionSearchKey(props.record) + + return ( + + ) +} + +function HomeSessionGroupHeader(props: { + title: string + titleOpacity: number + ref: ComponentProps<"div">["ref"] + elevated?: boolean +}) { + return ( +
+ +
+ ) +} + +function HomeSessionRow(props: { + record: HomeSessionRecord + showProjectName: boolean + server: ServerConnection.Key + openSession: (session: Session, options?: OpenSessionOptions) => void + archiveSession: (session: Session) => Promise +}) { + const language = useLanguage() + const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) + const showProjectName = () => props.showProjectName && props.record.projectName + + return ( +
+ + +
+ + } + aria-label={language.t("common.archive")} + onClick={(event) => { + event.preventDefault() + event.stopPropagation() + void props.archiveSession(props.record.session) + }} + /> + +
+
+
+ ) +} + +function HomeSessionsEmpty(props: { onNewSession?: () => void }) { + const language = useLanguage() + return ( +
+
+ {language.t("home.sessions.empty")} +
+

+ {language.t("home.sessions.empty.description")} +

+ + {(onNewSession) => ( + + {language.t("command.session.new")} + + )} + +
+ ) +} + +function HomeSessionSkeleton(props: { label: string }) { + return ( +
+
+ +
+ + ) +} + +function groupSessions(records: HomeSessionRecord[], language: ReturnType): HomeSessionGroup[] { + const now = DateTime.local() + const yesterday = now.minus({ days: 1 }) + const todaySessions = records.filter((record) => + DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(now, "day"), + ) + const yesterdaySessions = records.filter((record) => + DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(yesterday, "day"), + ) + const olderSessions = records.filter((record) => { + const time = DateTime.fromMillis(record.session.time.updated ?? record.session.time.created) + return !time.hasSame(now, "day") && !time.hasSame(yesterday, "day") + }) + const olderTitle = + todaySessions.length === 0 && yesterdaySessions.length === 0 + ? language.t("sidebar.project.recentSessions") + : language.t("home.sessions.group.older") + + return [ + { id: "today" as const, title: language.t("home.sessions.group.today"), sessions: todaySessions }, + { id: "yesterday" as const, title: language.t("home.sessions.group.yesterday"), sessions: yesterdaySessions }, + { id: "older" as const, title: olderTitle, sessions: olderSessions }, + ].filter((group) => group.sessions.length > 0) +} + +export function LegacyHome() { + const sync = useServerSync() + const platform = usePlatform() + const pickDirectory = useDirectoryPicker() + const dialog = useDialog() + const navigate = useNavigate() + const global = useGlobal() + const server = useServer() + const language = useLanguage() + const homedir = createMemo(() => sync().data.path.home) + const serverUnreachable = createMemo(() => global.servers.health[server.key]?.healthy === false) + const recent = createMemo(() => { + return sync() + .data.project.slice() + .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) + .slice(0, 5) + }) + + const serverDotClass = createMemo(() => { + const healthy = global.servers.health[server.key]?.healthy + if (healthy === true) return "bg-icon-success-base" + if (healthy === false) return "bg-icon-critical-base" + return "bg-border-weak-base" + }) + + function openProject(server: ServerConnection.Any, directory: string) { + const serverCtx = global.ensureServerCtx(server) + serverCtx.projects.open(directory) + serverCtx.projects.touch(directory) + navigate(`/${base64Encode(directory)}`) + } + + function chooseProject() { + if (serverUnreachable()) return + const s = server.current + if (!s) return + + const resolve = (result: string | string[] | null) => { + if (Array.isArray(result)) { + for (const directory of result) { + openProject(s, directory) + } + } else if (result) { + openProject(s, result) + } + } + + pickDirectory({ + server: s, + title: language.t("command.project.open"), + multiple: true, + onSelect: resolve, + }) + } + + return ( +
+ + + + 0}> +
+
+
{language.t("home.recentProjects")}
+ +
+
    + + {(project) => ( + + )} + +
+
+
+ +
+
{language.t("common.loading")}
+ +
+
+ +
+ +
+
{language.t("home.empty.title")}
+
{language.t("home.empty.description")}
+
+ +
+
+
+
+ ) +} diff --git a/packages/app/src/pages/home/home-controller.ts b/packages/app/src/pages/home/home-controller.ts deleted file mode 100644 index 5e7e50cfc4..0000000000 --- a/packages/app/src/pages/home/home-controller.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { useGlobal } from "@/context/global" -import { type HomeProjectSelection, useLayout } from "@/context/layout" -import { ServerConnection, useServer } from "@/context/server" -import { useServerSync } from "@/context/server-sync" -import { useTabs } from "@/context/tabs" -import { toggleHomeProjectSelection } from "@/pages/layout/helpers" -import { createEffect, createMemo } from "solid-js" - -export function createHomeController() { - const sync = useServerSync() - const layout = useLayout() - const server = useServer() - const global = useGlobal() - const tabs = useTabs() - const selection = layout.home.selection - const focusedServer = createMemo( - () => global.servers.list().find((conn) => ServerConnection.key(conn) === selection().server) ?? server.current, - ) - const focusedServerCtx = createMemo(() => { - const conn = focusedServer() - if (!conn) return undefined - return global.ensureServerCtx(conn) - }) - const focusedSync = () => focusedServerCtx()?.sync ?? sync() - const projects = createMemo(() => focusedServerCtx()?.projects.list() ?? layout.projects.list()) - const recentlyClosed = createMemo( - () => focusedServerCtx()?.projects.recentlyClosed() ?? layout.projects.recentlyClosed(), - ) - const homedir = createMemo(() => focusedSync().data.path.home ?? "") - const selectedProject = createMemo(() => projects().find((project) => project.worktree === selection().directory)) - const newSessionProject = createMemo( - () => - selectedProject() ?? - projects().find((project) => project.worktree === focusedServerCtx()?.projects.last()) ?? - projects()[0], - ) - - createEffect(() => { - const list = global.servers.list() - if (list.some((conn) => ServerConnection.key(conn) === selection().server)) return - const conn = list.find((conn) => ServerConnection.key(conn) === server.key) ?? list[0] - if (conn) setSelection({ server: ServerConnection.key(conn) }) - }) - - function setSelection(next: HomeProjectSelection) { - layout.home.setSelection(next) - } - - function openProjectNewSession(conn: ServerConnection.Any, directory: string) { - const ctx = global.ensureServerCtx(conn) - ctx.projects.open(directory) - ctx.projects.touch(directory) - void tabs.newDraft({ server: ServerConnection.key(conn), directory }) - } - - return { - selection: { - value: selection, - set: setSelection, - focusServer: (conn: ServerConnection.Any) => setSelection({ server: ServerConnection.key(conn) }), - }, - server: { - list: global.servers.list, - health: (conn: ServerConnection.Any) => global.servers.health[ServerConnection.key(conn)], - context: (conn: ServerConnection.Any) => global.ensureServerCtx(conn), - focused: focusedServer, - focusedContext: focusedServerCtx, - focusedSync, - }, - project: { - list: projects, - recentlyClosed, - homedir, - selected: selectedProject, - newSession: newSessionProject, - forServer: (conn: ServerConnection.Any) => global.ensureServerCtx(conn).projects.list(), - select: (conn: ServerConnection.Any, directory: string) => { - const key = ServerConnection.key(conn) - if (global.servers.health[key]?.healthy === false) return - if ( - !global - .ensureServerCtx(conn) - .projects.list() - .some((project) => project.worktree === directory) - ) - return - setSelection(toggleHomeProjectSelection(selection(), key, directory)) - }, - add: (conn: ServerConnection.Any, directories: string[]) => { - const directory = directories[0] - if (!directory) return - const ctx = global.ensureServerCtx(conn) - directories.forEach((item) => ctx.projects.open(item)) - ctx.projects.touch(directory) - setSelection({ server: ServerConnection.key(conn), directory }) - }, - openNewSession: () => { - const conn = focusedServer() - const project = newSessionProject() - if (!conn || !project) return - openProjectNewSession(conn, project.worktree) - }, - openProjectNewSession, - }, - } -} - -export type HomeController = ReturnType diff --git a/packages/app/src/pages/home/home-projects-controller.tsx b/packages/app/src/pages/home/home-projects-controller.tsx deleted file mode 100644 index 18c58bd290..0000000000 --- a/packages/app/src/pages/home/home-projects-controller.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { useDirectoryPicker } from "@/components/directory-picker" -import { useServerManagementController } from "@/components/dialog-select-server" -import { useSettingsCommand } from "@/components/settings-dialog" -import { DialogServerV2 } from "@/components/settings-v2/dialog-server-v2" -import { type LocalProject } from "@/context/layout" -import { useLanguage } from "@/context/language" -import { useNotification } from "@/context/notification" -import { usePlatform } from "@/context/platform" -import { ServerConnection } from "@/context/server" -import { closeHomeProject, errorMessage, homeProjectDirectories } from "@/pages/layout/helpers" -import { Persist, persisted } from "@/utils/persist" -import { showToast } from "@/utils/toast" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { createResource } from "solid-js" -import { createStore } from "solid-js/store" -import type { HomeController } from "./home-controller" - -export function createHomeProjectsController(home: HomeController) { - const platform = usePlatform() - const pickDirectory = useDirectoryPicker() - const dialog = useDialog() - const language = useLanguage() - const notification = useNotification() - const openSettings = useSettingsCommand() - const serverManagement = useServerManagementController({ navigateOnAdd: false }) - const [_state, setState, _, ready] = persisted( - Persist.global("home.servers", ["home.servers.v1"]), - createStore({ collapsed: {} as Record }), - ) - const [state] = createResource( - () => ready.promise ?? Promise.resolve(), - (promise) => promise.then(() => _state), - { initialValue: _state }, - ) - function directories(project: LocalProject) { - return [project.worktree, ...(project.sandboxes ?? [])] - } - - function canRevealProject(conn: ServerConnection.Any) { - return platform.platform === "desktop" && !!platform.openPath && ServerConnection.local(conn) - } - - return { - copy: { - language, - }, - selection: { - value: home.selection.value, - }, - server: { - list: home.server.list, - health: home.server.health, - projects: home.project.forServer, - collapsed: (conn: ServerConnection.Any) => state().collapsed[ServerConnection.key(conn)] ?? false, - toggleCollapsed: (conn: ServerConnection.Any) => { - const key = ServerConnection.key(conn) - setState("collapsed", key, !state().collapsed[key]) - }, - canDefault: serverManagement.canDefault, - defaultKey: serverManagement.defaultKey, - setDefault: (conn: ServerConnection.Any | undefined) => - serverManagement.setDefault(conn ? ServerConnection.key(conn) : null), - remove: (conn: ServerConnection.Any) => serverManagement.handleRemove(ServerConnection.key(conn)), - edit: (conn: ServerConnection.Http) => dialog.show(() => ), - focus: home.selection.focusServer, - }, - project: { - list: home.project.list, - recentlyClosed: home.project.recentlyClosed, - homedir: home.project.homedir, - select: home.project.select, - add: home.project.add, - openNewSession: home.project.openProjectNewSession, - edit: (conn: ServerConnection.Any, project: LocalProject) => { - void import("@/components/dialog-edit-project-v2").then(({ DialogEditProjectV2 }) => { - void dialog.show(() => ) - }) - }, - unseenCount: (conn: ServerConnection.Any, project: LocalProject) => { - const state = notification.ensureServerState(ServerConnection.key(conn)) - return directories(project).reduce((total, directory) => total + state.project.unseenCount(directory), 0) - }, - clearNotifications: (conn: ServerConnection.Any, project: LocalProject) => { - const state = notification.ensureServerState(ServerConnection.key(conn)) - directories(project) - .filter((directory) => state.project.unseenCount(directory) > 0) - .forEach((directory) => state.project.markViewed(directory)) - }, - choose: (conn: ServerConnection.Any) => { - if (home.server.health(conn)?.healthy === false) return - pickDirectory({ - server: conn, - title: language.t("command.project.open"), - multiple: true, - onSelect: (result) => home.project.add(conn, homeProjectDirectories(result)), - }) - }, - close: (conn: ServerConnection.Any, directory: string) => { - const next = closeHomeProject( - home.selection.value(), - ServerConnection.key(conn), - home.server.context(conn).projects, - directory, - ) - if (next) home.selection.set(next) - }, - move: (conn: ServerConnection.Any, worktree: string, index: number) => { - home.server.context(conn).projects.move(worktree, index) - }, - canReveal: canRevealProject, - reveal: (conn: ServerConnection.Any, project: LocalProject) => { - if (!platform.openPath || !canRevealProject(conn)) return - platform.openPath(project.worktree).catch((cause: unknown) => - showToast({ - title: language.t("common.requestFailed"), - description: errorMessage(cause, language.t("common.requestFailed")), - }), - ) - }, - }, - utility: { - settings: openSettings, - help: () => platform.openExternal("https://opencode.ai/desktop-feedback"), - }, - } -} - -export type HomeProjectsController = ReturnType diff --git a/packages/app/src/pages/home/home-projects-view.tsx b/packages/app/src/pages/home/home-projects-view.tsx deleted file mode 100644 index d80d4a942f..0000000000 --- a/packages/app/src/pages/home/home-projects-view.tsx +++ /dev/null @@ -1,615 +0,0 @@ -import { type Accessor, createMemo, For, type JSX, onCleanup, Show, splitProps } from "solid-js" -import { createStore } from "solid-js/store" -import { DragDropProvider, PointerSensor } from "@dnd-kit/solid" -import { isSortable, useSortable } from "@dnd-kit/solid/sortable" -import { AutoScroller, Feedback, PointerActivationConstraints } from "@dnd-kit/dom" -import { RestrictToVerticalAxis } from "@dnd-kit/abstract/modifiers" -import { RestrictToElement } from "@dnd-kit/dom/modifiers" -import { ScrollView } from "@opencode-ai/ui/scroll-view" -import { ProjectAvatar } from "@opencode-ai/ui/v2/project-avatar-v2" -import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" -import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" -import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" -import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import { getProjectAvatarVariant, type HomeProjectSelection, type LocalProject } from "@/context/layout" -import { ServerConnection } from "@/context/server" -import { useLanguage } from "@/context/language" -import { usePlatform } from "@/context/platform" -import { displayName, getProjectAvatarSource } from "@/pages/layout/helpers" -import { ServerRowMenuView, serverMenuLabels } from "@/components/server/server-row-menu" -import { ServerHealthIndicator } from "@/components/server/server-row" -import { type ServerHealth } from "@/utils/server-health" -import { fileManagerApp } from "@/utils/file-manager" - -const HOME_PROJECT_NAV_LABEL = "min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap" - -const serverContextMenuID = (server: ServerConnection.Any) => `server:${ServerConnection.key(server)}` -const projectContextMenuID = (server: ServerConnection.Any, directory: string) => - `project:${ServerConnection.key(server)}:${directory}` - -export type HomeProjectsViewProps = { - language: ReturnType - servers: Accessor - projects: Accessor - recentlyClosed: Accessor - selection: Accessor - homedir: Accessor - serverHealth: (server: ServerConnection.Any) => ServerHealth | undefined - projectsForServer: (server: ServerConnection.Any) => LocalProject[] - collapsed: (server: ServerConnection.Any) => boolean - canDefaultServer: Accessor - defaultServerKey: Accessor - canRevealProject: (server: ServerConnection.Any) => boolean - unseenCount: (server: ServerConnection.Any, project: LocalProject) => number - onWheel: (event: WheelEvent) => void - onChooseProject: (server: ServerConnection.Any) => void - onFocusServer: (server: ServerConnection.Any) => void - onToggleCollapsed: (server: ServerConnection.Any) => void - onEditServer: (server: ServerConnection.Http) => void - onSetDefaultServer: (server: ServerConnection.Any | undefined) => void - onRemoveServer: (server: ServerConnection.Any) => void - onMoveProject: (server: ServerConnection.Any, worktree: string, index: number) => void - onSelectProject: (server: ServerConnection.Any, directory: string) => void - onAddProjects: (server: ServerConnection.Any, directories: string[]) => void - onOpenProjectNewSession: (server: ServerConnection.Any, directory: string) => void - onEditProject: (server: ServerConnection.Any, project: LocalProject) => void - onRevealProject: (server: ServerConnection.Any, project: LocalProject) => void - onClearNotifications: (server: ServerConnection.Any, project: LocalProject) => void - onCloseProject: (server: ServerConnection.Any, directory: string) => void - onOpenSettings: () => void - onOpenHelp: () => void -} - -export function HomeProjectsView(props: HomeProjectsViewProps) { - const [contextMenu, setContextMenu] = createStore({ open: undefined as string | undefined }) - const contextMenuProps = { - contextMenuOpen: (id: string) => contextMenu.open === id, - onSetContextMenuOpen: (id: string, open: boolean) => setContextMenu("open", open ? id : undefined), - } - return ( - - ) -} - -export function HomeUtilityNav(props: { - class?: string - onOpenSettings: () => void - onOpenHelp: () => void - language: ReturnType -}) { - return ( -
- - - {props.language.t("sidebar.settings")} - - - - {props.language.t("sidebar.help")} - -
- ) -} - -function HomeServerRow(props: { - language: HomeProjectsViewProps["language"] - projectsForServer: HomeProjectsViewProps["projectsForServer"] - contextMenuOpen: HomeProjectsContextMenuProps["contextMenuOpen"] - canDefaultServer: HomeProjectsViewProps["canDefaultServer"] - defaultServerKey: HomeProjectsViewProps["defaultServerKey"] - onFocusServer: HomeProjectsViewProps["onFocusServer"] - onToggleCollapsed: HomeProjectsViewProps["onToggleCollapsed"] - onEditServer: HomeProjectsViewProps["onEditServer"] - onSetDefaultServer: HomeProjectsViewProps["onSetDefaultServer"] - onRemoveServer: HomeProjectsViewProps["onRemoveServer"] - onSetContextMenuOpen: HomeProjectsContextMenuProps["onSetContextMenuOpen"] - onChooseProject: HomeProjectsViewProps["onChooseProject"] - server: ServerConnection.Any - selected: boolean - collapsed: boolean - health: ServerHealth | undefined -}) { - const healthy = () => !!props.health?.healthy - const canToggle = () => healthy() && props.projectsForServer(props.server).length > 0 - const contextMenuID = () => serverContextMenuID(props.server) - onCleanup(() => { - const id = contextMenuID() - if (props.contextMenuOpen(id)) props.onSetContextMenuOpen(id, false) - }) - return ( -
- props.onFocusServer(props.server)} - > - { - event.preventDefault() - event.stopPropagation() - if (!canToggle()) return - props.onToggleCollapsed(props.server) - }} - onPointerDown={(event) => event.preventDefault()} - > - - -
- -
- - {props.server.displayName ?? new URL(props.server.http.url).host} - - {(label) => ( - - {label()} - - )} - - -
-
- props.onSetDefaultServer(props.server)} - onRemoveDefault={() => props.onSetDefaultServer(undefined)} - onRemove={() => props.onRemoveServer(props.server)} - open={props.contextMenuOpen(contextMenuID())} - onOpenChange={(open) => props.onSetContextMenuOpen(contextMenuID(), open)} - /> - - } - aria-label={props.language.t("home.project.add")} - disabled={props.health?.healthy === false} - onClick={() => props.onChooseProject(props.server)} - /> - -
-
- ) -} - -type HomeProjectsContextMenuProps = { - contextMenuOpen: (id: string) => boolean - onSetContextMenuOpen: (id: string, open: boolean) => void -} - -type HomeProjectListProps = HomeProjectsViewProps & - HomeProjectsContextMenuProps & { - server: ServerConnection.Any - items: LocalProject[] - } - -function HomeProjectList(props: HomeProjectListProps) { - let listRef!: HTMLDivElement - - return ( - [ - ...defaults.filter((sensor) => sensor !== PointerSensor), - PointerSensor.configure({ - activationConstraints: (event) => - event.pointerType === "touch" - ? [new PointerActivationConstraints.Delay({ value: 250, tolerance: 5 })] - : [new PointerActivationConstraints.Distance({ value: 4 })], - preventActivation: (event) => event.target instanceof Element && !!event.target.closest("[data-action]"), - }), - ]} - modifiers={[RestrictToVerticalAxis, RestrictToElement.configure({ element: () => listRef })]} - plugins={(defaults) => [ - ...defaults.filter((plugin) => plugin !== AutoScroller && plugin !== Feedback), - AutoScroller.configure({ acceleration: 8, threshold: { x: 0, y: 0.05 } }), - Feedback.configure({ dropAnimation: null }), - ]} - onDragEnd={(event) => { - const source = event.operation.source - if (event.canceled || !isSortable(source)) return - if (source.initialIndex !== source.index) props.onMoveProject(props.server, source.id.toString(), source.index) - if (props.selection().server !== ServerConnection.key(props.server)) - props.onSelectProject(props.server, source.id.toString()) - }} - > -
- {/* Keyed on worktree strings: the enriched project objects are - recreated on every store or sync update, so iterating them directly - remounts all rows — killing any in-flight drag activation (the - row's sortable unregisters on unmount) and discarding animations. - String keys keep row elements alive and move them on reorder. */} - project.worktree)}> - {(worktree, index) => } - -
-
- ) -} - -function HomeProjectSlot( - props: HomeProjectListProps & { - worktree: string - index: () => number - }, -) { - const initial = props.items.find((item) => item.worktree === props.worktree) - if (!initial) return - const project = createMemo( - (previous) => props.items.find((item) => item.worktree === props.worktree) ?? previous, - initial, - ) - - return ( - - ) -} - -function HomeProjectEmpty( - props: HomeProjectsViewProps & { - server: ServerConnection.Any - items: LocalProject[] - }, -) { - const unreachable = () => props.serverHealth(props.server)?.healthy === false - return ( -
- props.onChooseProject(props.server)} - > - - {props.language.t("home.project.add")} - - 0}> -
-
{props.language.t("home.recentlyClosed")}
-
- - {(project) => } - -
-
- ) -} - -function HomeRecentlyClosedRow( - props: HomeProjectsViewProps & { - project: LocalProject - server: ServerConnection.Any - }, -) { - const unreachable = () => props.serverHealth(props.server)?.healthy === false - const path = () => { - const home = props.homedir() - const worktree = props.project.worktree - if (home && (worktree === home || worktree.startsWith(`${home}/`))) return `~${worktree.slice(home.length)}` - return worktree - } - return ( - - props.onAddProjects(props.server, [props.project.worktree])} - > - - {displayName(props.project)} - - - ) -} - -function HomeProjectRow( - props: HomeProjectsViewProps & - HomeProjectsContextMenuProps & { - project: LocalProject - server: ServerConnection.Any - index: () => number - serverSelected: boolean - selected: boolean - unseen: number - }, -) { - const platform = usePlatform() - const serverUnreachable = () => props.serverHealth(props.server)?.healthy === false - const sortable = useSortable({ - get id() { - return props.project.worktree - }, - get index() { - return props.index() - }, - }) - let pointerDownSelected: boolean | undefined - const contextMenuID = () => projectContextMenuID(props.server, props.project.worktree) - onCleanup(() => { - const id = contextMenuID() - if (props.contextMenuOpen(id)) props.onSetContextMenuOpen(id, false) - }) - return ( -
- { - // Same-server mouse selection happens on pointerdown (like tabs), - // but only ever selects; selectProject toggles, and deselecting here - // would fire on every drag before the threshold is met. Cross-server - // selection waits for click so reordering a remote server's projects - // does not focus that server and load its session index. Touch is - // excluded so flick-scrolling the list cannot select rows. - pointerDownSelected = undefined - if (event.button !== 0 || event.pointerType === "touch") return - if (!props.serverSelected) return - pointerDownSelected = props.selected - if (!props.selected) props.onSelectProject(props.server, props.project.worktree) - }} - onClick={(event) => { - // The drag sensor calls preventDefault on post-drag clicks; never - // toggle selection as part of a reorder. - if (event.defaultPrevented) return - // Keyboard activation and touch taps keep the original toggle. - if (event.detail === 0 || pointerDownSelected === undefined) { - props.onSelectProject(props.server, props.project.worktree) - return - } - // Mouse: pointerdown already selected unselected rows; a plain click - // on an already-selected row toggles it off. - if (pointerDownSelected) props.onSelectProject(props.server, props.project.worktree) - pointerDownSelected = undefined - }} - > - - {displayName(props.project)} - -
- props.onSetContextMenuOpen(contextMenuID(), open)} - > - } - aria-label={props.language.t("common.moreOptions")} - /> - - - props.onOpenProjectNewSession(props.server, props.project.worktree)}> - {props.language.t("command.session.new")} - - props.onEditProject(props.server, props.project)}> - {props.language.t("dialog.project.edit.title")} - - - props.onRevealProject(props.server, props.project)}> - {props.language.t( - fileManagerApp(platform.platform === "desktop" ? (platform.os ?? "unknown") : "unknown") - .actionLabel, - )} - - - props.onClearNotifications(props.server, props.project)} - > - {props.language.t("sidebar.project.clearNotifications")} - - - props.onCloseProject(props.server, props.project.worktree)}> - {props.language.t("common.close")} - - - - - } - aria-label={props.language.t("command.session.new")} - onClick={() => props.onOpenProjectNewSession(props.server, props.project.worktree)} - /> -
-
- ) -} - -function HomeProjectNavButton(props: JSX.ButtonHTMLAttributes) { - const [local, rest] = splitProps(props, ["class", "classList", "children"]) - return ( - - ) -} - -function HomeProjectAvatar(props: { project: LocalProject; outline?: boolean }) { - const name = createMemo(() => displayName(props.project)) - return ( - - ) -} diff --git a/packages/app/src/pages/home/home-projects.tsx b/packages/app/src/pages/home/home-projects.tsx deleted file mode 100644 index ff2abf7c16..0000000000 --- a/packages/app/src/pages/home/home-projects.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { HomeProjectsController } from "./home-projects-controller" -import { HomeProjectsView } from "./home-projects-view" -import type { HomeScrollController } from "./home-scroll-controller" - -export function HomeProjects(props: { projects: HomeProjectsController; scroll: HomeScrollController }) { - return ( - - ) -} diff --git a/packages/app/src/pages/home/home-scroll-controller.ts b/packages/app/src/pages/home/home-scroll-controller.ts deleted file mode 100644 index 96b0cf46d3..0000000000 --- a/packages/app/src/pages/home/home-scroll-controller.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js" -import { createStore } from "solid-js/store" -import type { HomeSessionGroup } from "./home-sessions-controller" - -const HOME_SESSION_HEADER_STICKY_TOP = 12 -const HOME_SESSION_HEADER_TEXT_HEIGHT = 16 -const HOME_SESSION_HEADER_FADE_DISTANCE = 16 - -export function createHomeScrollController(groups: Accessor) { - const [thumbTrack, setThumbTrack] = createSignal() - const [hoverTarget, setHoverTarget] = createSignal() - const [state, setState] = createStore({ - titleOpacity: {} as Partial>, - }) - const headerRefs = new Map() - const headerOffsets = new Map() - let viewport: HTMLDivElement | undefined - let content: HTMLDivElement | undefined - let positionFrame: number | undefined - let resizeObserver: ResizeObserver | undefined - let stickyTop = HOME_SESSION_HEADER_STICKY_TOP - - createEffect(() => { - const items = groups() - const ids = new Set(items.map((group) => group.id)) - headerRefs.forEach((_, id) => { - if (!ids.has(id)) headerRefs.delete(id) - }) - headerOffsets.forEach((_, id) => { - if (!ids.has(id)) headerOffsets.delete(id) - }) - if (items.length === 0) { - content = undefined - bindResizeObserver() - } - queuePositionUpdate() - }) - - onCleanup(() => { - if (positionFrame !== undefined) cancelAnimationFrame(positionFrame) - resizeObserver?.disconnect() - }) - - function queuePositionUpdate() { - if (typeof requestAnimationFrame === "undefined") { - updatePositionCache() - return - } - if (positionFrame !== undefined) return - positionFrame = requestAnimationFrame(() => { - positionFrame = undefined - updatePositionCache() - }) - } - - function updatePositionCache() { - if (!viewport) return - const header = groups() - .map((group) => headerRefs.get(group.id)) - .find((element) => element !== undefined) - if (header && typeof getComputedStyle === "function") { - const top = Number.parseFloat(getComputedStyle(header).top) - if (Number.isFinite(top)) stickyTop = top - } - groups().forEach((group) => { - const element = headerRefs.get(group.id) - if (element) headerOffsets.set(group.id, element.offsetTop) - }) - update(viewport.scrollTop) - } - - function update(scrollTop: number) { - const items = groups() - items.forEach((group, index) => { - const nextOffset = items - .slice(index + 1) - .map((item) => headerOffsets.get(item.id)) - .find((offset) => offset !== undefined) - const fadeEnd = stickyTop + HOME_SESSION_HEADER_TEXT_HEIGHT - const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop - const opacity = - nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE)) - setState("titleOpacity", group.id, Math.round(opacity * 1000) / 1000) - }) - } - - function bindResizeObserver() { - resizeObserver?.disconnect() - if (typeof ResizeObserver === "undefined") return - resizeObserver = new ResizeObserver(queuePositionUpdate) - if (viewport) resizeObserver.observe(viewport) - if (content) resizeObserver.observe(content) - } - - function containWheel(event: WheelEvent) { - if (!viewport) return - if (event.defaultPrevented || event.ctrlKey || !event.deltaY) return - if (!(event.target instanceof Element)) return - const scrollable = event.target.closest("[data-scrollable]") - if ( - scrollable !== viewport && - scrollable && - (event.deltaY < 0 - ? scrollable.scrollTop > 0 - : scrollable.scrollTop < scrollable.scrollHeight - scrollable.clientHeight) - ) - return - event.preventDefault() - } - - return { - viewport: { - thumbTrack, - hoverTarget, - setThumbTrack, - setHoverTarget, - setViewport: (element: HTMLDivElement) => { - viewport = element - bindResizeObserver() - queuePositionUpdate() - }, - update, - containWheel, - containOuterWheel: (event: WheelEvent) => { - if (!viewport) return - if (event.target instanceof Node && viewport.contains(event.target)) return - containWheel(event) - }, - }, - header: { - setContent: (element: HTMLDivElement) => { - content = element - bindResizeObserver() - queuePositionUpdate() - }, - setHeader: (id: HomeSessionGroup["id"], element: HTMLDivElement) => { - headerRefs.set(id, element) - queuePositionUpdate() - }, - titleOpacity: (id: HomeSessionGroup["id"]) => state.titleOpacity[id] ?? 1, - }, - } -} - -export type HomeScrollController = ReturnType diff --git a/packages/app/src/pages/home/home-session-search-controller.ts b/packages/app/src/pages/home/home-session-search-controller.ts deleted file mode 100644 index 5e55c4b746..0000000000 --- a/packages/app/src/pages/home/home-session-search-controller.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" -import { serverName } from "@/context/server" -import { displayName } from "@/pages/layout/helpers" -import { makeEventListener } from "@solid-primitives/event-listener" -import { createMemo, onCleanup } from "solid-js" -import { createStore } from "solid-js/store" -import type { HomeController } from "./home-controller" -import { homeSessionSearchKey, type HomeSessionRecord, type HomeSessionsController } from "./home-sessions-controller" - -type HomeSessionSearchSource = Pick - -export function createHomeSessionSearchController(home: HomeController, sessions: HomeSessionSearchSource) { - const command = useCommand() - const language = useLanguage() - const [state, setState] = createStore({ value: "", focused: false, highlighted: "" }) - let root: HTMLDivElement | undefined - let input: HTMLInputElement | undefined - let list: HTMLDivElement | undefined - const query = createMemo(() => state.value.trim()) - const results = createMemo(() => { - const value = query().toLowerCase() - if (!value) return [] - return sessions.data - .searchRecords() - .filter((record) => `${record.session.title} ${record.projectName}`.toLowerCase().includes(value)) - }) - const active = createMemo(() => { - const records = results() - if (records.some((record) => homeSessionSearchKey(record) === state.highlighted)) return state.highlighted - return records[0] ? homeSessionSearchKey(records[0]) : "" - }) - const open = createMemo(() => state.focused && query().length > 0) - const placeholder = createMemo(() => { - const project = home.project.selected() - if (project) return language.t("home.sessions.search.placeholder.scoped", { scope: displayName(project) }) - if (home.server.list().length > 1) { - const conn = home.server.focused() - if (conn) return language.t("home.sessions.search.placeholder.scoped", { scope: serverName(conn) }) - } - return language.t("home.sessions.search.placeholder") - }) - - onCleanup( - makeEventListener(document, "pointerdown", (event) => { - if (!open()) return - const target = event.target - if (!(target instanceof Node) || root?.contains(target)) return - close() - }), - ) - - command.register("home.search", () => [ - { - id: "home.sessions.search.focus", - title: placeholder(), - keybind: "mod+f", - hidden: true, - onSelect: focus, - }, - ]) - - function focus() { - input?.focus() - setState("focused", true) - } - - function close() { - setState({ value: "", focused: false }) - } - - function select(record: HomeSessionRecord, options?: { background?: boolean }) { - sessions.session.open(record.session, options) - if (!options?.background) close() - } - - return { - query: { - value: () => state.value, - placeholder, - open, - focus, - input: (value: string) => setState({ value, highlighted: "" }), - close, - }, - result: { - loading: sessions.data.loading, - list: results, - active, - noResultsLabel: () => language.t("home.sessions.search.noResults", { query: query() }), - highlight: (record: HomeSessionRecord) => setState("highlighted", homeSessionSearchKey(record)), - move: (delta: number) => { - const records = results() - if (records.length === 0) return - const index = records.findIndex((record) => homeSessionSearchKey(record) === active()) - const next = ((index === -1 ? 0 : index) + delta + records.length) % records.length - setState("highlighted", homeSessionSearchKey(records[next])) - list?.querySelector(`[data-key="${state.highlighted}"]`)?.scrollIntoView({ block: "nearest" }) - }, - select, - selectActive: () => { - const record = results().find((item) => homeSessionSearchKey(item) === active()) - if (record) select(record) - }, - }, - element: { - setRoot: (element: HTMLDivElement) => (root = element), - setInput: (element: HTMLInputElement) => (input = element), - setList: (element: HTMLDivElement) => (list = element), - }, - } -} - -export type HomeSessionSearchController = ReturnType diff --git a/packages/app/src/pages/home/home-sessions-controller.tsx b/packages/app/src/pages/home/home-sessions-controller.tsx deleted file mode 100644 index a12e9800e9..0000000000 --- a/packages/app/src/pages/home/home-sessions-controller.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import type { Session } from "@opencode-ai/sdk/v2/client" -import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useMarked } from "@opencode-ai/ui/context/marked" -import { useQuery } from "@tanstack/solid-query" -import { DateTime } from "luxon" -import { type Accessor, createEffect, createMemo, createRoot, type JSX, startTransition } from "solid-js" -import { produce } from "solid-js/store" -import { useCommand } from "@/context/command" -import { - loadHomeSessionIndex, - retainHomeSessions, - type HomeSessionEvents, -} from "@/context/global-sync/home-session-index" -import type { LocalProject } from "@/context/layout" -import { useLanguage } from "@/context/language" -import { ServerConnection } from "@/context/server" -import { sessionHasOpenTab, useTabs } from "@/context/tabs" -import { displayName, errorMessage, projectForSession } from "@/pages/layout/helpers" -import { useSessionTabAvatarState } from "@/pages/layout/project-avatar-state" -import { pathKey } from "@/utils/path-key" -import { showToast } from "@/utils/toast" -import { Binary } from "@opencode-ai/core/util/binary" -import { archiveHomeSession } from "../home-session-archive" -import type { HomeController } from "./home-controller" - -const HOME_SESSION_LIMIT = 64 -export type HomeSessionRecord = { - session: Session - project: LocalProject - projectName: string -} - -export type HomeSessionGroup = { - id: "today" | "yesterday" | "older" - title: string - sessions: HomeSessionRecord[] -} - -export type OpenSessionOptions = { background?: boolean } - -export function createHomeSessionsController(home: HomeController) { - const tabs = useTabs() - const command = useCommand() - const dialog = useDialog() - const language = useLanguage() - const marked = useMarked() - const projectDirectories = createMemo(() => { - const project = home.project.selected() - if (!project) return home.project.list().flatMap(directories) - return directories(project) - }) - const projectByID = createMemo( - () => new Map(home.project.list().flatMap((project) => (project.id ? [[project.id, project] as const] : []))), - ) - const homeSessions = () => home.server.focusedSync().homeSessions - const sessionEventLoad = useQuery(() => ({ - queryKey: homeSessions().eventsKey, - queryFn: async (): Promise => ({ sequence: 0, entries: [] }), - initialData: { sequence: 0, entries: [] } satisfies HomeSessionEvents, - enabled: false, - })) - const sessionLoad = useQuery(() => ({ - queryKey: homeSessions().indexKey, - enabled: !!home.server.focusedContext(), - queryFn: async ({ signal }) => { - const ctx = home.server.focusedContext() - if (!ctx) return { sessions: [], eventSequence: 0 } - const cache = homeSessions() - const eventSequence = cache.eventSequence() - const index = await loadHomeSessionIndex( - (input, options) => ctx.sdk.client.v2.session.list(input, options), - eventSequence, - signal, - ) - cache.complete(eventSequence) - return index - }, - retry: false, - staleTime: 30_000, - refetchOnMount: true, - refetchOnReconnect: true, - })) - const indexedSessions = createMemo(() => - retainHomeSessions( - homeSessions().sessions(sessionLoad.data, sessionEventLoad.data), - HOME_SESSION_LIMIT, - Date.now(), - ), - ) - const allRecords = createMemo(() => - buildHomeSessionRecords({ - sessions: indexedSessions, - projectDirectories, - projects: home.project.list, - projectByID, - }), - ) - const records = createMemo(() => allRecords().slice(0, HOME_SESSION_LIMIT)) - const groups = createMemo(() => groupSessions(records(), language)) - const prefetched = new Set() - - createEffect(() => { - const ctx = home.server.focusedContext() - const conn = home.server.focused() - if (!ctx || !conn) return - records() - .slice(0, 2) - .forEach((record) => { - const key = `${ServerConnection.key(conn)}\0${record.session.id}` - if (prefetched.has(key)) return - prefetched.add(key) - createRoot((dispose) => { - try { - void ctx.sync.session - .sync(record.session.id) - .then(() => - Promise.all( - (ctx.sync.session.data.message[record.session.id] ?? []).flatMap((message) => - (ctx.sync.session.data.part[message.id] ?? []).flatMap((part) => { - if (part.type !== "text" || !part.text) return [] - return preloadMarkdown(part.text, part.id, marked) - }), - ), - ), - ) - .catch(() => {}) - .finally(dispose) - } catch { - dispose() - } - }) - }) - }) - - command.register("home.palette", () => [ - { - id: "command.palette", - title: language.t("command.palette"), - hidden: true, - onSelect: async () => { - const conn = home.server.focused() - if (!conn) return - const ctx = home.server.focusedContext() - if (!ctx) return - const { DialogHomeCommandPaletteV2 } = await import("@/components/dialog-command-palette-v2") - void dialog.show(() => ( - { - if (!entry.sessionID || !entry.directory || !entry.server) return - const sessionID = entry.sessionID - const server = entry.server - const directory = entry.project?.worktree ?? entry.directory - ctx.projects.open(directory) - ctx.projects.touch(directory) - void startTransition(() => { - const tab = tabs.addSessionTab({ server, sessionId: sessionID }) - tabs.select(tab) - }) - }} - /> - )) - }, - }, - ]) - - return { - copy: { - language, - }, - data: { - records, - groups, - loading: () => sessionLoad.isLoading, - searchRecords: allRecords, - }, - session: { - showProjectName: () => !home.project.selected(), - server: () => home.selection.value().server, - canCreate: () => !!home.project.newSession(), - create: home.project.openNewSession, - open: (session: Session, options?: OpenSessionOptions) => { - const directoryKey = pathKey(session.directory) - const project = - home.project - .list() - .find( - (item) => - pathKey(item.worktree) === directoryKey || - item.sandboxes?.some((sandbox) => pathKey(sandbox) === directoryKey), - ) ?? projectForSession(session, home.project.list(), projectByID()) - const conn = home.server.focused() - if (!conn) return - const directory = project?.worktree ?? session.directory - const ctx = home.server.focusedContext() - if (!ctx) return - ctx.projects.open(directory) - if (options?.background) { - tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id }) - return - } - ctx.projects.touch(directory) - void startTransition(() => { - const tab = tabs.addSessionTab({ server: ServerConnection.key(conn), sessionId: session.id }) - tabs.select(tab) - }) - }, - archive: async (session: Session) => { - const conn = home.server.focused() - const ctx = home.server.focusedContext() - if (!conn || !ctx) return - const [, setStore] = ctx.sync.child(session.directory) - if ((await ctx.sdk.protocol) !== "v1") return - await archiveHomeSession({ - server: ServerConnection.key(conn), - session, - archive: (sessionID) => - ctx.sdk.client.session.update({ - sessionID, - directory: session.directory, - time: { archived: Date.now() }, - }), - remove: () => - setStore( - produce((draft) => { - const match = Binary.search(draft.session, session.id, (item) => item.id) - if (match.found) draft.session.splice(match.index, 1) - }), - ), - onError: (cause) => - showToast({ - title: language.t("common.requestFailed"), - description: errorMessage(cause, language.t("common.requestFailed")), - }), - }) - }, - }, - tab: { - isOpen: (record: HomeSessionRecord) => - sessionHasOpenTab(tabs.store, home.selection.value().server, record.session), - }, - } -} - -function directories(project: LocalProject) { - return [project.worktree, ...(project.sandboxes ?? [])] -} - -function buildHomeSessionRecords(input: { - sessions: () => Session[] - projectDirectories: () => string[] - projects: () => LocalProject[] - projectByID: () => Map -}) { - const directories = new Set(input.projectDirectories().map(pathKey)) - const sessions = input.sessions().filter((session) => directories.has(pathKey(session.directory))) - return [...new Map(sessions.map((session) => [session.id, session] as const)).values()] - .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) - .flatMap((session) => { - const directory = pathKey(session.directory) - const project = - input - .projects() - .find( - (item) => - pathKey(item.worktree) === directory || item.sandboxes?.some((sandbox) => pathKey(sandbox) === directory), - ) ?? projectForSession(session, input.projects(), input.projectByID()) - if (!project) return [] - return { session, project, projectName: displayName(project) } - }) -} - -export function homeSessionSearchKey(record: HomeSessionRecord) { - return `${pathKey(record.session.directory)}:${record.session.id}` -} - -function groupSessions(records: HomeSessionRecord[], language: ReturnType): HomeSessionGroup[] { - const now = DateTime.local() - const yesterday = now.minus({ days: 1 }) - const todaySessions = records.filter((record) => - DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(now, "day"), - ) - const yesterdaySessions = records.filter((record) => - DateTime.fromMillis(record.session.time.updated ?? record.session.time.created).hasSame(yesterday, "day"), - ) - const olderSessions = records.filter((record) => { - const time = DateTime.fromMillis(record.session.time.updated ?? record.session.time.created) - return !time.hasSame(now, "day") && !time.hasSame(yesterday, "day") - }) - const olderTitle = - todaySessions.length === 0 && yesterdaySessions.length === 0 - ? language.t("sidebar.project.recentSessions") - : language.t("home.sessions.group.older") - return [ - { id: "today" as const, title: language.t("home.sessions.group.today"), sessions: todaySessions }, - { id: "yesterday" as const, title: language.t("home.sessions.group.yesterday"), sessions: yesterdaySessions }, - { id: "older" as const, title: olderTitle, sessions: olderSessions }, - ].filter((group) => group.sessions.length > 0) -} - -export type HomeSessionsController = ReturnType - -export function HomeSessionStatusController(props: { - server: Accessor - record: HomeSessionRecord - isOpenTab: (record: HomeSessionRecord) => boolean - render: (state: { unread: Accessor; loading: Accessor; open: Accessor }) => JSX.Element -}) { - const avatar = useSessionTabAvatarState( - props.server, - () => props.record.session.directory, - () => props.record.session.id, - ) - return props.render({ - unread: avatar.unread, - loading: avatar.loading, - open: () => props.isOpenTab(props.record), - }) -} diff --git a/packages/app/src/pages/home/home-sessions-view.tsx b/packages/app/src/pages/home/home-sessions-view.tsx deleted file mode 100644 index 72f2f37616..0000000000 --- a/packages/app/src/pages/home/home-sessions-view.tsx +++ /dev/null @@ -1,550 +0,0 @@ -import type { Session } from "@opencode-ai/sdk/v2/client" -import { type Accessor, createMemo, For, Show, Suspense } from "solid-js" -import { Spinner } from "@opencode-ai/ui/spinner" -import { ScrollView } from "@opencode-ai/ui/scroll-view" -import { ButtonV2 } from "@opencode-ai/ui/v2/button-v2" -import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" -import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" -import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import { useLanguage } from "@/context/language" -import { ServerConnection } from "@/context/server" -import { SessionTabAvatarView } from "@/pages/layout/session-tab-avatar" -import { sessionTitle } from "@/utils/session-title" -import { shouldOpenSessionInBackground } from "../home-session-open" -import { - HomeSessionStatusController, - homeSessionSearchKey, - type HomeSessionGroup, - type HomeSessionRecord, - type OpenSessionOptions, -} from "./home-sessions-controller" - -const SHOW_HOME_SESSION_ARCHIVE = false -const HOME_SECTION_LABEL = "text-v2-text-text-muted [font-weight:440]" -const HOME_SESSION_SEARCH_RESULTS_ID = "home-session-search-results" - -// Middle-click or Cmd+click on macOS (Ctrl+click elsewhere) opens a session -// tab in the background without navigating, matching browser conventions. -function isBackgroundOpen(event: MouseEvent) { - return shouldOpenSessionInBackground({ - button: event.button, - mac: typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform), - meta: event.metaKey, - ctrl: event.ctrlKey, - shift: event.shiftKey, - alt: event.altKey, - }) -} - -export type HomeSessionsViewProps = { - language: ReturnType - groups: Accessor - showProjectName: Accessor - server: Accessor - canCreateSession: Accessor - searchValue: Accessor - searchPlaceholder: Accessor - searchOpen: Accessor - searchLoading: Accessor - searchResults: Accessor - searchActive: Accessor - searchNoResultsLabel: Accessor - titleOpacity: (id: HomeSessionGroup["id"]) => number - isOpenTab: (record: HomeSessionRecord) => boolean - onCreateSession: () => void - onOpenSession: (session: Session, options?: OpenSessionOptions) => void - onArchiveSession: (session: Session) => Promise - onSetHoverTarget: (element: HTMLElement) => void - onSetThumbTrack: (element: HTMLDivElement) => void - onSetContent: (element: HTMLDivElement) => void - onSetHeader: (id: HomeSessionGroup["id"], element: HTMLDivElement) => void - onWheel: (event: WheelEvent) => void - onSetSearchRoot: (element: HTMLDivElement) => void - onSetSearchInput: (element: HTMLInputElement) => void - onSetSearchList: (element: HTMLDivElement) => void - onSearchFocus: () => void - onSearchInput: (value: string) => void - onSearchClose: () => void - onSearchMove: (delta: number) => void - onSearchSelectActive: () => void - onSearchHighlight: (record: HomeSessionRecord) => void - onSearchSelect: (record: HomeSessionRecord, options?: OpenSessionOptions) => void -} - -export function HomeSessionsView(props: HomeSessionsViewProps) { - return ( -
-
- - - 0 && props.canCreateSession()}> -
- - {props.language.t("command.session.new")} - -
-
-
-
- -
- ) -} - -function HomeSessionLeadingController(props: { - server: HomeSessionsViewProps["server"] - isOpenTab: HomeSessionsViewProps["isOpenTab"] - record: HomeSessionRecord - revealProjectOnHover: boolean -}) { - return ( - ( - - )} - /> - ) -} - -function HomeSessionLeading(props: { - record: HomeSessionRecord - revealProjectOnHover: boolean - open: boolean - unread: boolean - loading: boolean -}) { - return ( -
- - - -
- ) -} - -function HomeSessionSearch(props: HomeSessionsViewProps) { - return ( -
-
- -
-
-
- - -
- } - > - 0} - fallback={ -

- {props.searchNoResultsLabel()} -

- } - > -
-

- {props.language.t("home.sessions.search.sessions")} -

- -
- - {(record) => ( - - )} - -
-
-
-
- -
-
-
- - -
-
- ) -} - -function HomeSessionSearchResultRow( - props: HomeSessionsViewProps & { - record: HomeSessionRecord - selected: boolean - }, -) { - const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) - const showProjectName = () => props.showProjectName() && props.record.projectName - const key = () => homeSessionSearchKey(props.record) - - return ( - - ) -} - -function HomeSessionGroupHeader(props: { - title: string - titleOpacity: number - onSetRef: (element: HTMLDivElement) => void - elevated?: boolean -}) { - return ( -
- -
- ) -} - -function HomeSessionRow(props: HomeSessionsViewProps & { record: HomeSessionRecord }) { - const title = createMemo(() => sessionTitle(props.record.session.title) || props.record.session.id) - const showProjectName = () => props.showProjectName() && props.record.projectName - - return ( -
- - -
- - } - aria-label={props.language.t("common.archive")} - onClick={(event) => { - event.preventDefault() - event.stopPropagation() - void props.onArchiveSession(props.record.session) - }} - /> - -
-
-
- ) -} - -function HomeSessionTitle(props: { title: string; showProjectName: boolean; search?: boolean }) { - return ( - - {props.title} - - ) -} - -function HomeSessionProjectName(props: { name: string; search?: boolean }) { - return ( - - {props.name} - - ) -} - -function HomeSessionsEmpty(props: { onNewSession?: () => void; language: ReturnType }) { - return ( -
-
- {props.language.t("home.sessions.empty")} -
-

- {props.language.t("home.sessions.empty.description")} -

- - {(onNewSession) => ( - - {props.language.t("command.session.new")} - - )} - -
- ) -} - -function HomeSessionSkeleton(props: { label: string }) { - return ( -
-
- -
- - ) -} diff --git a/packages/app/src/pages/home/home-sessions.tsx b/packages/app/src/pages/home/home-sessions.tsx deleted file mode 100644 index 7bda4dd363..0000000000 --- a/packages/app/src/pages/home/home-sessions.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { HomeScrollController } from "./home-scroll-controller" -import type { HomeSessionSearchController } from "./home-session-search-controller" -import type { HomeSessionsController } from "./home-sessions-controller" -import { HomeSessionsView } from "./home-sessions-view" - -export function HomeSessions(props: { - sessions: HomeSessionsController - search: HomeSessionSearchController - scroll: HomeScrollController -}) { - return ( - - ) -} diff --git a/packages/app/src/pages/home/legacy-home.tsx b/packages/app/src/pages/home/legacy-home.tsx deleted file mode 100644 index 0556f7cf60..0000000000 --- a/packages/app/src/pages/home/legacy-home.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { DialogSelectServer } from "@/components/dialog-select-server" -import { useDirectoryPicker } from "@/components/directory-picker" -import { useGlobal } from "@/context/global" -import { useLanguage } from "@/context/language" -import { type ServerConnection, useServer } from "@/context/server" -import { useServerSync } from "@/context/server-sync" -import { base64Encode } from "@opencode-ai/core/util/encode" -import { Button } from "@opencode-ai/ui/button" -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Icon } from "@opencode-ai/ui/icon" -import { Logo } from "@opencode-ai/ui/logo" -import { useNavigate } from "@solidjs/router" -import { DateTime } from "luxon" -import { createMemo, For, Match, Switch } from "solid-js" - -export function LegacyHome() { - const sync = useServerSync() - const pickDirectory = useDirectoryPicker() - const dialog = useDialog() - const navigate = useNavigate() - const global = useGlobal() - const server = useServer() - const language = useLanguage() - const homedir = createMemo(() => sync().data.path.home) - const serverUnreachable = createMemo(() => global.servers.health[server.key]?.healthy === false) - const recent = createMemo(() => { - return sync() - .data.project.slice() - .sort((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created)) - .slice(0, 5) - }) - - const serverDotClass = createMemo(() => { - const healthy = global.servers.health[server.key]?.healthy - if (healthy === true) return "bg-icon-success-base" - if (healthy === false) return "bg-icon-critical-base" - return "bg-border-weak-base" - }) - - function openProject(conn: ServerConnection.Any, directory: string) { - const serverCtx = global.ensureServerCtx(conn) - serverCtx.projects.open(directory) - serverCtx.projects.touch(directory) - navigate(`/${base64Encode(directory)}`) - } - - function chooseProject() { - if (serverUnreachable()) return - const conn = server.current - if (!conn) return - - const resolve = (result: string | string[] | null) => { - if (Array.isArray(result)) { - result.forEach((directory) => openProject(conn, directory)) - return - } - if (result) openProject(conn, result) - } - - pickDirectory({ - server: conn, - title: language.t("command.project.open"), - multiple: true, - onSelect: resolve, - }) - } - - return ( -
- - - - 0}> -
-
-
{language.t("home.recentProjects")}
- -
-
    - - {(project) => ( - - )} - -
-
-
- -
-
{language.t("common.loading")}
- -
-
- -
- -
-
{language.t("home.empty.title")}
-
{language.t("home.empty.description")}
-
- -
-
-
-
- ) -} diff --git a/packages/app/src/pages/layout-new.tsx b/packages/app/src/pages/layout-new.tsx index 6d76ca8401..c6fed8f6d5 100644 --- a/packages/app/src/pages/layout-new.tsx +++ b/packages/app/src/pages/layout-new.tsx @@ -1,13 +1,17 @@ import { createEffect, Suspense, type ParentProps } from "solid-js" import { createStore } from "solid-js/store" +import { useNavigate } from "@solidjs/router" import { DebugBar } from "@/components/debug-bar" import { TabsInfoPopup } from "@/components/help-button" import { Titlebar, type TitlebarUpdate } from "@/components/titlebar" import { usePlatform } from "@/context/platform" +import { setNavigate } from "@/utils/notification-click" import { setV2Toast, ToastRegion } from "@/utils/toast" export default function NewLayout(props: ParentProps) { const platform = usePlatform() + const navigate = useNavigate() + setNavigate(navigate) const [state, setState] = createStore({ debugTools: true }) createEffect(() => setV2Toast(true)) diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 963a542f88..812a479b20 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -33,9 +33,9 @@ import { createStore, produce, reconcile } from "solid-js/store" import { DragDropProvider, DragDropSensors, DragOverlay, SortableProvider, closestCenter } from "@thisbeyond/solid-dnd" import type { DragEvent } from "@thisbeyond/solid-dnd" import { useProviders } from "@/hooks/use-providers" -import { dismissToast, setV2Toast, showToast, ToastRegion } from "@/utils/toast" +import { toaster } from "@opencode-ai/ui/toast" +import { setV2Toast, showToast, ToastRegion } from "@/utils/toast" import { useServerSDK } from "@/context/server-sdk" -import { normalizeProjectInfo } from "@/context/global-sync/utils" import { clearWorkspaceTerminals } from "@/context/terminal" import { pickSessionCacheEvictions } from "@/context/global-sync/session-cache" import { useNotification } from "@/context/notification" @@ -44,10 +44,10 @@ 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" import { Worktree as WorktreeState } from "@/utils/worktree" import { setSessionHandoff } from "@/pages/session/handoff" import { SessionRouteKey, SessionStateKey } from "@/utils/server-scope" -import { listAllSessions } from "@/utils/session" import { useDialog } from "@opencode-ai/ui/context/dialog" import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme/context" @@ -116,7 +116,8 @@ export default function LegacyLayout(props: ParentProps) { const notification = useNotification() const permission = usePermission() const navigate = useNavigate() - const providers = useProviders(() => undefined) + setNavigate(navigate) + const providers = useProviders() const dialog = useDialog() const command = useCommand() const theme = useTheme() @@ -381,7 +382,7 @@ export default function LegacyLayout(props: ParentProps) { const dismissSessionAlert = (sessionKey: string) => { const toastId = toastBySession.get(sessionKey) if (toastId === undefined) return - dismissToast(toastId) + toaster.dismiss(toastId) toastBySession.delete(sessionKey) alertedAtBySession.delete(sessionKey) } @@ -446,13 +447,13 @@ export default function LegacyLayout(props: ParentProps) { void playSoundById(settings.sounds.permissions()) } if (settings.notifications.permissions()) { - void platform.notify(title, description, () => navigate(href)) + void platform.notify(title, description, href) } } if (e.details.type === "question.asked") { if (settings.notifications.agent()) { - void platform.notify(title, description, () => navigate(href)) + void platform.notify(title, description, href) } } @@ -869,15 +870,14 @@ export default function LegacyLayout(props: ParentProps) { } async function archiveSession(session: Session) { - if ((await serverSDK().protocol) !== "v1") return const [store, setStore] = serverSync().child(session.directory) const sessions = store.session ?? [] const index = sessions.findIndex((s) => s.id === session.id) const nextSession = sessions[index + 1] ?? sessions[index - 1] await serverSDK().client.session.update({ - sessionID: session.id, directory: session.directory, + sessionID: session.id, time: { archived: Date.now() }, }) setStore( @@ -1185,12 +1185,9 @@ export default function LegacyLayout(props: ParentProps) { } const refreshDirs = async (target?: string) => { if (!target || target === root || canOpen(target)) return canOpen(target) - const listed = await Promise.resolve( - project?.id ?? serverSDK().api.project.current({ location: { directory: root } }), - ) - .then((value) => (typeof value === "string" ? value : value.id)) - .then((projectID) => serverSDK().api.project.directories({ projectID, location: { directory: root } })) - .then((items) => items.map((item) => item.directory).filter((item) => pathKey(item) !== pathKey(root))) + const listed = await serverSDK() + .client.worktree.list({ directory: root }) + .then((x) => x.data ?? []) .catch(() => [] as string[]) dirs = effectiveWorkspaceOrder(root, [root, ...listed], store.workspaceOrder[root]) return canOpen(target) @@ -1234,11 +1231,10 @@ export default function LegacyLayout(props: ParentProps) { await Promise.all( dirs.map(async (item) => ({ path: { directory: item }, - session: await listAllSessions(serverSDK().api.session, { - directory: item, - parentID: null, - order: "desc", - }).catch(() => []), + session: await serverSDK() + .client.session.list({ directory: item }) + .then((x) => x.data ?? []) + .catch(() => []), })), ), Date.now(), @@ -1298,16 +1294,7 @@ export default function LegacyLayout(props: ParentProps) { const name = next === getFilename(project.worktree) ? "" : next if (project.id && project.id !== "global") { - const sdk = serverSDK() - if ((await sdk.protocol) !== "v1") return - const result = await sdk.client.project - .update({ projectID: project.id, directory: project.worktree, name }) - .then((response) => response.data) - if (!result) return - // const result = await serverSDK().api.project.update({ projectID: project.id, name }) - serverSync().set("project", (items) => - items.map((item) => (item.id === result.id ? normalizeProjectInfo(result) : item)), - ) + await serverSDK().client.project.update({ projectID: project.id, directory: project.worktree, name }) return } @@ -1456,9 +1443,12 @@ export default function LegacyLayout(props: ParentProps) { title: language.t("workspace.resetting.title"), description: language.t("workspace.resetting.description"), }) - const dismiss = () => dismissToast(progress) + const dismiss = () => toaster.dismiss(progress) - const sessions = await listAllSessions(serverSDK().api.session, { directory, order: "desc" }).catch(() => []) + const sessions: Session[] = await serverSDK() + .client.session.list({ directory }) + .then((x) => x.data ?? []) + .catch(() => []) clearWorkspaceTerminals( directory, @@ -1487,20 +1477,20 @@ export default function LegacyLayout(props: ParentProps) { return } - if ((await serverSDK().protocol) === "v1") - await Promise.all( - sessions - .filter((session) => session.time.archived === undefined) - .map((session) => - serverSDK() - .client.session.update({ - sessionID: session.id, - directory: session.directory, - time: { archived: Date.now() }, - }) - .catch(() => undefined), - ), - ) + const archivedAt = Date.now() + await Promise.all( + sessions + .filter((session) => session.time.archived === undefined) + .map((session) => + serverSDK() + .client.session.update({ + sessionID: session.id, + directory: session.directory, + time: { archived: archivedAt }, + }) + .catch(() => undefined), + ), + ) setBusy(directory, false) dismiss() @@ -1534,9 +1524,9 @@ export default function LegacyLayout(props: ParentProps) { onMount(() => { serverSDK() - .api.vcs.status({ location: { directory: props.directory } }) - .then((result) => { - const files = result.data + .client.vcs.status({ directory: props.directory }) + .then((x) => { + const files = x.data ?? [] const dirty = files.length > 0 setData({ status: "ready", dirty }) }) @@ -1592,19 +1582,19 @@ export default function LegacyLayout(props: ParentProps) { }) const refresh = async () => { - const sessions = await listAllSessions(serverSDK().api.session, { - directory: props.directory, - order: "desc", - }).catch(() => []) + const sessions = await serverSDK() + .client.session.list({ directory: props.directory }) + .then((x) => x.data ?? []) + .catch(() => []) const active = sessions.filter((session) => session.time.archived === undefined) setState({ sessions: active }) } onMount(() => { serverSDK() - .api.vcs.status({ location: { directory: props.directory } }) - .then((result) => { - const files = result.data + .client.vcs.status({ directory: props.directory }) + .then((x) => { + const files = x.data ?? [] const dirty = files.length > 0 setState({ status: "ready", dirty }) void refresh() @@ -2249,7 +2239,7 @@ export default function LegacyLayout(props: ParentProps) { settingsKeybind={() => command.keybind("settings.open")} onOpenSettings={openSettings} helpLabel={() => language.t("sidebar.help")} - onOpenHelp={() => platform.openExternal("https://opencode.ai/desktop-feedback")} + onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} renderPanel={() => mobile ? : } @@ -2447,7 +2437,7 @@ function UpdateAvailableToast(props: { onCleanup(() => { if (toastId === undefined) return - dismissToast(toastId) + toaster.dismiss(toastId) }) return null diff --git a/packages/app/src/pages/layout/project-avatar-state.ts b/packages/app/src/pages/layout/project-avatar-state.ts index 236f6bd405..8e5dc38d67 100644 --- a/packages/app/src/pages/layout/project-avatar-state.ts +++ b/packages/app/src/pages/layout/project-avatar-state.ts @@ -13,6 +13,7 @@ export function useSessionTabAvatarState( const global = useGlobal() const notification = useNotification() const permission = usePermission() + const permissionState = createMemo(() => permission.ensureServerState(server())) const connection = createMemo(() => global.servers.list().find((item) => ServerConnection.key(item) === server())) const sync = createMemo(() => { const conn = connection() @@ -21,10 +22,9 @@ export function useSessionTabAvatarState( const hasPermissions = createMemo(() => { const serverSync = sync() if (!serverSync) return false - const permissionState = permission.ensureServerState(server()) const [store] = serverSync.child(directory(), { bootstrap: false }) return !!sessionPermissionRequest(store.session, serverSync.session.data.permission, sessionId(), (item) => { - return !permissionState.autoResponds(item, directory()) + return !permissionState().autoResponds(item, directory()) }) }) const hasQuestions = createMemo(() => { @@ -34,11 +34,9 @@ export function useSessionTabAvatarState( return !!sessionQuestionRequest(store.session, serverSync.session.data.question, sessionId()) }) const needsAttention = createMemo(() => hasPermissions() || hasQuestions()) - const notificationState = createMemo(() => { - if (!connection()) return - return notification.ensureServerState(server()) - }) - const unread = createMemo(() => needsAttention() || (notificationState()?.session.unseenCount(sessionId()) ?? 0) > 0) + const unread = createMemo( + () => needsAttention() || notification.ensureServerState(server()).session.unseenCount(sessionId()) > 0, + ) const loading = createMemo(() => { const serverSync = sync() if (!serverSync) return false diff --git a/packages/app/src/pages/layout/session-tab-avatar.tsx b/packages/app/src/pages/layout/session-tab-avatar.tsx index 0902173643..3c776c8671 100644 --- a/packages/app/src/pages/layout/session-tab-avatar.tsx +++ b/packages/app/src/pages/layout/session-tab-avatar.tsx @@ -19,34 +19,16 @@ export function SessionTabAvatar(props: { () => props.directory, () => props.sessionId, ) - return ( - - ) -} - -export function SessionTabAvatarView(props: { - project?: LocalProject - directory: string - revealProjectOnHover?: boolean - unread: boolean - loading: boolean -}) { const projectAvatar = () => ( ) return ( - + sdk().directory) + const openProviders = () => { + void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => { + void dialog.show(() => sdk().directory} />) + }) + } + useSettingsCommand() + const route = useSessionKey() + const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() + const local = useLocal() + const model = createPromptModelSelection({ agent: local.agent.current }) + + useComposerCommands({ model }) + + const inputController = createPromptInputController({ + sessionKey: route.sessionKey, + sessionID: () => route.params.id, + queryOptions: serverSync().queryOptions, + model, + }) + const projectControls = createPromptProjectControls() + + const [store, setStore] = createStore<{ worktree?: string }>({}) const rightMount = useTitlebarRightMount() - const workspace = createNewSessionWorkspaceController() - const draft = createNewSessionDraftController({ - worktree: workspace.selection.value, - resetWorktree: workspace.selection.reset, + + const showWorkspaceBar = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") + const newSessionWorktree = createMemo(() => { + if (!showWorkspaceBar()) return "main" + if (store.worktree) return store.worktree + const project = sync().project + if (project && sdk().directory !== project.worktree) return sdk().directory + return "main" }) - const project = createPromptProjectController({ - controls: draft.project.controls, - onDone: draft.input.restoreFocus, + const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) + const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) + const selectedBranch = createMemo(() => { + const worktree = newSessionWorktree() + if (worktree === "main" || worktree === "create") return localBranch() + return serverSync().child(worktree)[0].vcs?.branch ?? localBranch() }) - useNewSessionCommands({ - restoreFocus: draft.input.restoreFocus, - project: { - empty: project.empty, - open: () => project.setOpen(true), + const promptInputV2Controller = usePromptInputV2Controller({ + get controls() { + return inputController() }, + get newSessionWorktree() { + return newSessionWorktree() + }, + onNewSessionWorktreeReset: () => setStore("worktree", undefined), + onSubmit: () => comments.clear(), }) + const projectController = createPromptProjectController({ + controls: projectControls, + onDone: promptInputV2Controller.restoreFocus, + }) + + command.register("new-session", () => [ + { + id: "command.palette", + title: language.t("command.palette"), + hidden: true, + onSelect: async () => { + const { DialogSelectFile } = await import("@/components/dialog-select-file") + void dialog.show(() => ) + }, + }, + { + id: "input.focus", + title: language.t("command.input.focus"), + category: language.t("command.category.view"), + keybind: "ctrl+l", + onSelect: () => promptInputV2Controller.restoreFocus(), + }, + ]) + createEffect(() => { - if (!draft.prompt.ready()) return - draft.input.restoreFocus() + if (!prompt.ready()) return + untrack(() => { + const text = searchParams.prompt + if (!text) return + prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) + setSearchParams({ ...searchParams, prompt: undefined }) + }) }) + + createEffect(() => { + if (!prompt.ready()) return + promptInputV2Controller.restoreFocus() + }) + const ready = Promise.resolve() const [suspendUntilPromptReady] = createResource( - () => draft.prompt.readyPromise() ?? ready, + () => prompt.ready.promise ?? ready, (promise) => promise.then(() => true), ) return (
{suspendUntilPromptReady()} - + + {(mount) => ( + + + + + + + + )} +
- +
+
+ +
+
+ + + + + +
+ + } + > + + setStore( + "worktree", + value === "main" && sync().project?.worktree !== sdk().directory + ? sync().project?.worktree + : value, + ) + } + onDone={promptInputV2Controller.restoreFocus} + /> + +
+
+
+ {/**/} +
+
+ serverSync().child(sdk().directory)[0].provider_ready} + connected={() => providers.paid().length > 0} + openProviders={openProviders} + /> +
+
) } + +function ProviderTip(props: { ready: () => boolean; connected: () => boolean; openProviders: () => void }) { + const language = useLanguage() + const [persistedState, setPersistedState, , persistedReady] = persisted( + Persist.global("new-session.provider-tip"), + createStore({ dismissedAt: 0 }), + ) + const visible = createMemo( + () => + props.ready() && + persistedReady() && + !props.connected() && + Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, + ) + + function dismiss() { + setPersistedState("dismissedAt", Date.now()) + } + + const [ref, setRef] = createSignal() + const presence = createPresence({ + show: () => visible(), + element: () => ref() ?? null, + }) + + return ( + +
+
+ + + + +
+
+
+ ) +} diff --git a/packages/app/src/pages/new-session/new-session-draft-controller.ts b/packages/app/src/pages/new-session/new-session-draft-controller.ts deleted file mode 100644 index bf22834e48..0000000000 --- a/packages/app/src/pages/new-session/new-session-draft-controller.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useSearchParams } from "@solidjs/router" -import { createEffect, untrack } from "solid-js" -import { usePromptInputV2Controller } from "@/components/prompt-input-v2" -import { useComments } from "@/context/comments" -import { useLocal } from "@/context/local" -import { usePrompt } from "@/context/prompt" -import { useServerSync } from "@/context/server-sync" -import { createPromptInputController, createPromptProjectControls } from "@/pages/session/composer" -import { createPromptModelSelection } from "@/pages/session/composer/prompt-model-selection" -import { useSessionKey } from "@/pages/session/session-layout" -import { useComposerCommands } from "@/pages/session/use-composer-commands" - -export function createNewSessionDraftController(workspace: { worktree: () => string; resetWorktree: () => void }) { - const prompt = usePrompt() - const serverSync = useServerSync() - const comments = useComments() - const local = useLocal() - const route = useSessionKey() - const [searchParams, setSearchParams] = useSearchParams<{ draftId?: string; prompt?: string }>() - const model = createPromptModelSelection({ agent: () => local.agent.current() }) - - useComposerCommands({ model }) - - const controls = createPromptInputController({ - sessionKey: route.sessionKey, - sessionID: () => route.params.id, - queryOptions: serverSync().queryOptions, - model, - }) - const projectControls = createPromptProjectControls() - const input = usePromptInputV2Controller({ - get controls() { - return controls() - }, - get newSessionWorktree() { - return workspace.worktree() - }, - onNewSessionWorktreeReset: workspace.resetWorktree, - onSubmit: comments.clear, - }) - - createEffect(() => { - if (!prompt.ready()) return - untrack(() => { - const text = searchParams.prompt - if (!text) return - prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length) - setSearchParams({ ...searchParams, prompt: undefined }) - }) - }) - - return { - input, - prompt: { - ready: prompt.ready, - readyPromise: () => prompt.ready.promise, - }, - project: { - controls: projectControls, - }, - } -} - -export type NewSessionDraftController = ReturnType diff --git a/packages/app/src/pages/new-session/new-session-view.tsx b/packages/app/src/pages/new-session/new-session-view.tsx deleted file mode 100644 index 5960e64335..0000000000 --- a/packages/app/src/pages/new-session/new-session-view.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { Tooltip } from "@opencode-ai/ui/tooltip" -import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" -import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" -import { WordmarkV2 } from "@opencode-ai/ui/v2/wordmark-v2" -import { Show, createMemo, createSignal, type Accessor } from "solid-js" -import { createStore } from "solid-js/store" -import { Portal } from "solid-js/web" -import createPresence from "solid-presence" -import { PromptInputV2Composer } from "@/components/prompt-input-v2" -import { PromptGitStatus, PromptWorkspaceSelector } from "@/components/prompt-workspace-selector" -import { - PromptProjectAddButton, - PromptProjectSelector, - type PromptProjectController, -} from "@/components/prompt-project-selector" -import { StatusPopoverV2 } from "@/components/status-popover" -import { useLanguage } from "@/context/language" -import { useSDK } from "@/context/sdk" -import { useServerSync } from "@/context/server-sync" -import { useProviders } from "@/hooks/use-providers" -import { NEW_SESSION_CONTENT_WIDTH } from "@/pages/session/new-session-layout" -import { Persist, persisted } from "@/utils/persist" -import type { NewSessionDraftController } from "./new-session-draft-controller" -import type { NewSessionWorkspaceController } from "./new-session-workspace-controller" - -const providerTipDismissalDuration = 30 * 24 * 60 * 60 * 1000 - -export function NewSessionView(props: { - input: NewSessionDraftController["input"] - project: PromptProjectController - workspace: NewSessionWorkspaceController -}) { - return ( -
-
-
-
- -
- - - - - -
- - - } - > - - -
-
-
-
-
- -
-
- ) -} - -export function NewSessionStatus(props: { mount: Accessor; visible: Accessor }) { - const language = useLanguage() - - return ( - - {(mount) => ( - - - - - - - - )} - - ) -} - -function ProviderTip() { - const language = useLanguage() - const dialog = useDialog() - const sdk = useSDK() - const serverSync = useServerSync() - const providers = useProviders(() => sdk().directory) - const [persistedState, setPersistedState, , persistedReady] = persisted( - Persist.global("new-session.provider-tip"), - createStore({ dismissedAt: 0 }), - ) - const visible = createMemo( - () => - serverSync().child(sdk().directory)[0].provider_ready && - persistedReady() && - providers.paid().length === 0 && - Date.now() - persistedState.dismissedAt >= providerTipDismissalDuration, - ) - const [ref, setRef] = createSignal() - const presence = createPresence({ - show: visible, - element: () => ref() ?? null, - }) - const openProviders = () => { - void import("@/components/dialog-connect-provider").then(({ DialogConnectProvider }) => { - void dialog.show(() => sdk().directory} />) - }) - } - - return ( - -
-
- - - - -
-
-
- ) -} diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts deleted file mode 100644 index 2a79ae77fa..0000000000 --- a/packages/app/src/pages/new-session/new-session-workspace-controller.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { - normalizeNewSessionWorktree, - resolveNewSessionBranch, - resolveNewSessionWorktree, -} from "./new-session-workspace-controller" - -describe("new session workspace selection", () => { - test("uses main when the workspace bar is unavailable", () => { - expect( - resolveNewSessionWorktree({ - enabled: false, - selected: "/project/feature", - directory: "/project/feature", - projectWorktree: "/project", - }), - ).toBe("main") - }) - - test("derives an existing worktree from the current directory", () => { - expect( - resolveNewSessionWorktree({ enabled: true, directory: "/project/feature", projectWorktree: "/project" }), - ).toBe("/project/feature") - expect(resolveNewSessionWorktree({ enabled: true, directory: "/project", projectWorktree: "/project" })).toBe( - "main", - ) - }) - - test("normalizes main to the project root outside the main worktree", () => { - expect(normalizeNewSessionWorktree("main", "/project/feature", "/project")).toBe("/project") - expect(normalizeNewSessionWorktree("main", "/project", "/project")).toBe("main") - }) - - test("falls back to the local branch for main, create, and unknown worktrees", () => { - const branch = (worktree: string) => (worktree === "/project/feature" ? "feature" : undefined) - expect(resolveNewSessionBranch({ worktree: "main", local: "dev", worktreeBranch: branch })).toBe("dev") - expect(resolveNewSessionBranch({ worktree: "create", local: "dev", worktreeBranch: branch })).toBe("dev") - expect(resolveNewSessionBranch({ worktree: "/project/feature", local: "dev", worktreeBranch: branch })).toBe( - "feature", - ) - expect(resolveNewSessionBranch({ worktree: "/missing", local: "dev", worktreeBranch: branch })).toBe("dev") - }) -}) diff --git a/packages/app/src/pages/new-session/new-session-workspace-controller.ts b/packages/app/src/pages/new-session/new-session-workspace-controller.ts deleted file mode 100644 index f3fc9b2708..0000000000 --- a/packages/app/src/pages/new-session/new-session-workspace-controller.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createMemo, createSignal } from "solid-js" -import { useSDK } from "@/context/sdk" -import { useServerSync } from "@/context/server-sync" -import { useSync } from "@/context/sync" - -const workspaceBarEnabled = import.meta.env.VITE_OPENCODE_CHANNEL !== "prod" - -export function resolveNewSessionWorktree(input: { - enabled: boolean - selected?: string - directory: string - projectWorktree?: string -}) { - if (!input.enabled) return "main" - if (input.selected) return input.selected - if (input.projectWorktree && input.directory !== input.projectWorktree) return input.directory - return "main" -} - -export function normalizeNewSessionWorktree(value: string, directory: string, projectWorktree?: string) { - if (value === "main" && projectWorktree !== directory) return projectWorktree - return value -} - -export function resolveNewSessionBranch(input: { - worktree: string - local?: string - worktreeBranch: (worktree: string) => string | undefined -}) { - if (input.worktree === "main" || input.worktree === "create") return input.local - return input.worktreeBranch(input.worktree) ?? input.local -} - -export function createNewSessionWorkspaceController() { - const sdk = useSDK() - const sync = useSync() - const serverSync = useServerSync() - const [worktree, setWorktree] = createSignal() - const visible = createMemo(() => workspaceBarEnabled && sync().project?.vcs === "git") - const value = createMemo(() => - resolveNewSessionWorktree({ - enabled: visible(), - selected: worktree(), - directory: sdk().directory, - projectWorktree: sync().project?.worktree, - }), - ) - const projectRoot = createMemo(() => sync().project?.worktree ?? sdk().directory) - const localBranch = createMemo(() => serverSync().child(projectRoot())[0].vcs?.branch) - const branch = createMemo(() => - resolveNewSessionBranch({ - worktree: value(), - local: localBranch(), - worktreeBranch: (worktree) => serverSync().child(worktree)[0].vcs?.branch, - }), - ) - - return { - selection: { - value, - reset: () => setWorktree(), - set: (worktree: string) => - setWorktree(normalizeNewSessionWorktree(worktree, sdk().directory, sync().project?.worktree)), - }, - project: { - root: projectRoot, - workspaces: () => sync().project?.sandboxes ?? [], - git: () => sync().project?.vcs === "git", - }, - bar: { - visible, - branch, - }, - } -} - -export type NewSessionWorkspaceController = ReturnType diff --git a/packages/app/src/pages/new-session/use-new-session-commands.tsx b/packages/app/src/pages/new-session/use-new-session-commands.tsx deleted file mode 100644 index a0d835f97f..0000000000 --- a/packages/app/src/pages/new-session/use-new-session-commands.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useDialog } from "@opencode-ai/ui/context/dialog" -import { useSettingsCommand } from "@/components/settings-dialog" -import { useCommand } from "@/context/command" -import { useLanguage } from "@/context/language" - -export function useNewSessionCommands(input: { - restoreFocus: () => void - project: { - empty: () => boolean - open: () => void - } -}) { - const command = useCommand() - const dialog = useDialog() - const language = useLanguage() - - useSettingsCommand() - command.register("new-session", () => [ - { - id: "command.palette", - title: language.t("command.palette"), - hidden: true, - onSelect: async () => { - const { DialogSelectFile } = await import("@/components/dialog-select-file") - void dialog.show(() => ) - }, - }, - { - id: "input.focus", - title: language.t("command.input.focus"), - category: language.t("command.category.view"), - keybind: "ctrl+l", - onSelect: input.restoreFocus, - }, - { - id: "project.select", - title: language.t("session.new.project.search"), - category: language.t("command.category.project"), - keybind: "mod+shift+o", - disabled: input.project.empty(), - onSelect: input.project.open, - }, - ]) -} diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index c6e3a5fcbb..3ce2841676 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -6,7 +6,6 @@ import { batch, ErrorBoundary, onCleanup, - Suspense, Show, Match, Switch, @@ -533,6 +532,7 @@ export default function Page() { const info = createMemo(() => (params.id ? sync().session.get(params.id) : undefined)) const isChildSession = createMemo(() => !!info()?.parentID) + const diffs = createMemo(() => (params.id ? list(sync().data.session_diff[params.id]) : [])) const canReview = createMemo(() => !!sync().project) const reviewTab = createMemo(() => isDesktop()) const tabState = createSessionTabs({ @@ -690,8 +690,8 @@ export default function Page() { queryFn: mode ? () => sdk() - .api.vcs.diff({ location: { directory: sdk().directory }, mode: mode === "git" ? "working" : mode }) - .then((result) => result.data) + .client.vcs.diff({ mode }) + .then((result) => list(result.data)) .catch((error) => { console.debug("[session-review] failed to load vcs diff", { mode, error }) return [] @@ -738,12 +738,8 @@ export default function Page() { retry: 2, queryFn: () => sdk() - .api.vcs.diff({ - location: { directory: scope }, - mode: mode === "git" ? "working" : mode, - context, - }) - .then((result) => result.data), + .client.vcs.diff({ mode, directory: scope, context }) + .then((result) => result.data ?? []), }) .then((diffs) => diffs.find((diff) => diff.file === file)) @@ -950,11 +946,10 @@ export default function Page() { ) const stopVcs = sdk().event.listen((evt) => { - const details = evt.details as { type: string; properties?: unknown } - if (details.type !== "file.watcher.updated" && details.type !== "filesystem.changed") return + if (evt.details.type !== "file.watcher.updated") return const props = - typeof details.properties === "object" && details.properties - ? (details.properties as Record) + typeof evt.details.properties === "object" && evt.details.properties + ? (evt.details.properties as Record) : undefined const file = typeof props?.file === "string" ? props.file : undefined if (!file || file.startsWith(".git/")) return @@ -1469,6 +1464,44 @@ export default function Page() { requestAnimationFrame(() => attempt(0)) }) + createEffect(() => { + const id = params.id + if (!id) return + + if (!wantsReview()) return + if (sync().data.session_diff[id] !== undefined) return + if (sync().status === "loading") return + + void sync().session.diff(id) + }) + + createEffect( + on( + () => [sessionKey(), wantsReview()] as const, + ([key, wants]) => { + if (diffFrame !== undefined) cancelAnimationFrame(diffFrame) + if (diffTimer !== undefined) window.clearTimeout(diffTimer) + diffFrame = undefined + diffTimer = undefined + if (!wants) return + + const id = params.id + if (!id) return + if (!untrack(() => sync().data.session_diff[id] !== undefined)) return + + diffFrame = requestAnimationFrame(() => { + diffFrame = undefined + diffTimer = window.setTimeout(() => { + diffTimer = undefined + if (sessionKey() !== key) return + void sync().session.diff(id, { force: true }) + }, 0) + }) + }, + { defer: true }, + ), + ) + let treeDir: string | undefined createEffect(() => { const dir = sdk().directory @@ -1724,7 +1757,7 @@ export default function Page() { setFollowup("failed", input.sessionID, undefined) const ok = await sendFollowupDraft({ - api: sdk().api.session, + client: sdk().client, sync: sync(), serverSync: serverSync(), draft: item, @@ -1820,13 +1853,13 @@ export default function Page() { const halt = (sessionID: string) => busy(sessionID) ? sdk() - .api.session.interrupt({ sessionID }) + .client.session.abort({ sessionID }) .catch(() => {}) : Promise.resolve() const revertMutation = useMutation(() => ({ mutationFn: async (input: { sessionID: string; messageID: string }) => { - const session = sdk().api.session + const client = sdk().client const target = sync() const last = target.session.get(input.sessionID)?.revert const value = draft(input.messageID) @@ -1836,8 +1869,10 @@ export default function Page() { roll(input.sessionID, { messageID: input.messageID }, target) prompt.set(value) }, - request: () => halt(input.sessionID).then(() => session.revert.stage(input)), - complete: () => undefined, + request: () => halt(input.sessionID).then(() => client.session.revert(input)), + complete: (result) => { + if (result.data) merge(result.data, target) + }, rollback: () => roll(input.sessionID, last, target), fail, }) @@ -1849,7 +1884,7 @@ export default function Page() { const sessionID = params.id if (!sessionID) return - const session = sdk().api.session + const client = sdk().client const target = sync() const next = userMessages().find((item) => item.id > id) const last = target.session.get(sessionID)?.revert @@ -1866,9 +1901,11 @@ export default function Page() { }, request: () => !next - ? halt(sessionID).then(() => session.revert.clear({ sessionID })) - : halt(sessionID).then(() => session.revert.stage({ sessionID, messageID: next.id }).then(() => undefined)), - complete: () => undefined, + ? halt(sessionID).then(() => client.session.unrevert({ sessionID })) + : halt(sessionID).then(() => client.session.revert({ sessionID, messageID: next.id })), + complete: (result) => { + if (result.data) merge(result.data, target) + }, rollback: () => roll(sessionID, last, target), fail, }) @@ -2218,10 +2255,6 @@ export default function Page() { comments.clear() resumeScroll() }, - get edit() { - return editingFollowup() - }, - onEditLoaded: clearFollowupEdit, shouldQueue: queueEnabled, onQueue: queueFollowup, onAbort: () => { @@ -2230,7 +2263,14 @@ export default function Page() { setFollowup("paused", id, true) }, }) - return + return ( + + ) }}
} @@ -2298,53 +2338,52 @@ export default function Page() {
- - - +
- - hasReview() || reviewV2State.sidebarOpened()} - reviewCount={reviewCount} - reviewPanel={reviewPanelV2} - reviewSidebarToggle={(disabled) => ( - - )} - fileBrowserState={reviewV2State} - activeDiff={activeReviewFile()} - focusReviewDiff={focusReviewDiff} - reviewSnap={ui.reviewSnap} - size={size} - stacked={desktopV2PanelLayout().stacked} - /> - + hasReview() || reviewV2State.sidebarOpened()} + reviewCount={reviewCount} + reviewPanel={reviewPanelV2} + diffVersion={vcsQuery.dataUpdatedAt} + loadDiff={loadReviewDiff} + expandUnchanged={reviewV2State.expandMode() === "expand"} + reviewSidebarToggle={(disabled) => ( + + )} + fileBrowserState={reviewV2State} + activeDiff={activeReviewFile()} + focusReviewDiff={focusReviewDiff} + reviewSnap={ui.reviewSnap} + size={size} + stacked={desktopV2PanelLayout().stacked} + />
diff --git a/packages/app/src/pages/session/composer/session-composer-controls.ts b/packages/app/src/pages/session/composer/session-composer-controls.ts index a9b0070bc0..f52b7f4b42 100644 --- a/packages/app/src/pages/session/composer/session-composer-controls.ts +++ b/packages/app/src/pages/session/composer/session-composer-controls.ts @@ -25,39 +25,34 @@ export function createPromptInputController(input: { }) { const layout = useLayout() const local = useLocal() - const sdk = useSDK() + const providers = useProviders() const sync = useSync() - const providers = useProviders(() => sdk().directory) + const sdk = useSDK() const view = layout.view(input.sessionKey) const agentsQuery = createQuery(() => input.queryOptions.agents(pathKey(sdk().directory))) const globalProvidersQuery = createQuery(() => input.queryOptions.providers(null)) const providersQuery = createQuery(() => input.queryOptions.providers(pathKey(sdk().directory))) - return createMemo(() => { - return { - agents: { - available: sync().data.agent, - options: local.agent.list().map((agent) => agent.name), - current: local.agent.current()?.name ?? "", - loading: agentsQuery.isLoading, - visible: local.agent.visible(), - select: local.agent.set, - }, - model: { - selection: input.model ?? local.model, - paid: providers.paid().length > 0, - loading: - (local.agent.visible() && agentsQuery.isLoading) || - providersQuery.isLoading || - globalProvidersQuery.isLoading, - }, - session: { - id: input.sessionID(), - tabs: layout.tabs(input.sessionKey), - reviewPanel: view.reviewPanel, - }, - } - }) + return createMemo(() => ({ + agents: { + available: sync().data.agent, + options: local.agent.list().map((agent) => agent.name), + current: local.agent.current()?.name ?? "", + loading: agentsQuery.isLoading, + visible: local.agent.visible(), + select: local.agent.set, + }, + model: { + selection: input.model ?? local.model, + paid: providers.paid().length > 0, + loading: agentsQuery.isLoading || providersQuery.isLoading || globalProvidersQuery.isLoading, + }, + session: { + id: input.sessionID(), + tabs: layout.tabs(input.sessionKey), + reviewPanel: view.reviewPanel, + }, + })) } export function createPromptProjectControls() { diff --git a/packages/app/src/pages/session/composer/session-composer-state.ts b/packages/app/src/pages/session/composer/session-composer-state.ts index f54e0c9e4f..45f5e4cb26 100644 --- a/packages/app/src/pages/session/composer/session-composer-state.ts +++ b/packages/app/src/pages/session/composer/session-composer-state.ts @@ -82,7 +82,7 @@ export function createSessionComposerController(options?: { closeMs?: number | ( setStore("responding", perm.id) sdk() - .api.permission.reply({ sessionID: perm.sessionID, requestID: perm.id, reply: response }) + .client.permission.respond({ sessionID: perm.sessionID, permissionID: perm.id, response }) .catch((err: unknown) => { const description = err instanceof Error ? err.message : String(err) showToast({ title: language.t("common.requestFailed"), description }) diff --git a/packages/app/src/pages/session/composer/session-question-dock.tsx b/packages/app/src/pages/session/composer/session-question-dock.tsx index 941424e247..445a9f47a0 100644 --- a/packages/app/src/pages/session/composer/session-question-dock.tsx +++ b/packages/app/src/pages/session/composer/session-question-dock.tsx @@ -223,8 +223,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit } const replyMutation = useMutation(() => ({ - mutationFn: (answers: QuestionAnswer[]) => - sdk().api.question.reply({ sessionID: props.request.sessionID, requestID: props.request.id, answers }), + mutationFn: (answers: QuestionAnswer[]) => sdk().client.question.reply({ requestID: props.request.id, answers }), onMutate: () => { props.onSubmit() }, @@ -236,7 +235,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit })) const rejectMutation = useMutation(() => ({ - mutationFn: () => sdk().api.question.reject({ sessionID: props.request.sessionID, requestID: props.request.id }), + mutationFn: () => sdk().client.question.reject({ requestID: props.request.id }), onMutate: () => { props.onSubmit() }, diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index b67b810a4b..688714fcf1 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -1,4 +1,4 @@ -import { createEffect, createMemo, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js" +import { createEffect, createMemo, createResource, createSignal, Match, on, onCleanup, Show, Switch } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" import { makeEventListener } from "@solid-primitives/event-listener" @@ -8,6 +8,7 @@ import { cloneSelectedLineRange, previewSelectedLines } from "@opencode-ai/sessi import { createLineCommentController } from "@opencode-ai/session-ui/line-comment-annotations" import { createLineCommentControllerV2 } from "@opencode-ai/session-ui/v2/line-comment-annotations-v2" import { sampledChecksum } from "@opencode-ai/core/util/encode" +import { normalize, text } from "@opencode-ai/session-ui/session-diff" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { IconButton } from "@opencode-ai/ui/icon-button" import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" @@ -18,14 +19,20 @@ import { showToast } from "@/utils/toast" import { selectionFromLines, useFile, type FileSelection, type SelectedLineRange } from "@/context/file" import { useComments } from "@/context/comments" import { useLanguage } from "@/context/language" +import { useLayout } from "@/context/layout" import { usePrompt } from "@/context/prompt" import { useSettings } from "@/context/settings" import { getSessionHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionTabs } from "@/pages/session/helpers" +import { reviewDiffNeedsLoad, type RenderDiff } from "@/pages/session/v2/review-diff-kinds" type SessionFileViewProps = { tab: string + diff?: RenderDiff + diffVersion?: number + loadDiff?: (path: string, version?: number) => Promise + expandUnchanged?: boolean } const selectionSide = (range: SelectedLineRange) => range.endSide ?? range.side ?? "additions" @@ -215,10 +222,25 @@ export function FileTabContent(props: { tab: string }) { export function SessionFileView(props: SessionFileViewProps) { const settings = useSettings() + const detailSource = createMemo(() => { + if (!props.diff || !props.loadDiff || !reviewDiffNeedsLoad(props.diff)) return + return { diff: props.diff, load: props.loadDiff, version: props.diffVersion } + }) + const [loadedDiff] = createResource(detailSource, async ({ diff, load, version }) => { + const value = await load(diff.file, version) + if (value?.file !== diff.file) return + return { source: diff, version, value } + }) + const diff = createMemo(() => { + const source = props.diff + if (!source) return + const loaded = loadedDiff() + return normalize(loaded?.source === source && loaded.version === props.diffVersion ? loaded.value : source) + }) return ( }> - + ) } @@ -508,11 +530,12 @@ function SessionFileViewV1(props: { tab: string }) { return content() } -function SessionFileViewV2(props: { tab: string }) { +function SessionFileViewV2(props: { tab: string; diff?: ReturnType; expandUnchanged?: boolean }) { const file = useFile() const comments = useComments() const language = useLanguage() const prompt = usePrompt() + const layout = useLayout() const fileComponent = useFileComponent() const { sessionKey, tabs, view } = useSessionLayout() const activeFileTab = createSessionTabs({ @@ -556,7 +579,12 @@ function SessionFileViewV2(props: { tab: string }) { } const buildPreview = (filePath: string, lines: SelectedLineRange) => { - const source = filePath === path() ? contents() : file.get(filePath)?.content?.content + const source = + filePath === path() + ? props.diff + ? text(props.diff, selectionSide(lines)) + : contents() + : file.get(filePath)?.content?.content if (!source) return undefined return selectionPreview(source, selectionFromLines(lines)) } @@ -733,12 +761,21 @@ function SessionFileViewV2(props: { tab: string }) {
{ @@ -786,6 +824,7 @@ function SessionFileViewV2(props: { tab: string }) {
+ {renderFile(contents())} {renderFile(contents())}
{language.t("common.loading")}...
diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index 1b65af7121..3854bf0276 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -1,7 +1,6 @@ import { createEffect, onCleanup, type JSX } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { SessionReview } from "@opencode-ai/session-ui/session-review" import type { SessionReviewCommentActions, @@ -15,7 +14,7 @@ import type { LineComment } from "@/context/comments" export type DiffStyle = "unified" | "split" -type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type ReviewDiff = SnapshotFileDiff | VcsFileDiff export interface SessionReviewTabProps { title?: JSX.Element diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 6f074fa203..571428e29f 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -24,7 +24,6 @@ import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { KeybindV2 } from "@opencode-ai/ui/v2/keybind-v2" import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd" import { useDialog } from "@opencode-ai/ui/context/dialog" @@ -57,23 +56,24 @@ import { setSessionHandoff } from "@/pages/session/handoff" import { useSessionLayout } from "@/pages/session/session-layout" import { SessionFileBrowserTab, type SessionFileBrowserState } from "@/pages/session/v2/session-file-browser-tab" -type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff -type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff -const FILE_TREE_WIDTH_MIN = 240 +type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff -function renderDiff(value: ReviewDiff): value is RenderDiff { +function renderDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff { return typeof value.file === "string" } export function SessionSidePanel(props: { canReview: () => boolean - diffs: () => ReviewDiff[] + diffs: () => (SnapshotFileDiff | VcsFileDiff)[] diffsReady: () => boolean empty: () => string hasReview: () => boolean reviewHasFocusableContent: () => boolean reviewCount: () => number reviewPanel: () => JSX.Element + diffVersion?: number + loadDiff?: (path: string, version?: number) => Promise + expandUnchanged?: boolean reviewSidebarToggle?: (disabled: boolean) => JSX.Element fileBrowserState?: SessionFileBrowserState activeDiff?: string @@ -91,6 +91,11 @@ export function SessionSidePanel(props: { const sdk = useSDK() const { sessionKey, tabs, view, params } = useSessionLayout() const projectDirectory = createMemo(() => sdk().directory) + const diffForTab = (tab: string) => { + const path = file.pathFromTab(tab) + if (!path) return + return props.diffs().find((diff): diff is RenderDiff => renderDiff(diff) && diff.file === path) + } const isDesktop = createMediaQuery("(min-width: 768px)") const shown = settings.visibility.fileTree @@ -105,14 +110,13 @@ export function SessionSidePanel(props: { }), ) const open = createMemo(() => reviewOpen() || fileOpen()) - const fileTreeWidth = createMemo(() => Math.max(FILE_TREE_WIDTH_MIN, layout.fileTree.width())) const reviewTab = createMemo(() => isDesktop()) const panelWidth = createMemo(() => { if (!open()) return "0px" if (reviewOpen()) return "auto" - return `${fileTreeWidth()}px` + return `${layout.fileTree.width()}px` }) - const treeWidth = createMemo(() => (fileOpen() ? `${fileTreeWidth()}px` : "0px")) + const treeWidth = createMemo(() => (fileOpen() ? `${layout.fileTree.width()}px` : "0px")) const diffs = createMemo(() => props.diffs().filter(renderDiff)) const diffFiles = createMemo(() => diffs().map((d) => d.file)) @@ -743,6 +747,10 @@ export function SessionSidePanel(props: { active={file.pathFromTab(browserTab() ?? activeFileTab() ?? "")} kinds={kinds()} state={props.fileBrowserState!} + diff={diffForTab(browserTab() ?? activeFileTab() ?? "")} + diffVersion={props.diffVersion} + loadDiff={props.loadDiff} + expandUnchanged={props.expandUnchanged} onSelect={(path) => previewTab(file.tab(path))} onSelectPermanent={(path) => openTab(file.tab(path))} filterRef={(element) => (fileFilter = element)} @@ -847,8 +855,8 @@ export function SessionSidePanel(props: { { props.size.touch() diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index eca1541cea..2959e9f8a9 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -283,14 +283,6 @@ export function MessageTimeline(props: { return sync().data.session_status[id] ?? idle }) const sessionMessages = createMemo(() => (sessionID() ? (sync().data.message[sessionID()!] ?? []) : [])) - const projectedMessages = createMemo(() => { - const id = sessionID() - if (!id) return [] - const visible = new Set(props.userMessages.map((message) => message.id)) - const boundary = sessionMessages().find((message) => message.role === "user" && !visible.has(message.id))?.id - const messages = sync().data.session_message[id] ?? [] - return boundary ? messages.filter((message) => message.id < boundary) : messages - }) const info = createMemo(() => { const id = sessionID() if (!id) return @@ -333,7 +325,6 @@ export function MessageTimeline(props: { const projection = createTimelineProjection({ messages: sessionMessages, userMessages: () => props.userMessages, - sessionMessages: projectedMessages, parts: getMsgParts, status: sessionStatus, showReasoningSummaries: settings.general.showReasoningSummaries, @@ -646,7 +637,7 @@ export function MessageTimeline(props: { const viewShare = () => { const url = shareUrl() if (!url) return - platform.openExternal(url) + platform.openLink(url) } const errorMessage = (err: unknown) => { @@ -674,7 +665,7 @@ export function MessageTimeline(props: { const titleMutation = useMutation(() => ({ mutationFn: (input: { id: string; title: string }) => - sdk().api.session.rename({ sessionID: input.id, title: input.title }), + sdk().client.session.update({ sessionID: input.id, title: input.title }), onSuccess: (_, input) => { sync().set( produce((draft) => { @@ -812,14 +803,13 @@ export function MessageTimeline(props: { const archiveSession = async (sessionID: string) => { const session = sync().session.get(sessionID) if (!session) return - if ((await sdk().protocol) !== "v1") return const sessions = sync().data.session ?? [] const index = sessions.findIndex((s) => s.id === sessionID) const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) await sdk() - .client.session.update({ sessionID, directory: sdk().directory, time: { archived: Date.now() } }) + .client.session.update({ sessionID, time: { archived: Date.now() } }) .then(() => { sync().set( produce((draft) => { @@ -848,8 +838,8 @@ export function MessageTimeline(props: { const nextSession = index === -1 ? undefined : (sessions[index + 1] ?? sessions[index - 1]) const result = await sdk() - .api.session.remove({ sessionID }) - .then(() => true) + .client.session.delete({ sessionID }) + .then((x) => x.data) .catch((err) => { showToast({ title: language.t("session.delete.failed.title"), diff --git a/packages/app/src/pages/session/timeline/projection.ts b/packages/app/src/pages/session/timeline/projection.ts index e30c936d73..ea8ea4f132 100644 --- a/packages/app/src/pages/session/timeline/projection.ts +++ b/packages/app/src/pages/session/timeline/projection.ts @@ -1,15 +1,16 @@ -import type { SessionMessageInfo } from "@opencode-ai/client/promise" +import { Binary } from "@opencode-ai/core/util/binary" import type { AssistantMessage, Message, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" -import { createMemo, type Accessor } from "solid-js" +import { createMemo, mapArray, type Accessor } from "solid-js" import { reuseTimelineRows } from "./row-reconciliation" import { Timeline, TimelineRow } from "./rows" export { reuseTimelineRows } from "./row-reconciliation" +const emptyAssistantMessages: AssistantMessage[] = [] + export function createTimelineProjection(input: { messages: Accessor userMessages: Accessor - sessionMessages: Accessor parts: (messageID: string) => Part[] status: Accessor showReasoningSummaries: Accessor @@ -29,20 +30,47 @@ export function createTimelineProjection(input: { }) return result }) - const projection = createMemo(() => - Timeline.constructSessionMessageRows( - input.sessionMessages(), - (messageID) => messageByID().get(messageID) as UserMessage | AssistantMessage | undefined, - input.parts, - input.showReasoningSummaries(), - input.status().type, - input.inlineComments(), - input.userMessages(), + const activeMessageID = createMemo(() => { + const parentID = input + .messages() + .findLast( + (message): message is AssistantMessage => + message.role === "assistant" && typeof message.time.completed !== "number", + )?.parentID + if (parentID) { + const messages = input.messages() + const result = Binary.search(messages, parentID, (message) => message.id) + const message = result.found ? messages[result.index] : messages.find((item) => item.id === parentID) + if (message?.role === "user") return message.id + } + + if (input.status().type === "idle") return + return input.messages().findLast((message) => message.role === "user")?.id + }) + const messageRowMemos = createMemo( + mapArray(input.userMessages, (userMessage, indexAccessor) => + createMemo((previous: TimelineRow.TimelineRow[] | undefined) => + reuseTimelineRows( + previous, + Timeline.constructMessageRows( + userMessage, + input.parts, + assistantMessagesByParent().get(userMessage.id) ?? emptyAssistantMessages, + indexAccessor(), + input.showReasoningSummaries(), + input.status().type, + activeMessageID() === userMessage.id, + input.inlineComments(), + ), + ), + ), ), ) - const activeMessageID = createMemo(() => projection().activeMessageID) const rows = createMemo((previous: TimelineRow.TimelineRow[] | undefined) => - reuseTimelineRows(previous, projection().rows), + reuseTimelineRows( + previous, + messageRowMemos().flatMap((memo) => memo()), + ), ) const rowByKey = createMemo(() => new Map(rows().map((row) => [TimelineRow.key(row), row] as const))) const messageRowIndex = createMemo(() => { diff --git a/packages/app/src/pages/session/timeline/rows-current.test.ts b/packages/app/src/pages/session/timeline/rows-current.test.ts deleted file mode 100644 index a5952a5e66..0000000000 --- a/packages/app/src/pages/session/timeline/rows-current.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { describe, expect, mock, test } from "bun:test" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" -import { normalizeSessionMessages } from "@/utils/session-message" - -mock.module("@opencode-ai/session-ui/message-part", () => ({ - renderable: () => true, - groupParts: (refs: Array<{ messageID: string; part: { id: string } }>) => - refs.map((ref) => ({ - type: "part" as const, - key: ref.part.id, - ref: { messageID: ref.messageID, partID: ref.part.id }, - })), -})) - -const { Timeline, TimelineRow } = await import("./rows") - -describe("current session timeline rows", () => { - test("derives turns and tagged rows from chronological current messages", () => { - const source = [ - { id: "msg_1", type: "user", text: "first", time: { created: 1 } }, - { - id: "msg_2", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "answer" }], - time: { created: 2, completed: 3 }, - }, - { id: "msg_3", type: "user", text: "second", time: { created: 4 } }, - { - id: "msg_4", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "reasoning", text: "working" }], - time: { created: 5 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "busy", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.activeMessageID).toBe("msg_3") - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_1", - "assistant-part:msg_1:msg_2:text:0", - "turn-gap:msg_3", - "user-message:msg_3", - "assistant-part:msg_3:msg_4:reasoning:0", - ]) - }) - - test("renders a current shell message as a standalone turn", () => { - const source = [ - { - id: "msg_shell", - type: "shell", - shellID: "shell_1", - command: "pwd", - status: "exited", - exit: 0, - output: { output: "/repo", cursor: 5, size: 5, truncated: false }, - time: { created: 1, completed: 2 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "idle", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.activeMessageID).toBe("msg_shell") - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_shell", - "assistant-part:msg_shell:msg_shell:tool", - ]) - }) - - test("keeps a projected parent missing from the source page before newer turns", () => { - const source = [ - { id: "msg_user_1", type: "user", text: "first question", time: { created: 1 } }, - { - id: "msg_assistant_1", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "first answer" }], - time: { created: 2, completed: 3 }, - }, - { id: "msg_user_2", type: "user", text: "second question", time: { created: 4 } }, - { - id: "msg_assistant_2", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "second answer" }], - time: { created: 5, completed: 6 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source.slice(1), - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "idle", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_user_1", - "assistant-part:msg_user_1:msg_assistant_1:text:0", - "turn-gap:msg_user_2", - "user-message:msg_user_2", - "assistant-part:msg_user_2:msg_assistant_2:text:0", - ]) - }) - - test("renders an optimistic user turn and thinking before the protocol message arrives", () => { - const source = [ - { id: "msg_1", type: "user", text: "existing", time: { created: 1 } }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const optimistic = { - id: "msg_2", - sessionID: "ses_1", - role: "user" as const, - time: { created: 2 }, - agent: "build", - model: { modelID: "model", providerID: "provider" }, - } - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => - messageID === optimistic.id ? optimistic : normalized.messages.find((message) => message.id === messageID), - () => [], - true, - "busy", - true, - [...normalized.messages.filter((message) => message.role === "user"), optimistic], - ) - - expect(result.activeMessageID).toBe(optimistic.id) - expect(result.rows.map(TimelineRow.key)).toEqual([ - "user-message:msg_1", - "turn-gap:msg_2", - "user-message:msg_2", - "thinking:msg_2", - ]) - }) - - test("removes a failed assistant error when the turn continues streaming", () => { - const source = [ - { id: "msg_user", type: "user", text: "recover", time: { created: 1 } }, - { - id: "msg_failed", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [], - error: { type: "ProviderError", message: "temporary failure" }, - time: { created: 2, completed: 3 }, - }, - { - id: "msg_recovery", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "streaming again" }], - time: { created: 4 }, - }, - ] satisfies SessionMessageInfo[] - const normalized = normalizeSessionMessages("ses_1", source) - const messages = new Map(normalized.messages.map((message) => [message.id, message])) - - const result = Timeline.constructSessionMessageRows( - source, - (messageID) => messages.get(messageID), - (messageID) => normalized.parts.get(messageID) ?? [], - true, - "busy", - true, - normalized.messages.filter((message) => message.role === "user"), - ) - - expect(result.rows.map((row) => row._tag)).toEqual(["UserMessage", "AssistantPart"]) - }) -}) diff --git a/packages/app/src/pages/session/timeline/rows.ts b/packages/app/src/pages/session/timeline/rows.ts index 879646e86a..a9c9a2d288 100644 --- a/packages/app/src/pages/session/timeline/rows.ts +++ b/packages/app/src/pages/session/timeline/rows.ts @@ -1,5 +1,4 @@ import { parseCommentNote, readCommentMetadata } from "@/utils/comment-note" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" import { AssistantMessage, Part, SessionStatus, UserMessage } from "@opencode-ai/sdk/v2" import { groupParts, renderable, type PartGroup } from "@opencode-ai/session-ui/message-part" import { TimelineRow, type SummaryDiff } from "./timeline-row" @@ -32,71 +31,6 @@ export type TimelineRowMap = { } export namespace Timeline { - export function constructSessionMessageRows( - messages: SessionMessageInfo[], - getMessage: (messageID: string) => UserMessage | AssistantMessage | undefined, - getMessageParts: (messageID: string) => Part[], - showReasoning: boolean, - status: SessionStatus["type"], - inlineComments: boolean, - projectedUserMessages: UserMessage[], - ) { - const turns: { user: UserMessage; assistants: AssistantMessage[] }[] = [] - const turnByUserID = new Map() - messages.forEach((message) => { - const projected = getMessage(message.id) - if (message.type === "shell" && projected?.role === "user") { - const assistant = getMessage(`${message.id}:assistant`) - const turn = { user: projected, assistants: assistant?.role === "assistant" ? [assistant] : [] } - turns.push(turn) - turnByUserID.set(projected.id, turn) - return - } - if (projected?.role === "user") { - if (turnByUserID.has(projected.id)) return - const turn = { user: projected, assistants: [] } - turns.push(turn) - turnByUserID.set(projected.id, turn) - return - } - if (projected?.role !== "assistant") return - const existing = turnByUserID.get(projected.parentID) - if (existing) { - existing.assistants.push(projected) - return - } - const user = getMessage(projected.parentID) - if (user?.role !== "user") return - const turn = { user, assistants: [projected] } - turns.push(turn) - turnByUserID.set(user.id, turn) - }) - const latestUserMessageID = turns.at(-1)?.user.id - projectedUserMessages.forEach((user) => { - if (turnByUserID.has(user.id)) return - if (latestUserMessageID && user.id < latestUserMessageID) return - const turn = { user, assistants: [] } - turns.push(turn) - turnByUserID.set(user.id, turn) - }) - const activeMessageID = turns.at(-1)?.user.id - return { - activeMessageID, - rows: turns.flatMap((turn, index) => - constructMessageRows( - turn.user, - getMessageParts, - turn.assistants, - index, - showReasoning, - status, - turn.user.id === activeMessageID, - inlineComments, - ), - ), - } - } - export function constructMessageRows( userMessage: UserMessage, getMessageParts: (messageID: string) => Part[], @@ -116,8 +50,7 @@ export namespace Timeline { const compaction = userParts.some((p) => p.type === "compaction") const interruptedMessageIndex = assistantMessages.findIndex((m) => m.error?.name === "MessageAbortedError") const interrupted = interruptedMessageIndex !== -1 - const latestError = assistantMessages.at(-1)?.error - const error = latestError?.name === "MessageAbortedError" ? undefined : latestError + const error = assistantMessages.find((m) => m.error && m.error.name !== "MessageAbortedError")?.error const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) => getMessageParts(message.id) diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 12dd96a5e6..275e6ec4bc 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -5,6 +5,7 @@ import { previewSelectedLines } from "@opencode-ai/session-ui/pierre/selection-b import { useFile, selectionFromLines, type FileSelection, type SelectedLineRange } from "@/context/file" import { useLanguage } from "@/context/language" import { useLayout } from "@/context/layout" +import { useLocal } from "@/context/local" import { usePermission } from "@/context/permission" import { usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" @@ -18,7 +19,6 @@ import { extractPromptFromParts } from "@/utils/prompt" import { UserMessage } from "@opencode-ai/sdk/v2" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionOwnership } from "./session-ownership" -import { useLocal } from "@/context/local" export type SessionCommandContext = { navigateMessageByOffset: (offset: number) => void @@ -40,6 +40,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const dialog = useDialog() const file = useFile() const language = useLanguage() + const local = useLocal() const permission = usePermission() const prompt = usePrompt() const sdk = useSDK() @@ -47,7 +48,6 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sync = useSync() const terminal = useTerminal() const layout = useLayout() - const local = useLocal() const navigate = useNavigate() const { params, sessionKey, tabs, view } = useSessionLayout() const sessionOwnership = createSessionOwnership(sessionKey) @@ -306,7 +306,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sessionID = params.id if (!sessionID) return const owner = sessionOwnership.capture() - const session = sdk().api.session + const client = sdk().client const directory = sdk().directory const promptSession = prompt.capture() const revert = info()?.revert?.messageID @@ -316,13 +316,13 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const parts = sync().data.part[message.id] if (sync().data.session_working(sessionID)) { - await session.interrupt({ sessionID }).catch(() => {}) + await client.session.abort({ sessionID }).catch(() => {}) } await runCommand({ owner, prompt: promptSession, - request: () => session.revert.stage({ sessionID, messageID: message.id }), + request: () => client.session.revert({ sessionID, messageID: message.id }), updatePrompt: (promptSession) => { if (parts) promptSession.set(extractPromptFromParts(parts, { directory })) }, @@ -334,7 +334,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { const sessionID = params.id if (!sessionID) return const owner = sessionOwnership.capture() - const session = sdk().api.session + const client = sdk().client const messages = userMessages() const promptSession = prompt.capture() @@ -346,7 +346,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { await runCommand({ owner, prompt: promptSession, - request: () => session.revert.clear({ sessionID }), + request: () => client.session.unrevert({ sessionID }), updatePrompt: (promptSession) => promptSession.reset(), updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id >= revertMessageID)), }) @@ -356,7 +356,7 @@ export const useSessionCommands = (actions: SessionCommandContext) => { await runCommand({ owner, prompt: promptSession, - request: () => session.revert.stage({ sessionID, messageID: next.id }), + request: () => client.session.revert({ sessionID, messageID: next.id }), updatePrompt: () => undefined, updateViewport: () => setActiveMessage(findLast(messages, (x) => x.id < next.id)), }) @@ -375,9 +375,10 @@ export const useSessionCommands = (actions: SessionCommandContext) => { return } - await sdk().api.session.compact({ + await sdk().client.session.summarize({ sessionID, - model: { providerID: model.provider.id, modelID: model.id }, + modelID: model.id, + providerID: model.provider.id, }) } diff --git a/packages/app/src/pages/session/v2/review-diff-kinds.ts b/packages/app/src/pages/session/v2/review-diff-kinds.ts index d3adb1f2ff..49cec334bc 100644 --- a/packages/app/src/pages/session/v2/review-diff-kinds.ts +++ b/packages/app/src/pages/session/v2/review-diff-kinds.ts @@ -1,15 +1,14 @@ import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { Kind } from "@/components/file-tree-v2" import { normalizeFileTreeV2Path } from "@/components/file-tree-v2-model" -export type RenderDiff = FileDiffInfo | (SnapshotFileDiff & { file: string }) | VcsFileDiff +export type RenderDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff export function normalizePath(p: string) { return normalizeFileTreeV2Path(p) } -export function filterRenderableDiff(value: FileDiffInfo | SnapshotFileDiff | VcsFileDiff): value is RenderDiff { +export function filterRenderableDiff(value: SnapshotFileDiff | VcsFileDiff): value is RenderDiff { return typeof value.file === "string" } diff --git a/packages/app/src/pages/session/v2/review-panel-v2.tsx b/packages/app/src/pages/session/v2/review-panel-v2.tsx index fcd6bbb79f..4f0cf612e1 100644 --- a/packages/app/src/pages/session/v2/review-panel-v2.tsx +++ b/packages/app/src/pages/session/v2/review-panel-v2.tsx @@ -1,6 +1,5 @@ import { createMemo, createResource, createSignal, Show, type JSX } from "solid-js" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX, SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN, @@ -31,7 +30,7 @@ import { import type { ReviewPanelV2State } from "@/pages/session/v2/review-panel-v2-state" import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2" -type ReviewDiff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type ReviewDiff = SnapshotFileDiff | VcsFileDiff export type ReviewPanelV2Props = { title?: JSX.Element diff --git a/packages/app/src/pages/session/v2/session-file-browser-tab.tsx b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx index 639429e80b..6862f295b9 100644 --- a/packages/app/src/pages/session/v2/session-file-browser-tab.tsx +++ b/packages/app/src/pages/session/v2/session-file-browser-tab.tsx @@ -11,6 +11,7 @@ import { useSDK } from "@/context/sdk" import { displayName } from "@/pages/layout/helpers" import { useSessionLayout } from "@/pages/session/session-layout" import { SessionFileView } from "@/pages/session/file-tabs" +import type { RenderDiff } from "@/pages/session/v2/review-diff-kinds" import { applyFileListKeyDown, SessionFileListV2 } from "@/pages/session/v2/session-file-list-v2" import { pathKey } from "@/utils/path-key" @@ -30,6 +31,10 @@ export function SessionFileBrowserTab(props: { active?: string kinds: ReadonlyMap state: SessionFileBrowserState + diff?: RenderDiff + diffVersion?: number + loadDiff?: (path: string, version?: number) => Promise + expandUnchanged?: boolean onSelect: (path: string) => void onSelectPermanent: (path: string) => void filterRef?: (element: HTMLInputElement) => void @@ -172,7 +177,15 @@ export function SessionFileBrowserTab(props: { >
- {(tab) => } + {(tab) => ( + + )}
diff --git a/packages/app/src/utils/diffs.test.ts b/packages/app/src/utils/diffs.test.ts index a3d25f4279..5fbca469b7 100644 --- a/packages/app/src/utils/diffs.test.ts +++ b/packages/app/src/utils/diffs.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test" import type { SnapshotFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { Message } from "@opencode-ai/sdk/v2/client" import { diffs, message } from "./diffs" @@ -10,7 +9,7 @@ const item = { additions: 1, deletions: 1, status: "modified", -} satisfies FileDiffInfo & SnapshotFileDiff +} satisfies SnapshotFileDiff describe("diffs", () => { test("keeps valid arrays", () => { diff --git a/packages/app/src/utils/diffs.ts b/packages/app/src/utils/diffs.ts index a8eec75a9a..0cb2504fbe 100644 --- a/packages/app/src/utils/diffs.ts +++ b/packages/app/src/utils/diffs.ts @@ -1,8 +1,7 @@ import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { Message } from "@opencode-ai/sdk/v2/client" -type Diff = FileDiffInfo | SnapshotFileDiff | VcsFileDiff +type Diff = SnapshotFileDiff | VcsFileDiff function diff(value: unknown): value is Diff { if (!value || typeof value !== "object" || Array.isArray(value)) return false diff --git a/packages/app/src/utils/draft-store.ts b/packages/app/src/utils/draft-store.ts deleted file mode 100644 index cd0895f52c..0000000000 --- a/packages/app/src/utils/draft-store.ts +++ /dev/null @@ -1,171 +0,0 @@ -import type { AsyncStorage } from "@solid-primitives/storage" - -export type BlobReference = { id: string; url: string } - -type Driver = { - get(key: string): Promise - set(key: string, value: string): Promise - remove(key: string): Promise - putBlob(blob: Blob): Promise - getBlob(id: string): Promise -} - -export type DraftStore = AsyncStorage & { putBlob(blob: Blob): Promise } -const urls = new Map() - -function blobUrl(id: string, blob: Blob) { - const existing = urls.get(id) - if (existing) return existing - const url = URL.createObjectURL(blob) - urls.set(id, url) - return url -} - -async function blobID(blob: Blob) { - const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await blob.arrayBuffer()))) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") - return id -} - -export async function createBlobReference(blob: Blob): Promise { - const id = await blobID(blob) - return { id, url: blobUrl(id, blob) } -} - -export function createDraftStore(driver: Driver): DraftStore { - const versions = new Map() - const putBlob = async (blob: Blob) => { - const id = await driver.putBlob(blob) - return { id, url: blobUrl(id, blob) } - } - const encode = async (value: unknown): Promise => { - if (Array.isArray(value)) return Promise.all(value.map(encode)) - if (!value || typeof value !== "object") return value - const item = value as Record - if (item.type === "image" && typeof item.dataUrl === "string") { - const blob = await fetch(item.dataUrl).then((response) => response.blob()) - const { dataUrl: _, ...rest } = item - return { ...rest, blob: { id: await driver.putBlob(blob) } } - } - if ("blob" in item && item.blob && typeof item.blob === "object") { - const blob = item.blob as Record - if (typeof blob.id === "string" && blob.id.startsWith("data:")) { - const data = await fetch(blob.id).then((response) => response.blob()) - return { ...item, blob: { id: await driver.putBlob(data) } } - } - return { ...item, blob: { id: blob.id } } - } - return Object.fromEntries( - await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await encode(entry)])), - ) - } - const decode = async (value: unknown): Promise => { - if (Array.isArray(value)) return Promise.all(value.map(decode)) - if (!value || typeof value !== "object") return value - const item = value as Record - if (item.blob && typeof item.blob === "object") { - const ref = item.blob as Record - if (typeof ref.id === "string") { - const blob = await driver.getBlob(ref.id) - if (blob) return { ...item, blob: { id: ref.id, url: blobUrl(ref.id, blob) } } - } - } - return Object.fromEntries( - await Promise.all(Object.entries(item).map(async ([key, entry]) => [key, await decode(entry)])), - ) - } - return { - getItem: async (key) => { - const value = await driver.get(key) - return value === null ? null : JSON.stringify(await decode(JSON.parse(value))) - }, - setItem: async (key, value) => { - const version = (versions.get(key) ?? 0) + 1 - versions.set(key, version) - const encoded = JSON.stringify(await encode(JSON.parse(value))) - if (versions.get(key) === version) await driver.set(key, encoded) - }, - removeItem: async (key) => { - versions.set(key, (versions.get(key) ?? 0) + 1) - await driver.remove(key) - }, - putBlob, - } -} - -export function createBrowserDraftStore(): DraftStore { - const request = indexedDB.open("opencode-drafts", 1) - request.addEventListener("upgradeneeded", () => { - request.result.createObjectStore("documents") - request.result.createObjectStore("blobs") - }) - const db = new Promise((resolve, reject) => { - request.addEventListener("success", () => { - const database = request.result - const transaction = database.transaction(["documents", "blobs"], "readwrite") - const documents = transaction.objectStore("documents").getAll() - documents.addEventListener("success", () => { - const used = new Set() - JSON.parse(`[${documents.result.join(",")}]`, (_key, item) => { - if (item?.blob && typeof item.blob.id === "string") used.add(item.blob.id) - return item - }) - const blobs = transaction.objectStore("blobs").openKeyCursor() - blobs.addEventListener("success", () => { - const cursor = blobs.result - if (!cursor) return - if (!used.has(String(cursor.key))) cursor.delete() - cursor.continue() - }) - }) - transaction.addEventListener("complete", () => resolve(database)) - transaction.addEventListener("abort", () => resolve(database)) - }) - request.addEventListener("error", () => reject(request.error)) - }) - const get = async (store: string, key: string) => { - const result = (await db).transaction(store).objectStore(store).get(key) - return new Promise((resolve, reject) => { - result.addEventListener("success", () => resolve(result.result)) - result.addEventListener("error", () => reject(result.error)) - }) - } - const write = async (store: string, key: string, value?: unknown) => { - const transaction = (await db).transaction(store, "readwrite") - if (value === undefined) transaction.objectStore(store).delete(key) - else transaction.objectStore(store).put(value, key) - return new Promise((resolve, reject) => { - transaction.addEventListener("complete", () => resolve()) - transaction.addEventListener("error", () => reject(transaction.error)) - }) - } - return createDraftStore({ - get: async (key) => ((await get("documents", key)) as string | undefined) ?? null, - set: (key, value) => write("documents", key, value), - remove: (key) => write("documents", key), - putBlob: async (blob) => { - const id = await blobID(blob) - await write("blobs", id, blob) - return id - }, - getBlob: async (id) => ((await get("blobs", id)) as Blob | undefined) ?? null, - }) -} - -export async function blobDataUrl(blob: BlobReference, mime: string) { - const data = await fetch(blob.url).then((response) => response.blob()) - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.addEventListener("error", () => reject(reader.error)) - reader.addEventListener("load", () => { - const value = typeof reader.result === "string" ? reader.result : "" - resolve(`data:${mime};base64,${value.slice(value.indexOf(",") + 1)}`) - }) - reader.readAsDataURL(data) - }) -} - -export function createLegacyBlobReference(dataUrl: string): BlobReference { - return { id: dataUrl, url: dataUrl } -} diff --git a/packages/app/src/utils/menu-dismiss-controller.ts b/packages/app/src/utils/menu-dismiss-controller.ts deleted file mode 100644 index 0a3009eb71..0000000000 --- a/packages/app/src/utils/menu-dismiss-controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** Coordinates focus restoration and actions that must run after menu content unmounts. */ -export function createMenuDismissController(content: () => HTMLElement | undefined) { - let restoreTrigger = true - - return { - /** Allows the menu primitive to restore focus to its trigger when closing. */ - allowTriggerRestore() { - restoreTrigger = true - }, - /** Keeps focus at its current or next destination instead of returning it to the trigger. */ - preventTriggerRestore() { - restoreTrigger = false - }, - /** Applies the current restoration policy during the menu primitive's close-focus event. */ - onCloseAutoFocus(event: Event) { - if (!restoreTrigger) event.preventDefault() - }, - /** Runs an action after the menu unmounts and its focus-close work has settled. */ - afterClose(callback: () => void) { - const complete = () => { - if (content()?.isConnected) { - requestAnimationFrame(complete) - return - } - requestAnimationFrame(() => requestAnimationFrame(callback)) - } - requestAnimationFrame(complete) - }, - } -} diff --git a/packages/app/src/utils/notification-click.test.ts b/packages/app/src/utils/notification-click.test.ts new file mode 100644 index 0000000000..fa81b0e025 --- /dev/null +++ b/packages/app/src/utils/notification-click.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { handleNotificationClick, setNavigate } from "./notification-click" + +describe("notification click", () => { + afterEach(() => { + setNavigate(undefined as any) + }) + + test("navigates via registered navigate function", () => { + const calls: string[] = [] + setNavigate((href) => calls.push(href)) + handleNotificationClick("/abc/session/123") + expect(calls).toEqual(["/abc/session/123"]) + }) + + test("does not navigate when href is missing", () => { + const calls: string[] = [] + setNavigate((href) => calls.push(href)) + handleNotificationClick(undefined) + expect(calls).toEqual([]) + }) + + test("falls back to location.assign without registered navigate", () => { + handleNotificationClick("/abc/session/123") + // falls back to window.location.assign — no error thrown + }) +}) diff --git a/packages/app/src/utils/notification-click.ts b/packages/app/src/utils/notification-click.ts new file mode 100644 index 0000000000..316b278206 --- /dev/null +++ b/packages/app/src/utils/notification-click.ts @@ -0,0 +1,13 @@ +let nav: ((href: string) => void) | undefined + +export const setNavigate = (fn: (href: string) => void) => { + nav = fn +} + +export const handleNotificationClick = (href?: string) => { + window.focus() + if (!href) return + if (nav) return nav(href) + console.warn("notification-click: navigate function not set, falling back to window.location.assign") + window.location.assign(href) +} diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index a7742c399e..a2daae4866 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -15,7 +15,6 @@ type PersistedWithReady = [ ] type PersistTarget = { - draft?: boolean storage?: string scope?: "window" legacyStorageNames?: string[] @@ -296,14 +295,6 @@ async function removeAsync(storage: AsyncStorage, key: string) { } catch {} } -function toAsyncStorage(storage: SyncStorage | AsyncStorage): AsyncStorage { - return { - getItem: async (key) => storage.getItem(key), - setItem: async (key, value) => storage.setItem(key, value), - removeItem: async (key) => storage.removeItem(key), - } -} - async function migrateLegacyAsync(input: { current: AsyncStorage legacyStore?: AsyncStorage @@ -522,9 +513,6 @@ export const Persist = { if (session) return Persist.serverSession(scope, dir, session, key, legacy) return Persist.serverWorkspace(scope, dir, key, legacy) }, - prompt(target: PersistTarget): PersistTarget { - return { ...target, draft: true } - }, } function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget { @@ -538,12 +526,9 @@ function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget } export function removePersisted( - target: { draft?: boolean; storage?: string; legacyStorageNames?: string[]; key: string }, + target: { storage?: string; legacyStorageNames?: string[]; key: string }, platform?: Platform, ) { - if (target.draft && platform?.draftStore) { - void platform.draftStore.removeItem(`${target.storage ?? "default"}:${target.key}`) - } const isDesktop = platform?.platform === "desktop" && !!platform.storage if (isDesktop) { @@ -576,17 +561,8 @@ export function persisted( const legacy = config.legacy ?? [] const isDesktop = platform.platform === "desktop" && !!platform.storage - const draft = config.draft ? platform.draftStore : undefined const currentStorage = (() => { - if (draft) { - const prefix = `${config.storage ?? "default"}:` - return { - getItem: (key: string) => draft.getItem(prefix + key), - setItem: (key: string, value: string) => draft.setItem(prefix + key, value), - removeItem: (key: string) => draft.removeItem(prefix + key), - } satisfies AsyncStorage - } if (isDesktop) return platform.storage?.(config.storage) if (!config.storage) return localStorageDirect() return localStorageWithPrefix(config.storage) @@ -601,7 +577,7 @@ export function persisted( const legacyStorageNames = config.legacyStorageNames ?? [] const storage = (() => { - if (!isDesktop && !draft) { + if (!isDesktop) { const current = currentStorage as SyncStorage const legacyStore = legacyStorage as SyncStorage const legacyStores = legacyStorageNames.map(localStorageWithPrefix) @@ -633,26 +609,15 @@ export function persisted( const current = currentStorage as AsyncStorage const legacyStore = legacyStorage as AsyncStorage | undefined - const oldCurrent = draft - ? isDesktop - ? platform.storage?.(config.storage) - : config.storage - ? localStorageWithPrefix(config.storage) - : localStorageDirect() - : undefined - const legacyStores = [ - oldCurrent, - ...legacyStorageNames.map((name) => (isDesktop ? platform.storage?.(name) : localStorageWithPrefix(name))), - ] + const legacyStores = legacyStorageNames + .map((name) => platform.storage?.(name) as AsyncStorage | undefined) .filter((x) => !!x) - .map(toAsyncStorage) - let draftLatest: string | undefined const api: AsyncStorage = { getItem: async (key) => { const value = await readCurrentAsync({ storage: current, key, defaults, migrate: config.migrate }) if (value !== undefined) return value - const migrated = await migrateLegacyAsync({ + return migrateLegacyAsync({ current, legacyStore, stores: legacyStores, @@ -661,15 +626,8 @@ export function persisted( defaults, migrate: config.migrate, }) - if (draftLatest === undefined) { - if (draft && migrated !== null) return (await current.getItem(key)) ?? migrated - return migrated - } - await current.setItem(key, draftLatest) - return draftLatest }, setItem: async (key, value) => { - if (draft) draftLatest = value await current.setItem(key, value) }, removeItem: async (key) => { diff --git a/packages/app/src/utils/prompt.test.ts b/packages/app/src/utils/prompt.test.ts index 8b86d43b90..1ecaf02c97 100644 --- a/packages/app/src/utils/prompt.test.ts +++ b/packages/app/src/utils/prompt.test.ts @@ -37,18 +37,8 @@ describe("extractPromptFromParts", () => { expect(result).toHaveLength(3) expect(result[0]).toMatchObject({ type: "text", content: "check these" }) expect(result.slice(1)).toMatchObject([ - { - type: "image", - filename: "a.png", - mime: "image/png", - blob: expect.objectContaining({ id: expect.any(String) }), - }, - { - type: "image", - filename: "b.pdf", - mime: "application/pdf", - blob: expect.objectContaining({ id: expect.any(String) }), - }, + { type: "image", filename: "a.png", mime: "image/png", dataUrl: "data:image/png;base64,AAA" }, + { type: "image", filename: "b.pdf", mime: "application/pdf", dataUrl: "data:application/pdf;base64,BBB" }, ]) }) }) diff --git a/packages/app/src/utils/prompt.ts b/packages/app/src/utils/prompt.ts index 67d32086bb..35aec0071a 100644 --- a/packages/app/src/utils/prompt.ts +++ b/packages/app/src/utils/prompt.ts @@ -1,6 +1,5 @@ import type { AgentPart as MessageAgentPart, FilePart, Part, TextPart } from "@opencode-ai/sdk/v2" import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" -import { createLegacyBlobReference } from "@/utils/draft-store" type Inline = | { @@ -108,7 +107,7 @@ export function extractPromptFromParts(parts: Part[], opts?: { directory?: strin id: filePart.id, filename: filePart.filename ?? attachmentName, mime: filePart.mime, - blob: createLegacyBlobReference(filePart.url), + dataUrl: filePart.url, }) } } diff --git a/packages/app/src/utils/server-compat.test.ts b/packages/app/src/utils/server-compat.test.ts deleted file mode 100644 index 52e5ec6e3b..0000000000 --- a/packages/app/src/utils/server-compat.test.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createApiForServer, createSdkForServer } from "./server" -import { createCompatibleApi } from "./server-compat" - -function setup( - protocol: "v1" | "v2" | Promise<"v1" | "v2">, - responses?: { vcs?: { branch: string; default_branch: string } }, -) { - const requests: Request[] = [] - const fetcher = Object.assign( - async (input: string | URL | Request, init?: RequestInit) => { - const request = new Request(input, init) - requests.push(request) - if (request.method === "PATCH") { - return Response.json({ - id: "ses_1", - slug: "ses_1", - projectID: "project", - directory: "/repo", - title: "Session", - version: "1", - time: { created: 1, updated: 1 }, - }) - } - if (request.method === "POST" && request.url.endsWith("/prompt_async")) - return new Response(undefined, { status: 204 }) - if (request.method === "POST" && request.url.endsWith("/prompt")) { - return Response.json({ - admittedSeq: 1, - id: "msg_1", - sessionID: "ses_1", - timeCreated: 1, - type: "user", - data: { text: "hello" }, - delivery: "steer", - }) - } - if (request.method === "GET" && new URL(request.url).pathname === "/vcs") - return Response.json(responses?.vcs ?? {}) - if (request.method === "GET") return Response.json([]) - return new Response(undefined, { status: 204 }) - }, - { preconnect: globalThis.fetch.preconnect }, - ) - const server = { url: "http://localhost:4096" } - const api = createCompatibleApi({ - protocol: typeof protocol === "string" ? Promise.resolve(protocol) : protocol, - current: createApiForServer({ server, fetch: fetcher }), - legacy: (directory) => createSdkForServer({ server, fetch: fetcher, directory, throwOnError: true }), - directory: "/repo", - }) - return { api, requests } -} - -describe("createCompatibleApi", () => { - /* - test("routes V1 archive through the legacy session update", async () => { - const { api, requests } = setup("v1") - await api.session.archive({ sessionID: "ses_1", directory: "/repo" }) - - const url = new URL(requests[0]!.url) - expect(url.pathname).toBe("/session/ses_1") - expect(requests[0]!.headers.get("x-opencode-directory")).toBe("%2Frepo") - expect(requests[0]!.method).toBe("PATCH") - expect(await requests[0]!.json()).toMatchObject({ time: { archived: expect.any(Number) } }) - }) - */ - - test("converts current prompts to the V1 prompt contract", async () => { - const { api, requests } = setup("v1") - await api.session.prompt({ - sessionID: "ses_1", - id: "msg_1", - text: "hello @src/index.ts", - agent: "build", - model: { providerID: "provider", modelID: "model" }, - files: [ - { uri: "file:///repo/src/index.ts", name: "index.ts", mention: { text: "@src/index.ts", start: 6, end: 19 } }, - { uri: "data:text/plain;base64,aGVsbG8=", name: "notes.txt" }, - ], - }) - - expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/prompt_async") - const body = await requests[0]!.json() - expect(body).toMatchObject({ - messageID: "msg_1", - agent: "build", - model: { providerID: "provider", modelID: "model" }, - parts: [ - { type: "text", text: "hello @src/index.ts" }, - { - type: "file", - mime: "text/plain", - url: "file:///repo/src/index.ts", - filename: "index.ts", - source: { - type: "file", - text: { value: "@src/index.ts", start: 6, end: 19 }, - path: "file:///repo/src/index.ts", - }, - }, - { - type: "file", - mime: "text/plain", - url: "data:text/plain;base64,aGVsbG8=", - filename: "notes.txt", - }, - ], - }) - expect(body.parts[2]).not.toHaveProperty("source") - }) - - test("preserves original parts for V1 optimistic reconciliation", async () => { - const { api, requests } = setup("v1") - await api.session.prompt({ - sessionID: "ses_1", - id: "msg_1", - text: "look", - files: [{ uri: "data:image/png;base64,AAAA", name: "image.png" }], - legacyParts: [ - { id: "prt_text", type: "text", text: "look" }, - { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" }, - ], - }) - - expect((await requests[0]!.json()).parts).toEqual([ - { id: "prt_text", type: "text", text: "look" }, - { id: "prt_image", type: "file", mime: "image/png", url: "data:image/png;base64,AAAA", filename: "image.png" }, - ]) - }) - - test("resolves protocol detection once across implementation methods", async () => { - let detections = 0 - const resolved = Promise.resolve<"v1" | "v2">("v2") - const protocol = new Proxy(resolved, { - get(target, property) { - if (property !== "then") return Reflect.get(target, property, target) - detections++ - return target.then.bind(target) - }, - }) - const { api } = setup(protocol) - - await api.session.list() - await api.session.list() - - expect(detections).toBe(1) - }) - - /* - test("keeps V2 session actions on the current API", async () => { - const { api, requests } = setup("v2") - await api.session.archive({ sessionID: "ses_1" }) - - expect(new URL(requests[0]!.url).pathname).toBe("/api/session/ses_1/archive") - expect(requests[0]!.method).toBe("POST") - }) - */ - - test("uses the global V1 session search endpoint", async () => { - const { api, requests } = setup("v1") - await api.session.list({ parentID: null, search: "session", limit: 50 }) - - expect(new URL(requests[0]!.url).pathname).toBe("/experimental/session") - }) - - /* - test("projects the V1 default branch", async () => { - const { api } = setup("v1", { vcs: { branch: "feature", default_branch: "dev" } }) - - expect(await api.vcs.get({ location: { directory: "/repo" } })).toMatchObject({ - data: { branch: "feature", defaultBranch: "dev" }, - }) - }) - */ - - test("translates current file searches to the V1 dirs parameter", async () => { - const { api, requests } = setup("v1") - await api.file.find({ location: { directory: "/repo" }, query: "src", type: "file", limit: 20 }) - - const url = new URL(requests[0]!.url) - expect(url.pathname).toBe("/find/file") - expect(url.searchParams.get("dirs")).toBe("false") - expect(url.searchParams.get("limit")).toBe("20") - }) - - test("routes V1 permission replies through the requested directory", async () => { - const { api, requests } = setup("v1") - await api.permission.reply({ - sessionID: "ses_1", - requestID: "permission_1", - reply: "once", - location: { directory: "/other" }, - }) - - expect(new URL(requests[0]!.url).pathname).toBe("/session/ses_1/permissions/permission_1") - expect(new URL(requests[0]!.url).searchParams.get("directory")).toBe("/other") - }) - - test("disposes the V1 instance after connecting a provider", async () => { - const { api, requests } = setup("v1") - - await api.integration.connect.key({ - integrationID: "openrouter", - key: "secret", - location: { directory: "/repo" }, - }) - - expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ - "/auth/openrouter", - "/instance/dispose", - "/instance/dispose", - ]) - expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") - expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() - }) - - test("disposes the V1 instance after completing provider OAuth", async () => { - const { api, requests } = setup("v1") - - await api.integration.oauth.complete({ - integrationID: "openrouter", - attemptID: "openrouter:0", - code: "code", - location: { directory: "/repo" }, - }) - - expect(requests.map((request) => new URL(request.url).pathname)).toEqual([ - "/provider/openrouter/oauth/callback", - "/instance/dispose", - "/instance/dispose", - ]) - expect(requests[1]!.headers.get("x-opencode-directory")).toBe("%2Frepo") - expect(requests[2]!.headers.get("x-opencode-directory")).toBeNull() - }) -}) diff --git a/packages/app/src/utils/server-compat.ts b/packages/app/src/utils/server-compat.ts deleted file mode 100644 index 1df1338b71..0000000000 --- a/packages/app/src/utils/server-compat.ts +++ /dev/null @@ -1,518 +0,0 @@ -import type { ServerApi } from "./server" -import type { ServerProtocol } from "./server-protocol" -import type { AgentPartInput, FilePartInput, OpencodeClient, Session, TextPartInput } from "@opencode-ai/sdk/v2/client" -import type { - Project, - ProjectCurrent, - SessionApi, - SessionCommandInput, - SessionCommandOutput, - SessionCompactInput, - SessionCompactOutput, - SessionInfo, - SessionPromptInput, - SessionPromptOutput, - SessionShellInput, - SessionShellOutput, -} from "@opencode-ai/client/promise" - -type LegacyClient = OpencodeClient -type LegacyFor = (directory?: string) => LegacyClient -type CompatibleSessionApi = Omit< - SessionApi, - "prompt" | "command" | "shell" | "compact" | "rename" | "archive" | "remove" -> & { - prompt: (input: SessionPromptInput & LegacyPrompt) => Promise - command: (input: SessionCommandInput) => Promise - shell: (input: SessionShellInput & LegacyPrompt) => Promise - compact: (input: SessionCompactInput & { model?: LegacyPrompt["model"] }) => Promise - rename: (input: Parameters[0] & LegacyLocation) => ReturnType - // archive: (input: Parameters[0] & LegacyLocation) => ReturnType - remove: (input: Parameters[0] & LegacyLocation) => ReturnType -} -type CompatiblePermissionApi = Omit & { - reply: ( - input: Parameters[0] & { location?: { directory?: string } }, - ) => ReturnType -} -export type CompatibleApi = Omit & { - readonly session: CompatibleSessionApi - readonly permission: CompatiblePermissionApi -} -type LegacyPrompt = { - agent?: string - model?: { providerID: string; modelID: string } - variant?: string - legacyParts?: (TextPartInput | FilePartInput | AgentPartInput)[] -} -type LegacyLocation = { directory?: string } -type CompatibleInput = { - protocol: Promise - current: ServerApi - legacy: LegacyFor - directory?: string -} - -function mime(uri: string) { - const match = /^data:([^;,]+)/.exec(uri) - return match?.[1] ?? "application/octet-stream" -} - -function sessionInfo(session: Session): SessionInfo { - return { - id: session.id, - parentID: session.parentID, - projectID: session.projectID, - agent: session.agent, - model: session.model && { - id: session.model.id, - providerID: session.model.providerID, - variant: session.model.variant, - }, - cost: session.cost ?? 0, - tokens: session.tokens ?? { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: session.time, - title: session.title, - location: { directory: session.directory, workspaceID: session.workspaceID }, - subpath: session.path, - revert: session.revert && { - messageID: session.revert.messageID, - partID: session.revert.partID, - snapshot: session.revert.snapshot, - }, - } -} - -export function createCompatibleApi(input: CompatibleInput): CompatibleApi { - const v1 = createV1Api(input) - return lazyApi( - input.protocol.then((protocol) => (protocol === "v1" ? v1 : input.current)), - input.current, - ) -} - -function lazyApi(implementation: Promise, shape: T): T { - const cache = new Map() - return new Proxy(shape, { - get(target, property, receiver) { - const sample = Reflect.get(target, property, receiver) - if (typeof sample === "function") { - return (...args: unknown[]) => - implementation.then((value) => { - const method = Reflect.get(value, property) - if (typeof method !== "function") throw new Error(`API method unavailable: ${String(property)}`) - return Reflect.apply(method, value, args) - }) - } - if (sample === null || typeof sample !== "object") return sample - if (cache.has(property)) return cache.get(property) - const nested = lazyApi( - implementation.then((value) => { - const result = Reflect.get(value, property) - if (result === null || typeof result !== "object") { - throw new Error(`API namespace unavailable: ${String(property)}`) - } - return result - }), - sample, - ) - cache.set(property, nested) - return nested - }, - }) -} - -function createV1Api(input: CompatibleInput): CompatibleApi { - const directory = (location?: { directory?: string }) => location?.directory ?? input.directory - const legacy = (location?: { directory?: string }) => input.legacy(directory(location)) - const located = (data: T, value?: { directory?: string }) => ({ - location: { - directory: directory(value) ?? "", - project: { id: "", directory: directory(value) ?? "" }, - }, - data, - }) - - return { - ...input.current, - session: { - ...input.current.session, - async list( - value?: Parameters[0], - options?: Parameters[1], - ) { - if (!value?.directory && value?.search !== undefined) { - const result = await legacy().experimental.session.list( - { - roots: value.parentID === null ? true : undefined, - search: value.search, - limit: value.limit, - }, - options, - ) - return { data: (result.data ?? []).map(sessionInfo), cursor: {} } - } - const result = await legacy({ directory: value?.directory }).session.list({ - directory: value?.directory, - roots: value?.parentID === null ? true : undefined, - search: value?.search, - limit: value?.limit, - }) - return { data: (result.data ?? []).map(sessionInfo), cursor: {} } - }, - async create(value?: Parameters[0]) { - const result = await legacy(value?.location ?? undefined).session.create({ - directory: directory(value?.location ?? undefined), - }) - if (!result.data) throw new Error("Failed to create session") - return sessionInfo(result.data) - }, - async get(value: Parameters[0]) { - const result = await legacy().session.get(value) - if (!result.data) throw new Error(`Session not found: ${value.sessionID}`) - return sessionInfo(result.data) - }, - async active() { - const result = await legacy().session.status() - return Object.fromEntries( - Object.entries(result.data ?? {}).flatMap(([sessionID, status]) => - status.type === "idle" ? [] : [[sessionID, { type: "running" as const }]], - ), - ) - }, - async rename(value: Parameters[0] & LegacyLocation) { - await legacy(value).session.update({ sessionID: value.sessionID, title: value.title }) - }, - // async archive(value: Parameters[0] & LegacyLocation) { - // await legacy(value).session.update({ sessionID: value.sessionID, time: { archived: Date.now() } }) - // }, - async remove(value: Parameters[0] & LegacyLocation) { - await legacy(value).session.delete(value) - }, - async fork(value: Parameters[0]) { - const result = await legacy().session.fork(value) - if (!result.data) throw new Error("Failed to fork session") - return sessionInfo(result.data) - }, - async interrupt(value: Parameters[0]) { - await legacy().session.abort(value) - }, - async prompt(value: SessionPromptInput & LegacyPrompt) { - await legacy().session.promptAsync({ - sessionID: value.sessionID, - messageID: value.id ?? undefined, - agent: value.agent, - model: value.model, - variant: value.variant, - parts: value.legacyParts ?? [ - { type: "text", text: value.text }, - ...(value.files ?? []).map((file) => ({ - type: "file" as const, - mime: file.mention ? "text/plain" : mime(file.uri), - url: file.uri, - filename: file.name, - source: file.mention - ? { - type: "file" as const, - text: { value: file.mention.text, start: file.mention.start, end: file.mention.end }, - path: file.uri, - } - : undefined, - })), - ...(value.agents ?? []).map((agent) => ({ - type: "agent" as const, - name: agent.name, - source: agent.mention - ? { value: agent.mention.text, start: agent.mention.start, end: agent.mention.end } - : undefined, - })), - ], - }) - return { - admittedSeq: 0, - id: value.id ?? "", - sessionID: value.sessionID, - timeCreated: Date.now(), - type: "user", - data: { text: value.text }, - delivery: value.delivery ?? "steer", - } - }, - async command(value: SessionCommandInput) { - await legacy().session.command({ - sessionID: value.sessionID, - messageID: value.id ?? undefined, - command: value.command, - arguments: value.arguments ?? "", - agent: value.agent ?? undefined, - model: value.model ? `${value.model.providerID}/${value.model.id}` : undefined, - variant: value.model?.variant, - parts: value.files?.map((file) => ({ - type: "file" as const, - mime: mime(file.uri), - url: file.uri, - filename: file.name, - })), - }) - return { - admittedSeq: 0, - id: value.id ?? "", - sessionID: value.sessionID, - timeCreated: Date.now(), - type: "user", - data: { text: `/${value.command} ${value.arguments ?? ""}`.trim() }, - delivery: value.delivery ?? "steer", - } - }, - async shell(value: SessionShellInput & LegacyPrompt) { - await legacy().session.shell({ - sessionID: value.sessionID, - command: value.command, - agent: value.agent, - model: value.model, - }) - }, - compact: async (value: SessionCompactInput & { model?: LegacyPrompt["model"] }) => { - if (!value.model) throw new Error("A model is required to compact a V1 session") - await legacy().session.summarize({ - sessionID: value.sessionID, - providerID: value.model.providerID, - modelID: value.model.modelID, - }) - return { - admittedSeq: 0, - id: value.id ?? "", - sessionID: value.sessionID, - timeCreated: Date.now(), - type: "compaction", - } - }, - revert: { - stage: async (value: Parameters[0]) => { - await legacy().session.revert(value) - return { messageID: value.messageID } - }, - clear: async (value: Parameters[0]) => { - await legacy().session.unrevert(value) - }, - commit: input.current.session.revert.commit, - }, - }, - project: { - ...input.current.project, - async list() { - return ((await legacy().project.list()).data ?? []) as Project[] - }, - async current(value?: Parameters[0]) { - const result = await legacy(value?.location).project.current() - if (!result.data) throw new Error("Project not found") - return { id: result.data.id, directory: result.data.worktree } satisfies ProjectCurrent - }, - // async update(value: Parameters[0]) { - // const project = (await legacy().project.list()).data?.find((item) => item.id === value.projectID) - // const result = await legacy({ directory: project?.worktree }).project.update({ - // ...value, - // directory: project?.worktree, - // }) - // if (!result.data) throw new Error(`Project not found: ${value.projectID}`) - // return result.data as Project - // }, - async directories(value: Parameters[0]) { - const result = await legacy(value.location).worktree.list() - return (result.data ?? []).map((item) => ({ directory: item })) - }, - }, - // path: { - // ...input.current.path, - // async get(value?: Parameters[0]) { - // const result = await legacy(value?.location).path.get() - // if (!result.data) throw new Error("Path unavailable") - // return result.data - // }, - // }, - vcs: { - ...input.current.vcs, - // async get(value?: Parameters[0]) { - // const result = await legacy(value?.location).vcs.get() - // return located({ branch: result.data?.branch, defaultBranch: result.data?.default_branch }, value?.location) - // }, - async status(value?: Parameters[0]) { - const result = await legacy(value?.location).vcs.status() - return located(result.data ?? [], value?.location) - }, - async diff(value: Parameters[0]) { - const result = await legacy(value.location).vcs.diff({ - mode: value.mode === "working" ? "git" : value.mode, - context: value.context, - }) - return located( - (result.data ?? []).map((file) => ({ - file: file.file, - patch: file.patch ?? "", - additions: file.additions, - deletions: file.deletions, - status: file.status ?? "modified", - })), - value.location, - ) - }, - }, - file: { - ...input.current.file, - async list(value?: Parameters[0]) { - const result = await legacy(value?.location).file.list({ path: value?.path ?? "" }) - return located(result.data ?? [], value?.location) - }, - async find(value: Parameters[0]) { - const result = await legacy(value.location).find.files({ - query: value.query, - dirs: value.type === undefined ? undefined : value.type === "directory" ? "true" : "false", - limit: value.limit, - }) - return located( - (result.data ?? []).map((path) => ({ path, type: value.type ?? "file" })), - value.location, - ) - }, - }, - integration: { - ...input.current.integration, - async get(value: Parameters[0]) { - const methods = ((await legacy(value.location).provider.auth()).data?.[value.integrationID] ?? []).map( - (method, index) => - method.type === "api" - ? { type: "key" as const, label: method.label } - : { type: "oauth" as const, id: String(index), label: method.label, prompts: method.prompts }, - ) - return located( - { - id: value.integrationID, - name: value.integrationID, - methods, - connections: [], - }, - value.location, - ) - }, - connect: { - ...input.current.integration.connect, - key: async (value: Parameters[0]) => { - await legacy(value.location).auth.set({ - providerID: value.integrationID, - auth: { type: "api", key: value.key }, - }) - await legacy(value.location).instance.dispose() - await input.legacy().instance.dispose() - }, - }, - oauth: { - ...input.current.integration.oauth, - connect: async (value: Parameters[0]) => { - const method = Number(value.methodID) - const result = await legacy(value.location).provider.oauth.authorize( - { providerID: value.integrationID, method, inputs: value.inputs }, - { throwOnError: true }, - ) - if (!result.data) throw new Error("Failed to start OAuth authorization") - return located( - { - attemptID: `${value.integrationID}:${method}`, - url: result.data.url, - instructions: result.data.instructions, - mode: result.data.method, - time: { created: Date.now(), expires: Date.now() + 10 * 60 * 1000 }, - }, - value.location, - ) - }, - complete: async (value: Parameters[0]) => { - const method = Number(value.attemptID.split(":").at(-1)) - await legacy(value.location).provider.oauth.callback( - { providerID: value.integrationID, method, code: value.code }, - { throwOnError: true }, - ) - await legacy(value.location).instance.dispose() - await input.legacy().instance.dispose() - }, - status: async (value: Parameters[0]) => { - const method = Number(value.attemptID.split(":").at(-1)) - await legacy(value.location).provider.oauth.callback( - { providerID: value.integrationID, method }, - { throwOnError: true }, - ) - await legacy(value.location).instance.dispose() - await input.legacy().instance.dispose() - return located( - { status: "complete" as const, time: { created: Date.now(), expires: Date.now() } }, - value.location, - ) - }, - }, - }, - pty: { - ...input.current.pty, - // async shells(value?: Parameters[0]) { - // return located((await legacy(value?.location).pty.shells()).data ?? [], value?.location) - // }, - async list(value?: Parameters[0]) { - return located((await legacy(value?.location).pty.list()).data ?? [], value?.location) - }, - async create(value?: Parameters[0]) { - const result = await legacy(value?.location).pty.create({ - command: value?.command, - args: value?.args ? [...value.args] : undefined, - cwd: value?.cwd, - title: value?.title, - env: value?.env, - }) - if (!result.data) throw new Error("Failed to create terminal") - return located(result.data, value?.location) - }, - async get(value: Parameters[0]) { - const result = await legacy(value.location).pty.get({ ptyID: value.ptyID }) - if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) - return located(result.data, value.location) - }, - async update(value: Parameters[0]) { - const result = await legacy(value.location).pty.update({ - ptyID: value.ptyID, - title: value.title, - size: value.size, - }) - if (!result.data) throw new Error(`Terminal not found: ${value.ptyID}`) - return located(result.data, value.location) - }, - async remove(value: Parameters[0]) { - await legacy(value.location).pty.remove({ ptyID: value.ptyID }) - }, - // async connectToken(value: Parameters[0]) { - // const result = await legacy(value.location).pty.connectToken({ ptyID: value.ptyID }) - // if (!result.data) throw new Error(`Failed to connect terminal: ${value.ptyID}`) - // return located(result.data, value.location) - // }, - }, - permission: { - ...input.current.permission, - async reply(value: Parameters[0] & { location?: { directory?: string } }) { - await legacy(value.location).permission.respond({ - sessionID: value.sessionID, - permissionID: value.requestID, - response: value.reply, - directory: directory(value.location), - }) - }, - }, - question: { - ...input.current.question, - async reply(value: Parameters[0]) { - await legacy().question.reply({ - requestID: value.requestID, - answers: value.answers.map((answer) => [...answer]), - }) - }, - async reject(value: Parameters[0]) { - await legacy().question.reject({ requestID: value.requestID }) - }, - }, - } -} diff --git a/packages/app/src/utils/server-health.test.ts b/packages/app/src/utils/server-health.test.ts index 69a8c7b3be..b1c8f2c7e2 100644 --- a/packages/app/src/utils/server-health.test.ts +++ b/packages/app/src/utils/server-health.test.ts @@ -14,45 +14,15 @@ function abortFromInput(input: RequestInfo | URL, init?: RequestInit) { describe("checkServerHealth", () => { test("returns healthy response with version", async () => { - let request: URL | undefined - const fetch = (async (input: RequestInfo | URL) => { - request = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) - return new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { + const fetch = (async () => + new Response(JSON.stringify({ healthy: true, version: "1.2.3" }), { status: 200, headers: { "content-type": "application/json" }, - }) - }) as unknown as typeof globalThis.fetch + })) as unknown as typeof globalThis.fetch const result = await checkServerHealth(server, fetch) expect(result).toEqual({ healthy: true, version: "1.2.3" }) - expect(request?.pathname).toBe("/api/health") - }) - - test("falls back to the V1 health endpoint", async () => { - const paths: string[] = [] - const fetch = (async (input: RequestInfo | URL) => { - const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) - paths.push(url.pathname) - if (url.pathname === "/api/health") return new Response(undefined, { status: 404 }) - return Response.json({ healthy: true, version: "1.18.4" }) - }) as unknown as typeof globalThis.fetch - - expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) - expect(paths).toEqual(["/api/health", "/global/health"]) - }) - - test("falls back when the current health response is malformed", async () => { - const paths: string[] = [] - const fetch = (async (input: RequestInfo | URL) => { - const url = input instanceof URL ? input : new URL(input instanceof Request ? input.url : input) - paths.push(url.pathname) - if (url.pathname === "/api/health") return Response.json({}) - return Response.json({ healthy: true, version: "1.18.4" }) - }) as unknown as typeof globalThis.fetch - - expect(await checkServerHealth(server, fetch)).toEqual({ healthy: true, version: "1.18.4" }) - expect(paths).toEqual(["/api/health", "/global/health"]) }) test("allows slow servers thirty seconds by default", async () => { @@ -172,7 +142,7 @@ describe("checkServerHealth", () => { retryDelayMs: 1, }) - expect(count).toBe(6) + expect(count).toBe(3) expect(result).toEqual({ healthy: false }) }) }) diff --git a/packages/app/src/utils/server-health.ts b/packages/app/src/utils/server-health.ts index 1d7d9e4b2e..1b684d9af7 100644 --- a/packages/app/src/utils/server-health.ts +++ b/packages/app/src/utils/server-health.ts @@ -1,7 +1,6 @@ import { usePlatform } from "@/context/platform" import { ServerConnection } from "@/context/server" -import { authTokenFromCredentials, createSdkForServer } from "./server" -import { ClientError, OpenCode } from "@opencode-ai/client" +import { createSdkForServer } from "./server" import { Accessor, createEffect, onCleanup } from "solid-js" import { createStore, reconcile } from "solid-js/store" @@ -62,7 +61,6 @@ function wait(ms: number, signal?: AbortSignal) { function retryable(error: unknown, signal?: AbortSignal) { if (signal?.aborted) return false - if (error instanceof ClientError) return error.reason === "Transport" if (!(error instanceof Error)) return false if (error.name === "AbortError" || error.name === "TimeoutError") return false if (error instanceof TypeError) return true @@ -84,31 +82,15 @@ export async function checkServerHealth( .then(() => attempt(count + 1)) .catch(() => ({ healthy: false })) } - const attempt = async (count: number): Promise => { - const current = await OpenCode.make({ - baseUrl: server.url, + const attempt = (count: number): Promise => + createSdkForServer({ + server, fetch, - headers: server.password - ? { - Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, - } - : undefined, + signal, }) - .health.get({ signal }) - .then((x) => - typeof x.healthy === "boolean" - ? { data: { healthy: x.healthy, version: x.version } } - : { error: new Error("Invalid health response") }, - ) - .catch((error) => ({ error })) - if ("data" in current && current.data) return current.data - if (signal?.aborted) return { healthy: false } - - return createSdkForServer({ server, fetch, signal }) .global.health() .then((x) => (x.error ? next(count, x.error) : { healthy: x.data?.healthy === true, version: x.data?.version })) .catch((error) => next(count, error)) - } return attempt(0).finally(() => timeout?.clear?.()) } diff --git a/packages/app/src/utils/server-protocol.test.ts b/packages/app/src/utils/server-protocol.test.ts deleted file mode 100644 index 2130a968c4..0000000000 --- a/packages/app/src/utils/server-protocol.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { detectServerProtocol } from "./server-protocol" - -const server = { url: "http://localhost:4096" } -const json = (value: unknown, status = 200) => - new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } }) -const mockFetch = (run: (input: string | URL | Request) => Promise) => - Object.assign(run, { preconnect: globalThis.fetch.preconnect }) - -describe("detectServerProtocol", () => { - test("prefers the legacy health endpoint when both API generations exist", async () => { - const fetcher = mockFetch((input) => { - const path = new URL(input instanceof Request ? input.url : input).pathname - if (path === "/global/health") return Promise.resolve(json({ healthy: true, version: "1.18.4" })) - return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) - }) - - expect(await detectServerProtocol(server, fetcher)).toBe("v1") - }) - - test("recognizes V2 health by its process identifier", async () => { - const fetcher = mockFetch((input) => { - const path = new URL(input instanceof Request ? input.url : input).pathname - if (path === "/global/health") return Promise.resolve(json({}, 404)) - return Promise.resolve(json({ healthy: true, version: "2.0.0", pid: 123 })) - }) - - expect(await detectServerProtocol(server, fetcher)).toBe("v2") - }) - - test("recognizes the transitional V1 API health response", async () => { - const fetcher = mockFetch((input) => { - const path = new URL(input instanceof Request ? input.url : input).pathname - if (path === "/global/health") return Promise.resolve(json({}, 404)) - return Promise.resolve(json({ healthy: true })) - }) - - expect(await detectServerProtocol(server, fetcher)).toBe("v1") - }) -}) diff --git a/packages/app/src/utils/server-protocol.ts b/packages/app/src/utils/server-protocol.ts deleted file mode 100644 index 27b8dc208e..0000000000 --- a/packages/app/src/utils/server-protocol.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ServerConnection } from "@/context/server" -import { authTokenFromCredentials } from "./server" - -export type ServerProtocol = "v1" | "v2" - -function headers(server: ServerConnection.HttpBase) { - if (!server.password) return - return { - Authorization: `Basic ${authTokenFromCredentials({ username: server.username, password: server.password })}`, - } -} - -async function probe(server: ServerConnection.HttpBase, fetch: typeof globalThis.fetch, path: string) { - const response = await fetch(new URL(path, server.url), { - headers: headers(server), - signal: AbortSignal.timeout(5_000), - }) - if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) return - const value: unknown = await response.json() - if (!value || typeof value !== "object") return - return value -} - -export async function detectServerProtocol( - server: ServerConnection.HttpBase, - fetch: typeof globalThis.fetch, -): Promise { - const legacy = await probe(server, fetch, "/global/health").catch(() => undefined) - if (legacy && "healthy" in legacy && legacy.healthy === true) return "v1" - - const current = await probe(server, fetch, "/api/health").catch(() => undefined) - if (current && "pid" in current && typeof current.pid === "number") return "v2" - if (current && "healthy" in current && current.healthy === true) return "v1" - return "v2" -} diff --git a/packages/app/src/utils/server.ts b/packages/app/src/utils/server.ts index 1c8292ca9d..603784e4d4 100644 --- a/packages/app/src/utils/server.ts +++ b/packages/app/src/utils/server.ts @@ -1,5 +1,4 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" -import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise" import type { ServerConnection } from "@/context/server" import { decode64 } from "@/utils/base64" @@ -40,23 +39,3 @@ export function createSdkForServer({ baseUrl: server.url, }) } - -export function createApiForServer(input: { - server: ServerConnection.HttpBase - fetch?: typeof globalThis.fetch -}): OpenCodeClient { - return OpenCode.make({ - baseUrl: input.server.url, - fetch: input.fetch, - headers: input.server.password - ? { - Authorization: `Basic ${authTokenFromCredentials({ - username: input.server.username, - password: input.server.password, - })}`, - } - : undefined, - }) -} - -export type ServerApi = OpenCodeClient diff --git a/packages/app/src/utils/session-message.test.ts b/packages/app/src/utils/session-message.test.ts deleted file mode 100644 index a69c414e16..0000000000 --- a/packages/app/src/utils/session-message.test.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { SessionMessageInfo } from "@opencode-ai/client/promise" -import { normalizeSessionMessages } from "./session-message" - -describe("normalizeSessionMessages", () => { - test("projects current turns into stable legacy rendering records", () => { - const source = [ - { id: "msg_1", type: "agent-switched", agent: "build", time: { created: 1 } }, - { - id: "msg_2", - type: "model-switched", - model: { id: "claude", providerID: "anthropic", variant: "high" }, - time: { created: 2 }, - }, - { - id: "msg_3", - type: "user", - text: "inspect @src/client.ts", - files: [ - { - data: "aGVsbG8=", - mime: "text/plain", - name: "note.txt", - source: { type: "inline" }, - }, - { - data: "ZXhwb3J0IHt9", - mime: "text/plain", - name: "client.ts", - source: { type: "inline" }, - mention: { text: "@src/client.ts", start: 8, end: 22 }, - }, - ], - agents: [{ name: "review", mention: { text: "@review", start: 0, end: 7 } }], - time: { created: 3 }, - }, - { - id: "msg_4", - type: "assistant", - agent: "build", - model: { id: "claude", providerID: "anthropic", variant: "high" }, - content: [ - { type: "reasoning", text: "Thinking", time: { created: 4, completed: 5 } }, - { type: "text", text: "Result" }, - { - type: "tool", - id: "call_1", - name: "read", - state: { - status: "completed", - input: { filePath: "note.txt" }, - metadata: { title: "note.txt" }, - content: [{ type: "text", text: "hello" }], - }, - time: { created: 5, ran: 6, completed: 7 }, - }, - ], - cost: 0.1, - tokens: { input: 10, output: 5, reasoning: 2, cache: { read: 1, write: 0 } }, - time: { created: 4, completed: 7 }, - }, - { - id: "msg_5", - type: "compaction", - status: "completed", - reason: "auto", - summary: "summary", - recent: "recent", - time: { created: 8 }, - }, - ] satisfies SessionMessageInfo[] - - const result = normalizeSessionMessages("ses_1", source) - - expect(result.messages).toHaveLength(2) - expect(result.messages[0]).toMatchObject({ - id: "msg_3", - role: "user", - agent: "build", - model: { providerID: "anthropic", modelID: "claude", variant: "high" }, - }) - expect(result.messages[1]).toMatchObject({ id: "msg_4", role: "assistant", parentID: "msg_3", cost: 0.1 }) - expect(result.parts.get("msg_3")?.map((part) => part.id)).toEqual([ - "msg_3:text:0", - "msg_3:file:0", - "msg_3:file:1", - "msg_3:agent:0", - "msg_5:compaction", - ]) - expect(result.parts.get("msg_3")?.[2]).toMatchObject({ - type: "file", - source: { - type: "file", - path: "src/client.ts", - text: { value: "@src/client.ts", start: 8, end: 22 }, - }, - }) - expect(result.parts.get("msg_4")?.map((part) => part.id)).toEqual(["msg_4:reasoning:0", "msg_4:text:0", "call_1"]) - expect(result.parts.get("msg_4")?.[2]).toMatchObject({ - type: "tool", - tool: "read", - state: { status: "completed", output: "hello" }, - }) - }) - - test("does not invent a parent for an assistant-only page", () => { - const source = [ - { - id: "msg_2", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [{ type: "text", text: "orphan" }], - time: { created: 2 }, - }, - ] satisfies SessionMessageInfo[] - - expect(normalizeSessionMessages("ses_1", source).messages).toEqual([]) - }) - - test("projects a current shell message into a renderable standalone turn", () => { - const source = [ - { - id: "msg_shell", - type: "shell", - shellID: "shell_1", - command: "printf hello", - status: "exited", - exit: 0, - output: { output: "hello", cursor: 5, size: 5, truncated: false }, - time: { created: 1, completed: 2 }, - }, - ] satisfies SessionMessageInfo[] - - const result = normalizeSessionMessages("ses_1", source) - - expect(result.messages).toEqual([ - expect.objectContaining({ id: "msg_shell", role: "user" }), - expect.objectContaining({ id: "msg_shell:assistant", role: "assistant", parentID: "msg_shell" }), - ]) - expect(result.parts.get("msg_shell")).toEqual([expect.objectContaining({ type: "text", text: "printf hello" })]) - expect(result.parts.get("msg_shell:assistant")).toEqual([ - expect.objectContaining({ - type: "tool", - tool: "bash", - state: expect.objectContaining({ - status: "completed", - input: { command: "printf hello" }, - output: "hello", - title: "Shell", - }), - }), - ]) - }) - - test("adapts current edit fields for the legacy edit renderer", () => { - const source = [ - { id: "msg_user", type: "user", text: "edit it", time: { created: 1 } }, - { - id: "msg_assistant", - type: "assistant", - agent: "build", - model: { id: "model", providerID: "provider" }, - content: [ - { - type: "tool", - id: "call_edit", - name: "edit", - state: { - status: "completed", - input: { path: "/repo/README.md", oldString: "old", newString: "new" }, - content: [{ type: "text", text: "Edited file successfully" }], - metadata: { - files: [ - { - file: "README.md", - patch: "@@ -1 +1 @@\n-old\n+new", - additions: 1, - deletions: 1, - status: "modified", - }, - ], - replacements: 1, - }, - }, - time: { created: 2, ran: 3, completed: 4 }, - }, - ], - time: { created: 2, completed: 4 }, - }, - ] satisfies SessionMessageInfo[] - - const result = normalizeSessionMessages("ses_1", source) - - expect(result.parts.get("msg_assistant")).toEqual([ - expect.objectContaining({ - type: "tool", - tool: "edit", - state: expect.objectContaining({ - status: "completed", - input: expect.objectContaining({ path: "/repo/README.md", filePath: "/repo/README.md" }), - metadata: expect.objectContaining({ - filediff: { - file: "README.md", - patch: "@@ -1 +1 @@\n-old\n+new", - additions: 1, - deletions: 1, - }, - }), - }), - }), - ]) - }) -}) diff --git a/packages/app/src/utils/session-message.ts b/packages/app/src/utils/session-message.ts deleted file mode 100644 index 93d86a66bb..0000000000 --- a/packages/app/src/utils/session-message.ts +++ /dev/null @@ -1,358 +0,0 @@ -import type { - SessionMessageAssistant, - SessionMessageAssistantTool, - SessionMessageInfo, - SessionMessageShell, - SessionMessageUser, -} from "@opencode-ai/client/promise" -import type { AssistantMessage, FilePart, Message, Part, ToolPart, UserMessage } from "@opencode-ai/sdk/v2" -import { Option, Schema } from "effect" - -const emptyTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } -const emptyModel: { id: string; providerID: string; variant?: string } = { id: "", providerID: "" } -const decodeToolInput = Schema.decodeUnknownOption(Schema.UnknownFromJsonString) - -function record(value: unknown): value is Record { - return !!value && typeof value === "object" && !Array.isArray(value) -} - -function normalizeToolInput(name: string, input: Record) { - if (!["edit", "write"].includes(name) || typeof input.path !== "string" || typeof input.filePath === "string") - return input - return { ...input, filePath: input.path } -} - -function normalizeToolMetadata(name: string, metadata: Record) { - if (name !== "edit" || !Array.isArray(metadata.files)) return metadata - const file = metadata.files.find(record) - if (!file || typeof file.file !== "string") return metadata - return { - ...metadata, - filediff: { - file: file.file, - patch: typeof file.patch === "string" ? file.patch : undefined, - additions: typeof file.additions === "number" ? file.additions : 0, - deletions: typeof file.deletions === "number" ? file.deletions : 0, - }, - } -} - -export function normalizeSessionMessages(sessionID: string, source: readonly SessionMessageInfo[]) { - const messages: Message[] = [] - const parts = new Map() - let agent = "" - let model = emptyModel - let parentID: string | undefined - - source.forEach((message) => { - if (message.type === "agent-switched") { - agent = message.agent - return - } - if (message.type === "model-switched") { - model = message.model - return - } - if (message.type === "user") { - parentID = message.id - messages.push(userMessage(sessionID, message, agent, model)) - parts.set(message.id, userParts(sessionID, message)) - return - } - if (message.type === "synthetic" && message.description?.trim()) { - parentID = message.id - messages.push({ - id: message.id, - sessionID, - role: "user", - time: message.time, - agent, - model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, - }) - parts.set(message.id, [textPart(sessionID, message.id, 0, message.description, true)]) - return - } - if (message.type === "shell") { - messages.push(...shellMessages(sessionID, message, agent, model)) - parts.set(message.id, [textPart(sessionID, message.id, 0, message.command)]) - parts.set(`${message.id}:assistant`, [shellPart(sessionID, message)]) - parentID = undefined - return - } - if (message.type === "assistant") { - agent = message.agent - model = message.model - if (!parentID) return - const parent = messages.findLast((item) => item.id === parentID) - if (parent?.role === "user") { - parent.agent = message.agent - parent.model = { - providerID: message.model.providerID, - modelID: message.model.id, - variant: message.model.variant, - } - } - messages.push(assistantMessage(sessionID, parentID, message)) - parts.set(message.id, assistantParts(sessionID, message)) - return - } - if (message.type !== "compaction" || !parentID) return - parts.set(parentID, [ - ...(parts.get(parentID) ?? []), - { - id: `${message.id}:compaction`, - sessionID, - messageID: parentID, - type: "compaction", - auto: message.reason === "auto", - }, - ]) - }) - - return { messages, parts } -} - -function shellMessages( - sessionID: string, - message: SessionMessageShell, - agent: string, - model: { id: string; providerID: string; variant?: string }, -): [UserMessage, AssistantMessage] { - return [ - { - id: message.id, - sessionID, - role: "user", - time: { created: message.time.created }, - agent, - model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, - }, - { - id: `${message.id}:assistant`, - sessionID, - role: "assistant", - time: message.time, - parentID: message.id, - modelID: model.id, - providerID: model.providerID, - variant: model.variant, - mode: agent, - agent, - path: { cwd: "", root: "" }, - cost: 0, - tokens: emptyTokens, - }, - ] -} - -function shellPart(sessionID: string, message: SessionMessageShell): ToolPart { - const input = { command: message.command } - const start = message.time.created - const state: ToolPart["state"] = - message.status === "running" - ? { status: "running", input, time: { start } } - : { - status: "completed", - input, - output: message.output?.output ?? "", - title: "Shell", - metadata: { - status: message.status, - exit: message.exit, - truncated: message.output?.truncated, - }, - time: { start, end: message.time.completed ?? start }, - } - return { - id: `${message.id}:tool`, - sessionID, - messageID: `${message.id}:assistant`, - type: "tool", - callID: message.shellID, - tool: "bash", - state, - } -} - -export function sessionMessagePartID(messageID: string, type: "text" | "reasoning", ordinal: number) { - return `${messageID}:${type}:${ordinal}` -} - -function userMessage( - sessionID: string, - message: SessionMessageUser, - agent: string, - model: { id: string; providerID: string; variant?: string }, -): UserMessage { - return { - id: message.id, - sessionID, - role: "user", - time: message.time, - agent, - model: { providerID: model.providerID, modelID: model.id, variant: model.variant }, - } -} - -function userParts(sessionID: string, message: SessionMessageUser): Part[] { - return [ - textPart(sessionID, message.id, 0, message.text), - ...(message.files ?? []).map( - (file, index): FilePart => ({ - id: `${message.id}:file:${index}`, - sessionID, - messageID: message.id, - type: "file", - mime: file.mime, - filename: file.name, - url: file.source.type === "uri" ? file.source.uri : `data:${file.mime};base64,${file.data}`, - source: file.mention - ? { - type: "file", - text: { value: file.mention.text, start: file.mention.start, end: file.mention.end }, - path: file.mention.text.startsWith("@") ? file.mention.text.slice(1) : (file.name ?? file.mention.text), - } - : undefined, - }), - ), - ...(message.agents ?? []).map( - (item, index): Part => ({ - id: `${message.id}:agent:${index}`, - sessionID, - messageID: message.id, - type: "agent", - name: item.name, - source: item.mention - ? { value: item.mention.text, start: item.mention.start, end: item.mention.end } - : undefined, - }), - ), - ] -} - -function assistantMessage(sessionID: string, parentID: string, message: SessionMessageAssistant): AssistantMessage { - const error = message.error - ? message.error.type.toLowerCase().includes("abort") || message.error.type.toLowerCase().includes("interrupt") - ? { name: "MessageAbortedError" as const, data: { message: message.error.message } } - : { name: "UnknownError" as const, data: { message: message.error.message } } - : undefined - return { - id: message.id, - sessionID, - role: "assistant", - time: message.time, - error, - parentID, - modelID: message.model.id, - providerID: message.model.providerID, - variant: message.model.variant, - mode: message.agent, - agent: message.agent, - path: { cwd: "", root: "" }, - cost: message.cost ?? 0, - tokens: message.tokens ?? emptyTokens, - finish: message.finish, - } -} - -function assistantParts(sessionID: string, message: SessionMessageAssistant): Part[] { - const ordinals = { text: 0, reasoning: 0 } - return message.content.flatMap((content): Part[] => { - if (content.type === "text") { - const part = textPart(sessionID, message.id, ordinals.text++, content.text) - return content.text.trim() ? [part] : [] - } - if (content.type === "reasoning") { - const part: Part = { - id: sessionMessagePartID(message.id, "reasoning", ordinals.reasoning++), - sessionID, - messageID: message.id, - type: "reasoning", - text: content.text, - metadata: content.state, - time: { - start: content.time?.created ?? message.time.created, - end: content.time?.completed, - }, - } - return content.text.trim() ? [part] : [] - } - return [toolPart(sessionID, message.id, content)] - }) -} - -function textPart(sessionID: string, messageID: string, ordinal: number, text: string, synthetic?: boolean): Part { - return { - id: sessionMessagePartID(messageID, "text", ordinal), - sessionID, - messageID, - type: "text", - text, - synthetic, - } -} - -function toolPart(sessionID: string, messageID: string, tool: SessionMessageAssistantTool): ToolPart { - const start = tool.time.ran ?? tool.time.created - const state = (() => { - if (tool.state.status === "streaming") { - const value = Option.getOrUndefined(decodeToolInput(tool.state.input)) - const input = normalizeToolInput(tool.name, record(value) ? value : {}) - return { status: "pending" as const, input, raw: tool.state.input } - } - if (tool.state.status === "running") { - return { - status: "running" as const, - input: normalizeToolInput(tool.name, tool.state.input), - // metadata: normalizeToolMetadata(tool.name, tool.state.structured), - metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), - time: { start }, - } - } - if (tool.state.status === "error") { - return { - status: "error" as const, - input: normalizeToolInput(tool.name, tool.state.input), - error: tool.state.error.message, - // metadata: normalizeToolMetadata(tool.name, tool.state.structured), - metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), - time: { start, end: tool.time.completed ?? start }, - } - } - const attachments = tool.state.content.flatMap((item, index): FilePart[] => - item.type === "file" - ? [ - { - id: `${tool.id}:file:${index}`, - sessionID, - messageID, - type: "file", - mime: item.mime, - filename: item.name, - url: item.uri, - }, - ] - : [], - ) - return { - status: "completed" as const, - input: normalizeToolInput(tool.name, tool.state.input), - output: tool.state.content.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n"), - title: tool.name, - // metadata: normalizeToolMetadata(tool.name, tool.state.structured), - metadata: normalizeToolMetadata(tool.name, tool.state.metadata ?? {}), - time: { start, end: tool.time.completed ?? start }, - attachments: attachments.length ? attachments : undefined, - } - })() - return { - id: tool.id, - sessionID, - messageID, - type: "tool", - callID: tool.id, - tool: tool.name, - state, - metadata: { providerState: tool.providerState, providerResultState: tool.providerResultState }, - } -} diff --git a/packages/app/src/utils/session.test.ts b/packages/app/src/utils/session.test.ts deleted file mode 100644 index b15c23b660..0000000000 --- a/packages/app/src/utils/session.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { describe, expect, test } from "bun:test" -import type { SessionApi, SessionInfo, SessionListInput } from "@opencode-ai/client/promise" -import { listAllSessions, normalizeSessionInfo } from "./session" - -describe("normalizeSessionInfo", () => { - test("adapts a current session to the app session shape", () => { - const result = normalizeSessionInfo({ - id: "session-1", - projectID: "project-1", - agent: "build", - model: { id: "gpt-5", providerID: "openai", variant: "high" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1, updated: 1 }, - title: "New session", - location: { directory: "/repo/worktree", workspaceID: "workspace-1" }, - subpath: "worktree", - revert: { messageID: "message-1", partID: "part-1", snapshot: "snapshot", files: [] }, - } as SessionInfo) - - expect(result).toEqual({ - id: "session-1", - slug: "session-1", - projectID: "project-1", - workspaceID: "workspace-1", - directory: "/repo/worktree", - path: "worktree", - parentID: undefined, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - title: "New session", - agent: "build", - model: { id: "gpt-5", providerID: "openai", variant: "high" }, - version: "", - time: { created: 1, updated: 1 }, - revert: { messageID: "message-1", partID: "part-1", snapshot: "snapshot" }, - }) - }) -}) - -describe("listAllSessions", () => { - test("loads every page in server order and retains the query", async () => { - const calls: SessionListInput[] = [] - const pages = new Map([ - [undefined, { data: [sessionInfo("session-3"), sessionInfo("session-2")], cursor: { next: "next" } }], - ["next", { data: [sessionInfo("session-1", true)], cursor: {} }], - ]) - const api = { - list: async (query = {}) => { - calls.push(query) - return pages.get(query.cursor) ?? { data: [], cursor: {} } - }, - } satisfies Pick - - const result = await listAllSessions(api, { directory: "/repo", order: "desc" }) - - expect(result.map((session) => session.id)).toEqual(["session-3", "session-2", "session-1"]) - expect(result[2]?.time.archived).toBe(2) - expect(calls).toEqual([ - { directory: "/repo", order: "desc", limit: 100, cursor: undefined }, - { directory: "/repo", order: "desc", limit: 100, cursor: "next" }, - ]) - }) - - test("requests the terminal empty page when the server returns a next cursor", async () => { - const cursors: Array = [] - const api = { - list: async (query = {}) => { - cursors.push(query.cursor) - if (query.cursor) return { data: [], cursor: { next: "unused" } } - return { data: [sessionInfo("session-1")], cursor: { next: "terminal" } } - }, - } satisfies Pick - - const result = await listAllSessions(api, { directory: "/repo", limit: 25 }) - - expect(result.map((session) => session.id)).toEqual(["session-1"]) - expect(cursors).toEqual([undefined, "terminal"]) - }) -}) - -function sessionInfo(id: string, archived = false) { - return { - id, - projectID: "project-1", - agent: "build", - model: { id: "model-1", providerID: "provider-1" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - time: { created: 1, updated: 1, archived: archived ? 2 : undefined }, - title: id, - location: { directory: "/repo" }, - } as SessionInfo -} diff --git a/packages/app/src/utils/session.ts b/packages/app/src/utils/session.ts deleted file mode 100644 index faf847967b..0000000000 --- a/packages/app/src/utils/session.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { SessionApi, SessionInfo, SessionListInput } from "@opencode-ai/client/promise" -import type { Session } from "@opencode-ai/sdk/v2/client" - -export function normalizeSessionInfo(input: SessionInfo | Session): Session { - if (!("location" in input)) return input - return { - id: input.id, - slug: input.id, - projectID: input.projectID, - workspaceID: input.location.workspaceID, - directory: input.location.directory, - path: input.subpath, - parentID: input.parentID, - cost: input.cost, - tokens: input.tokens, - title: input.title, - agent: input.agent, - model: input.model, - version: "", - time: input.time, - revert: input.revert && { - messageID: input.revert.messageID, - partID: input.revert.partID, - snapshot: input.revert.snapshot, - }, - } -} - -export async function listAllSessions(api: Pick, input: Omit) { - const load = async (cursor?: string): Promise => { - const result = await api.list({ ...input, limit: input.limit ?? 100, cursor }) - const sessions = result.data.map(normalizeSessionInfo) - if (result.data.length === 0 || !result.cursor.next) return sessions - return [...sessions, ...(await load(result.cursor.next))] - } - return load() -} diff --git a/packages/app/src/utils/terminal-websocket-url.test.ts b/packages/app/src/utils/terminal-websocket-url.test.ts index aac854ca82..5fa1506b1e 100644 --- a/packages/app/src/utils/terminal-websocket-url.test.ts +++ b/packages/app/src/utils/terminal-websocket-url.test.ts @@ -2,28 +2,8 @@ import { describe, expect, test } from "bun:test" import { terminalWebSocketURL } from "./terminal-websocket-url" describe("terminalWebSocketURL", () => { - test("uses the current ticketed PTY route", () => { + test("uses query auth without embedding credentials in websocket URL", () => { const url = terminalWebSocketURL({ - url: "http://127.0.0.1:49365", - id: "pty_test", - directory: "/tmp/project", - cursor: 0, - ticket: "connect-ticket", - }) - - expect(url.protocol).toBe("ws:") - expect(url.username).toBe("") - expect(url.password).toBe("") - expect(url.pathname).toBe("/api/pty/pty_test/connect") - expect(url.searchParams.get("location[directory]")).toBe("/tmp/project") - expect(url.searchParams.get("cursor")).toBe("0") - expect(url.searchParams.get("ticket")).toBe("connect-ticket") - expect(url.searchParams.has("auth_token")).toBe(false) - }) - - test("uses query auth without embedding credentials in websocket URL for v1", () => { - const url = terminalWebSocketURL({ - protocol: "v1", url: "http://127.0.0.1:49365", id: "pty_test", directory: "/tmp/project", @@ -36,14 +16,11 @@ describe("terminalWebSocketURL", () => { expect(url.protocol).toBe("ws:") expect(url.username).toBe("") expect(url.password).toBe("") - expect(url.pathname).toBe("/pty/pty_test/connect") - expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret")) }) - test("omits query auth for same-origin saved credentials for v1", () => { + test("omits query auth for same-origin saved credentials", () => { const url = terminalWebSocketURL({ - protocol: "v1", url: "https://app.example.test", id: "pty_test", directory: "/tmp/project", @@ -54,14 +31,11 @@ describe("terminalWebSocketURL", () => { }) expect(url.protocol).toBe("wss:") - expect(url.pathname).toBe("/pty/pty_test/connect") - expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.has("auth_token")).toBe(false) }) - test("uses query auth for same-origin credentials from auth_token for v1", () => { + test("uses query auth for same-origin credentials from auth_token", () => { const url = terminalWebSocketURL({ - protocol: "v1", url: "https://app.example.test", id: "pty_test", directory: "/tmp/project", @@ -73,8 +47,6 @@ describe("terminalWebSocketURL", () => { }) expect(url.protocol).toBe("wss:") - expect(url.pathname).toBe("/pty/pty_test/connect") - expect(url.searchParams.get("directory")).toBe("/tmp/project") expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret")) }) }) diff --git a/packages/app/src/utils/terminal-websocket-url.ts b/packages/app/src/utils/terminal-websocket-url.ts index a32b239cc9..06facdc7d2 100644 --- a/packages/app/src/utils/terminal-websocket-url.ts +++ b/packages/app/src/utils/terminal-websocket-url.ts @@ -1,7 +1,6 @@ import { authTokenFromCredentials } from "@/utils/server" export function terminalWebSocketURL(input: { - protocol?: "v1" | "v2" url: string id: string directory: string @@ -12,24 +11,18 @@ export function terminalWebSocketURL(input: { password?: string authToken?: boolean }) { - const isV1 = input.protocol === "v1" - const next = new URL(`${input.url}${isV1 ? `/pty/${input.id}/connect` : `/api/pty/${input.id}/connect`}`) - if (isV1) { - next.searchParams.set("directory", input.directory) - } else { - next.searchParams.set("location[directory]", input.directory) - } + const next = new URL(`${input.url}/pty/${input.id}/connect`) + next.searchParams.set("directory", input.directory) next.searchParams.set("cursor", String(input.cursor)) next.protocol = next.protocol === "https:" ? "wss:" : "ws:" if (input.ticket) { next.searchParams.set("ticket", input.ticket) return next } - if (isV1 && input.password && (!input.sameOrigin || input.authToken)) { + if (input.password && (!input.sameOrigin || input.authToken)) next.searchParams.set( "auth_token", authTokenFromCredentials({ username: input.username, password: input.password }), ) - } return next } diff --git a/packages/app/src/utils/toast.tsx b/packages/app/src/utils/toast.tsx index 6f23b63d1e..e444548508 100644 --- a/packages/app/src/utils/toast.tsx +++ b/packages/app/src/utils/toast.tsx @@ -1,12 +1,6 @@ import { Icon, type IconProps } from "@opencode-ai/ui/icon" -import { - Toast, - showToast as showLegacyToast, - toaster as legacyToaster, - type ToastOptions, - type ToastVariant, -} from "@opencode-ai/ui/toast" -import { ToastV2, showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" +import { Toast, showToast as showLegacyToast, type ToastOptions, type ToastVariant } from "@opencode-ai/ui/toast" +import { ToastV2, showToastV2 } from "@opencode-ai/ui/v2/toast-v2" let v2 = false @@ -33,13 +27,6 @@ export function showToast(options: ToastOptions | string) { }) } -// v1 and v2 ids come from separate registries, so dismissal has to use the same -// implementation that issued the id. -export function dismissToast(toastId: number) { - if (!v2) return legacyToaster.dismiss(toastId) - return toasterV2.dismiss(toastId) -} - function resolveIcon(icon: IconProps["name"] | undefined, variant: ToastVariant | undefined) { const name = icon ?? (variant === "success" ? "check" : undefined) if (!name) return diff --git a/packages/app/test-browser/command-palette.test.ts b/packages/app/test-browser/command-palette.test.ts index 6a74834fd0..421a2e71fd 100644 --- a/packages/app/test-browser/command-palette.test.ts +++ b/packages/app/test-browser/command-palette.test.ts @@ -1,6 +1,5 @@ import { describe, expect, test } from "bun:test" -import type { Project } from "@opencode-ai/sdk/v2/client" -import type { SessionInfo } from "@opencode-ai/client/promise" +import type { GlobalSession, Project } from "@opencode-ai/sdk/v2/client" import { createRoot } from "solid-js" import { createServerSessionEntries } from "@/components/command-palette" import type { LocalProject } from "@/context/layout" @@ -15,16 +14,15 @@ const stored: Project = { time: { created: 1, updated: 1 }, } -const session: SessionInfo = { +const session: GlobalSession = { id: "session-1", + slug: "session-1", projectID: stored.id, - agent: "build", - model: { id: "model-1", providerID: "provider-1" }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - location: { directory: stored.worktree }, + directory: stored.worktree, title: "Palette session", + version: "1", time: { created: 1, updated: 2 }, + project: { id: stored.id, name: stored.name, worktree: stored.worktree }, } describe("command palette sessions", () => { diff --git a/packages/app/test-browser/prompt-attachments.test.ts b/packages/app/test-browser/prompt-attachments.test.ts index 49dad279cd..ca7686598e 100644 --- a/packages/app/test-browser/prompt-attachments.test.ts +++ b/packages/app/test-browser/prompt-attachments.test.ts @@ -1,10 +1,7 @@ import { describe, expect, test } from "bun:test" import { createRoot } from "solid-js" -import { createStore } from "solid-js/store" import { createPromptAttachmentsCore } from "@/components/prompt-input/attachments" import { createPromptState } from "@/context/prompt" -import { createPromptInputV2Attachments } from "../../session-ui/src/v2/components/prompt-input/attachments" -import type { PromptInputV2Prompt } from "../../session-ui/src/v2/components/prompt-input/types" describe("prompt attachment session ownership", () => { test("adds an asynchronously read image to the session where the read started", async () => { @@ -88,88 +85,6 @@ describe("prompt attachment session ownership", () => { }) }) -test("rejects a duplicate native clipboard attachment in the V2 prompt store", async () => { - await createRoot(async (dispose) => { - const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) - const duplicate = Promise.withResolvers() - const files = [ - new File(["hello"], "clipboard-1.txt", { type: "text/plain" }), - new File(["hello"], "clipboard-2.txt", { type: "text/plain" }), - ] - const attachments = createPromptInputV2Attachments({ - capture: () => ({ - current: () => state.prompt, - cursor: () => 0, - set: (prompt) => setState("prompt", prompt), - }), - editor: () => document.createElement("div"), - focusEditor: () => undefined, - addPart: () => false, - setDraggingType: () => undefined, - directory: () => "/", - isDialogActive: () => false, - warn: () => undefined, - duplicate: duplicate.resolve, - onError: () => undefined, - readClipboardImage: async () => files.shift() ?? null, - }) - const event = { - clipboardData: { items: [], getData: () => "" }, - preventDefault: () => undefined, - stopPropagation: () => undefined, - } as unknown as ClipboardEvent - - await attachments.handlePaste(event) - await attachments.handlePaste(event) - await duplicate.promise - - expect(state.prompt).toHaveLength(1) - dispose() - }) -}) - -test("rejects desktop duplicates and keeps changed files in the V2 prompt store", async () => { - await createRoot(async (dispose) => { - const [state, setState] = createStore({ prompt: [] as PromptInputV2Prompt }) - const duplicates: string[] = [] - const attachments = createPromptInputV2Attachments({ - capture: () => ({ - current: () => state.prompt, - cursor: () => 0, - set: (prompt) => setState("prompt", prompt), - }), - editor: () => document.createElement("div"), - focusEditor: () => undefined, - addPart: () => false, - setDraggingType: () => undefined, - directory: () => "/", - isDialogActive: () => false, - warn: () => undefined, - duplicate: () => duplicates.push("duplicate"), - onError: () => undefined, - getPathForFile: (file) => (file.name === "browser.txt" ? "" : `/tmp/${file.name}`), - }) - const first = new File(["first"], "a.txt", { type: "text/plain" }) - const second = new File(["second"], "b.txt", { type: "text/plain" }) - - await attachments.addAttachments([first, second]) - await attachments.addAttachments([first, second]) - expect(state.prompt).toHaveLength(2) - expect(duplicates).toEqual(["duplicate", "duplicate"]) - - await attachments.addAttachments([new File(["edited"], "a.txt", { type: "text/plain" })]) - expect(state.prompt).toHaveLength(3) - - await attachments.addAttachments([ - new File(["same"], "browser.txt", { type: "text/plain" }), - new File(["same"], "browser.txt", { type: "text/plain" }), - ]) - expect(state.prompt).toHaveLength(4) - expect(duplicates).toHaveLength(3) - dispose() - }) -}) - function images(prompt: ReturnType) { return prompt.current().filter((part) => part.type === "image") } diff --git a/packages/app/test-browser/prompt-persistence.test.ts b/packages/app/test-browser/prompt-persistence.test.ts index f3d8f8ae4f..a7b08078d7 100644 --- a/packages/app/test-browser/prompt-persistence.test.ts +++ b/packages/app/test-browser/prompt-persistence.test.ts @@ -2,7 +2,6 @@ import { beforeAll, describe, expect, mock, test } from "bun:test" import type { AsyncStorage } from "@solid-primitives/storage" import { createEffect, createRoot } from "solid-js" import { ServerScope } from "@/utils/server-scope" -import { createDraftStore } from "@/utils/draft-store" let Prompt: typeof import("@/context/prompt") let read: ((value: string | null) => void) | undefined @@ -31,7 +30,7 @@ beforeAll(async () => { }), })) mock.module("@/context/platform", () => ({ - usePlatform: () => ({ platform: "desktop", storage: () => storage, draftStore: storage }), + usePlatform: () => ({ platform: "desktop", storage: () => storage }), })) Prompt = await import("@/context/prompt") @@ -70,50 +69,3 @@ describe("prompt persistence", () => { }) }) }) - -test("moves legacy image data URLs into blobs and hydrates object URLs", async () => { - const documents = new Map() - const blobs = new Map() - const store = createDraftStore({ - get: async (key) => documents.get(key) ?? null, - set: async (key, value) => void documents.set(key, value), - remove: async (key) => void documents.delete(key), - putBlob: async (blob) => { - const id = String(blob.size) - blobs.set(id, blob) - return id - }, - getBlob: async (id) => blobs.get(id) ?? null, - }) - - await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] })) - expect(documents.get("prompt")).not.toContain("dataUrl") - const value = JSON.parse((await store.getItem("prompt"))!) - expect(value.prompt[0].blob.id).toBe("1") - expect(value.prompt[0].blob.url).toStartWith("blob:") -}) - -test("does not let delayed blob migration overwrite a newer draft", async () => { - const documents = new Map() - const migration = Promise.withResolvers() - const store = createDraftStore({ - get: async () => null, - set: async (key, value) => void documents.set(key, value), - remove: async () => undefined, - putBlob: async () => { - await migration.promise - return "blob" - }, - getBlob: async () => null, - }) - const older = store.setItem( - "prompt", - JSON.stringify({ prompt: [{ type: "image", dataUrl: "data:image/png;base64,YQ==" }] }), - ) - await Bun.sleep(0) - await store.setItem("prompt", JSON.stringify({ prompt: [{ type: "text", content: "latest" }] })) - migration.resolve() - await older - - expect(documents.get("prompt")).toContain("latest") -}) diff --git a/packages/app/test-browser/settings-keybinds.test.ts b/packages/app/test-browser/settings-keybinds.test.ts deleted file mode 100644 index 75f0b75543..0000000000 --- a/packages/app/test-browser/settings-keybinds.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, test } from "bun:test" -import { createRoot } from "solid-js" -import { createKeybindSettingsController } from "../src/components/settings-keybinds" - -function setup(overrides: Record = {}) { - const changes: [string, string][] = [] - const suppression: boolean[] = [] - const notifications: { title: string; description?: string }[] = [] - let resets = 0 - let controller: ReturnType - - const dispose = createRoot((dispose) => { - controller = createKeybindSettingsController( - { - command: { - catalog: [ - { id: "session.alpha", title: "Alpha", keybind: "mod+a" }, - { id: "session.beta", title: "Beta", keybind: "mod+b" }, - ], - options: [], - keybinds: (enabled) => suppression.push(enabled), - }, - settings: { - current: { keybinds: overrides }, - keybinds: { - get: (id) => overrides[id], - set: (id, value) => { - overrides[id] = value - changes.push([id, value]) - }, - resetAll: () => { - resets++ - }, - }, - }, - notify: (toast) => notifications.push(toast), - }, - { - locale: () => "en", - t: (key, params) => { - if (params) return `${key}:${Object.values(params).join("|")}` - if (key === "common.key.alt") return "Alt" - return String(key) - }, - }, - ) - return dispose - }) - - return { - controller: controller!, - changes, - suppression, - notifications, - resets: () => resets, - dispose, - } -} - -function modKey(key: string) { - const mac = /(Mac|iPod|iPhone|iPad)/.test(navigator.platform) - return new KeyboardEvent("keydown", { key, ctrlKey: !mac, metaKey: mac, bubbles: true, cancelable: true }) -} - -describe("keybind settings controller", () => { - test("derives the catalog, effective bindings, and filtered groups", () => { - const state = setup({ "session.beta": "alt+k" }) - - expect(state.controller.catalog.title("session.alpha")).toBe("Alpha") - expect(state.controller.catalog.keybind("session.beta")).toBe("Alt+K") - expect(state.controller.catalog.filtered("alt k").get("Session")).toEqual(["session.beta"]) - expect(state.controller.settings.hasOverrides()).toBe(true) - - state.dispose() - }) - - test("captures bindings, rejects conflicts, and restores command handling", () => { - const state = setup() - - state.controller.capture.toggle("session.beta") - document.dispatchEvent(modKey("a")) - expect(state.changes).toEqual([]) - expect(state.notifications).toHaveLength(1) - expect(state.controller.capture.active()).toBe("session.beta") - - document.dispatchEvent(modKey("x")) - expect(state.changes).toEqual([["session.beta", "mod+x"]]) - expect(state.suppression).toEqual([false, true]) - expect(state.controller.capture.active()).toBeNull() - - state.controller.capture.toggle("session.alpha") - state.dispose() - expect(state.suppression).toEqual([false, true, false, true]) - document.dispatchEvent(modKey("z")) - expect(state.changes).toEqual([["session.beta", "mod+x"]]) - }) - - test("resets persisted overrides and reports success", () => { - const state = setup({ "session.alpha": "none" }) - - state.controller.settings.reset() - expect(state.resets()).toBe(1) - expect(state.notifications[0]?.title).toBe("settings.shortcuts.reset.toast.title") - - state.dispose() - }) -}) diff --git a/packages/app/test-browser/solid-router-cleanup.test.ts b/packages/app/test-browser/solid-router-cleanup.test.ts deleted file mode 100644 index 5795fa5648..0000000000 --- a/packages/app/test-browser/solid-router-cleanup.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect, test } from "bun:test" -import { MetaProvider, Title } from "@solidjs/meta" -import { MemoryRouter, Route, createMemoryHistory, useParams } from "@solidjs/router" -import { createMemo } from "solid-js" -import { createComponent, render } from "solid-js/web" - -test("route cleanup cannot invalidate an owner list being disposed", async () => { - const host = document.createElement("div") - document.body.append(host) - const history = createMemoryHistory() - - const RepoPage = () => { - const params = useParams<{ id?: string }>() - const title = createMemo(() => params.id ?? "") - const button = document.createElement("button") - button.textContent = "Back" - button.addEventListener("click", () => history.set({ value: "/", scroll: false, replace: false })) - return [ - createComponent(Title, { - get children() { - return title() - }, - }), - button, - ] - } - - const HomePage = () => { - const button = document.createElement("button") - button.textContent = "Go" - button.addEventListener("click", () => history.set({ value: "/project", scroll: false, replace: false })) - return button - } - - const App = () => - createComponent(MetaProvider, { - get children() { - return createComponent(MemoryRouter, { - history, - get children() { - return [ - createComponent(Route, { path: "/", component: HomePage }), - createComponent(Route, { path: "/:id", component: RepoPage }), - ] - }, - }) - }, - }) - - const dispose = render(() => createComponent(App, {}), host) - const go = host.querySelector("button") - expect(go?.textContent).toBe("Go") - go?.click() - await new Promise((resolve) => setTimeout(resolve, 0)) - - const back = host.querySelector("button") - expect(back?.textContent).toBe("Back") - back?.click() - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(host.querySelector("button")?.textContent).toBe("Go") - dispose() - host.remove() -}) diff --git a/packages/app/test-browser/toast-owner.test.ts b/packages/app/test-browser/toast-owner.test.ts index 25ba5c000e..104a505851 100644 --- a/packages/app/test-browser/toast-owner.test.ts +++ b/packages/app/test-browser/toast-owner.test.ts @@ -1,46 +1,8 @@ -import { beforeEach, describe, expect, test } from "bun:test" +import { describe, expect, test } from "bun:test" import { createSignal, type JSX } from "solid-js" import { showToastV2, toasterV2 } from "@opencode-ai/ui/v2/toast-v2" describe("showToastV2", () => { - // The toast registry is module state, so each test starts from an empty stack. - beforeEach(() => { - toasterV2.dismiss() - }) - - test("coalesces exact active content", () => { - const first = showToastV2({ title: "Repeated error", description: "Try again" }) - const second = showToastV2({ title: "Repeated error", description: "Try again" }) - const different = showToastV2({ title: "Repeated error", description: "A different error" }) - - expect(second).toBe(first) - expect(different).not.toBe(first) - - toasterV2.dismiss(first) - toasterV2.dismiss(different) - }) - - test("allows dismissed content to appear again", () => { - const first = showToastV2("Dismiss and retry") - toasterV2.dismiss(first) - - const second = showToastV2("Dismiss and retry") - expect(second).not.toBe(first) - - toasterV2.dismiss(second) - }) - - test("recreates matching content when it is not the topmost toast", () => { - const first = showToastV2("First toast") - const topmost = showToastV2("Topmost toast") - const repeated = showToastV2("First toast") - - expect(repeated).not.toBe(first) - - toasterV2.dismiss(topmost) - toasterV2.dismiss(repeated) - }) - test("creates no reactive computations at call time", () => { const [tick, setTick] = createSignal(0) let reads = 0 diff --git a/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz b/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz deleted file mode 100644 index bc1b664f82..0000000000 Binary files a/packages/app/vendor/opencode-ai-client-1.17.13-v2.tgz and /dev/null differ diff --git a/packages/cli/package.json b/packages/cli/package.json index be5d853de0..2152bc3e2a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/cli", - "version": "1.18.11", + "version": "1.18.3", "type": "module", "license": "MIT", "bin": { diff --git a/packages/cli/script/generate.ts b/packages/cli/script/generate.ts index e162f2ea7e..d98565e298 100755 --- a/packages/cli/script/generate.ts +++ b/packages/cli/script/generate.ts @@ -1,4 +1,4 @@ -const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai" +const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" export const modelsData = process.env.MODELS_DEV_API_JSON ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 1641518483..45a7eb80d9 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "1.18.11", + "version": "1.18.3", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 5023021661..c10a9aff34 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-app", - "version": "1.18.11", + "version": "1.18.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/app.tsx b/packages/console/app/src/app.tsx index 1f1d0066ec..99b1010767 100644 --- a/packages/console/app/src/app.tsx +++ b/packages/console/app/src/app.tsx @@ -9,6 +9,7 @@ import "./app.css" import { LanguageProvider } from "~/context/language" import { I18nProvider, useI18n } from "~/context/i18n" import { strip } from "~/lib/language" +import { DesktopPromo } from "~/component/desktop-promo" function AppMeta() { const i18n = useI18n() @@ -33,6 +34,7 @@ export default function App() { {props.children} + diff --git a/packages/console/app/src/i18n/ar.ts b/packages/console/app/src/i18n/ar.ts index dd03a553f5..1102332286 100644 --- a/packages/console/app/src/i18n/ar.ts +++ b/packages/console/app/src/i18n/ar.ts @@ -252,9 +252,9 @@ export const dict = { "zen.privacy.exceptionsLink": "الاستثناءات التالية", "go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع", - "go.banner.text": "يحصل GPT 5.6 Luna على حدود استخدام مضاعفة لفترة محدودة", + "go.banner.text": "يحصل Kimi K3 على حدود استخدام مضاعفة لفترة محدودة", "go.meta.description": - "يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.", + "يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود طلب سخية لمدة 5 ساعات لـ Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", "go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع", "go.hero.body": "يجلب Go البرمجة الوكيلة للمبرمجين حول العالم. يوفر حدودًا سخية ووصولًا موثوقًا إلى أقوى النماذج مفتوحة المصدر، حتى تتمكن من البناء باستخدام وكلاء أقوياء دون القلق بشأن التكلفة أو التوفر.", @@ -301,7 +301,8 @@ export const dict = { "go.problem.item1": "أسعار اشتراك منخفضة التكلفة", "go.problem.item2": "حدود سخية ووصول موثوق", "go.problem.item3": "مصمم لأكبر عدد ممكن من المبرمجين", - "go.problem.item4": "مجموعة منسقة من النماذج المختبرة للبرمجة الوكيلة", + "go.problem.item4": + "يتضمن Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash", "go.how.title": "كيف يعمل Go", "go.how.body": "يبدأ Go من $5 للشهر الأول، ثم $10/شهر. يمكنك استخدامه مع OpenCode أو أي وكيل.", "go.how.step1.title": "أنشئ حسابًا", @@ -325,7 +326,7 @@ export const dict = { "go.faq.a2": "يتضمن Go النماذج المدرجة أدناه، مع حدود سخية وإتاحة موثوقة.", "go.faq.q3": "هل Go هو نفسه Zen؟", "go.faq.a3": - "لا. يعتمد Zen على الدفع حسب الاستخدام، بينما يبدأ Go بسعر $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى مجموعة منسقة من النماذج.", + "لا. Zen هو الدفع حسب الاستخدام، بينما يبدأ Go من $5 للشهر الأول، ثم $10/شهر، مع حدود سخية ووصول موثوق إلى نماذج المصدر المفتوح Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash.", "go.faq.q4": "كم تكلفة Go؟", "go.faq.a4.p1.beforePricing": "تكلفة Go", "go.faq.a4.p1.pricingLink": "$5 للشهر الأول", @@ -334,20 +335,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "حسابك", "go.faq.a4.p3": "ألغِ في أي وقت.", "go.faq.q5": "ماذا عن البيانات والخصوصية؟", - "go.faq.a5.model": "النموذج", - "go.faq.a5.training": "تدريب النموذج", - "go.faq.a5.retention": "الاحتفاظ بالبيانات", - "go.faq.a5.retention30": "30 يومًا", - "go.faq.a5.retention0": "0 أيام", - "go.faq.a5.used": "مستخدَمة", - "go.faq.a5.notUsed": "غير مستخدَمة", - "go.faq.a5.noAgreement": "لا توجد اتفاقية", - "go.faq.a5.grokRetention": - "تعطّل ZDR ميزات API مهمة تعتمد على البيانات المخزنة، بما في ذلك Responses API ذات الحالة، وFiles and Collections، وBatch API.", - "go.faq.a5.gptRetention": - "تُنشأ سجلات مراقبة إساءة الاستخدام لكل استخدام لميزات API، ويُحتفظ بها لمدة تصل إلى 30 يومًا.", - "go.faq.a5.learnMore": "اعرف المزيد", - "go.faq.a5.deepseekRetention": "تُجدَّد اتفاقية ZDR شهريًا. الاتفاقية الحالية سارية حتى 31 أغسطس 2026.", + "go.faq.a5.body": + "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. يتبع مزودونا سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج.", "go.faq.a5.beforeExceptions": "تتم استضافة نماذج Go في الولايات المتحدة. يتبع المزودون سياسة عدم الاحتفاظ بالبيانات ولا يستخدمون بياناتك لتدريب النماذج، مع", "go.faq.a5.exceptionsLink": "الاستثناءات التالية", @@ -360,7 +349,7 @@ export const dict = { "go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟", "go.faq.a9": - "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة قدرها 200 طلب/يوم. يقدّم Go مجموعة منسقة من النماذج مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، وأسبوعية، وشهرية)، تعادل تقريبًا $12 لكل 5 ساعات، و$30 في الأسبوع، و$60 في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", + "تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة 200 طلب/يوم. يتضمن Go نماذج Grok 4.5 وGLM-5.2 وGLM-5.1 وKimi K3 وKimi K2.7 Code وKimi K2.6 وMiMo-V2.5-Pro وMiMo-V2.5 وQwen3.7 Max وQwen3.7 Plus وQwen3.6 Plus وMiniMax M2.7 وMiniMax M3 وDeepSeek V4 Pro وDeepSeek V4 Flash مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، أسبوعيًا، وشهريًا)، تعادل تقريبًا 12 دولارًا كل 5 ساعات، و30 دولارًا في الأسبوع، و60 دولارًا في الشهر (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).", "zen.api.error.rateLimitExceeded": "تم تجاوز حد الطلبات. يرجى المحاولة مرة أخرى لاحقًا.", "zen.api.error.modelNotSupported": "النموذج {{model}} غير مدعوم", @@ -369,7 +358,6 @@ export const dict = { "zen.api.error.providerNotSupported": "المزود {{provider}} غير مدعوم", "zen.api.error.missingApiKey": "مفتاح API مفقود.", "zen.api.error.invalidApiKey": "مفتاح API غير صالح.", - "zen.api.error.requestBlockedByUpstreamProvider": "حظر المزود الخارجي الطلب.", "zen.api.error.subscriptionQuotaExceeded": "تم تجاوز حصة الاشتراك. أعد المحاولة خلال {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "تم الوصول إلى حد الاستخدام لمدة 5 ساعات. تتم إعادة التعيين خلال {{retryIn}}. لمواصلة استخدام هذا النموذج الآن، فعّل الاستخدام من رصيدك المتاح: {{consoleGoUrl}}", @@ -385,7 +373,7 @@ export const dict = { "لقد وصلت إلى حد الإنفاق الشهري البالغ ${{amount}}. إدارة حدودك هنا: {{membersUrl}}", "zen.api.error.modelDisabled": "النموذج معطل", "zen.api.error.regionNotAllowed": - "لا يتوفر أحدث إصدار من هذا النموذج إلا مستضافًا في الصين، ويتطلب تفعيلًا صريحًا: {{consoleGoUrl}}", + "هذا النموذج مستضاف في الصين. إذا كنت ترغب في استخدام هذا النموذج، فعّله في إعداداتك: {{consoleGoUrl}}", "zen.api.error.trialEnded": "انتهى العرض المجاني لـ {{model}}. يمكنك مواصلة استخدام النموذج بالاشتراك في OpenCode Go - {{link}}", @@ -675,7 +663,7 @@ export const dict = { "workspace.lite.promo.price": "$5 للشهر الأول", "workspace.lite.promo.modelsTitle": "ما يتضمنه", "workspace.lite.promo.footer": - "صُممت الخطة بشكل أساسي للمستخدمين الدوليين، وتوفر وصولًا عالميًا مستقرًا. قد تتغير الأسعار وحدود الاستخدام بينما نتعلم من الاستخدام المبكر والملاحظات.", + "تم تصميم الخطة بشكل أساسي للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة للحصول على وصول عالمي مستقر. قد تتغير الأسعار وحدود الاستخدام بناءً على تعلمنا من الاستخدام المبكر والملاحظات.", "workspace.lite.promo.subscribe": "الاشتراك في Go", "workspace.lite.promo.subscribing": "جارٍ إعادة التوجيه...", "workspace.lite.promo.otherMethods": "طرق دفع أخرى", diff --git a/packages/console/app/src/i18n/br.ts b/packages/console/app/src/i18n/br.ts index dac06753e9..9df305b8d8 100644 --- a/packages/console/app/src/i18n/br.ts +++ b/packages/console/app/src/i18n/br.ts @@ -256,9 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "seguintes exceções", "go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos", - "go.banner.text": "GPT 5.6 Luna tem limites de uso 2x maiores por tempo limitado", + "go.banner.text": "Kimi K3 tem limites de uso 2x maiores por tempo limitado", "go.meta.description": - "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.", + "O Go começa em $5 no primeiro mês, depois $10/mês, com limites generosos de solicitação de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -306,7 +306,8 @@ export const dict = { "go.problem.item1": "Preço de assinatura de baixo custo", "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": "Uma seleção de modelos testados para codificação com agentes", + "go.problem.item4": + "Inclui Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -332,7 +333,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 a uma seleção de modelos.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -341,21 +342,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "conta", "go.faq.a4.p3": "Cancele a qualquer momento.", "go.faq.q5": "E sobre dados e privacidade?", - "go.faq.a5.model": "Modelo", - "go.faq.a5.training": "Treinamento de modelos", - "go.faq.a5.retention": "Retenção de dados", - "go.faq.a5.retention30": "30 dias", - "go.faq.a5.retention0": "0 dias", - "go.faq.a5.used": "Usado", - "go.faq.a5.notUsed": "Não usado", - "go.faq.a5.noAgreement": "Sem acordo", - "go.faq.a5.grokRetention": - "O ZDR desativa recursos importantes da API que dependem de dados armazenados, incluindo a Responses API com estado, Files and Collections e a Batch API.", - "go.faq.a5.gptRetention": - "Logs de monitoramento de abuso são gerados para todo uso de recursos da API e retidos por até 30 dias.", - "go.faq.a5.learnMore": "Saiba mais", - "go.faq.a5.deepseekRetention": - "O acordo de ZDR é renovado mensalmente. O acordo atual é válido até 31 de agosto de 2026.", + "go.faq.a5.body": + "O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Nossos provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos.", "go.faq.a5.beforeExceptions": "Os modelos Go são hospedados nos EUA. Os provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos, com as", "go.faq.a5.exceptionsLink": "seguintes exceções", @@ -369,7 +357,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 oferece uma seleção de modelos 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -378,7 +366,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Provedor {{provider}} não suportado", "zen.api.error.missingApiKey": "Chave de API ausente.", "zen.api.error.invalidApiKey": "Chave de API inválida.", - "zen.api.error.requestBlockedByUpstreamProvider": "Solicitação bloqueada pelo provedor upstream.", "zen.api.error.subscriptionQuotaExceeded": "Cota de assinatura excedida. Tente novamente em {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Limite de uso de 5 horas atingido. Será reiniciado em {{retryIn}}. Para continuar usando este modelo agora, habilite o uso a partir do seu saldo disponível: {{consoleGoUrl}}", @@ -394,7 +381,7 @@ export const dict = { "Você atingiu seu limite de gastos mensais de ${{amount}}. Gerencie seus limites aqui: {{membersUrl}}", "zen.api.error.modelDisabled": "O modelo está desabilitado", "zen.api.error.regionNotAllowed": - "A versão mais recente deste modelo está disponível apenas com hospedagem na China e requer adesão explícita: {{consoleGoUrl}}", + "Este modelo está hospedado na China. Se você quiser usar este modelo, ative-o nas suas configurações: {{consoleGoUrl}}", "zen.api.error.trialEnded": "A promoção gratuita do {{model}} terminou. Você pode continuar usando o modelo assinando o OpenCode Go - {{link}}", @@ -686,7 +673,7 @@ export const dict = { "workspace.lite.promo.price": "$5 no primeiro mês", "workspace.lite.promo.modelsTitle": "O que está incluído", "workspace.lite.promo.footer": - "O plano foi desenvolvido principalmente para usuários internacionais e oferece acesso global estável. Os preços e limites de uso podem mudar à medida que aprendemos com o uso inicial e o feedback recebido.", + "O plano é projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, UE e Singapura para acesso global estável. Preços e limites de uso podem mudar conforme aprendemos com o uso inicial e feedback.", "workspace.lite.promo.subscribe": "Assinar Go", "workspace.lite.promo.subscribing": "Redirecionando...", "workspace.lite.promo.otherMethods": "Outros métodos de pagamento", diff --git a/packages/console/app/src/i18n/da.ts b/packages/console/app/src/i18n/da.ts index 4e3d230f90..286fa1ea93 100644 --- a/packages/console/app/src/i18n/da.ts +++ b/packages/console/app/src/i18n/da.ts @@ -254,9 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "følgende undtagelser", "go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle", - "go.banner.text": "GPT 5.6 Luna får fordoblet brugsgrænse i en begrænset periode", + "go.banner.text": "Kimi K3 får fordoblet brugsgrænse i en begrænset periode", "go.meta.description": - "Go starter ved $5 for den første måned, derefter $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.", + "Go starter ved $5 for den første måned, derefter $10/måned, med generøse 5-timers anmodningsgrænser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -303,7 +303,8 @@ export const dict = { "go.problem.item1": "Lavpris abonnementspriser", "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": "Et kurateret modeludvalg testet til agentisk kodning", + "go.problem.item4": + "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -329,7 +330,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 et kurateret modeludvalg.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -338,22 +339,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "konto", "go.faq.a4.p3": "Annuller til enhver tid.", "go.faq.q5": "Hvad med data og privatliv?", - "go.faq.a5.model": "Model", - "go.faq.a5.training": "Modeltræning", - "go.faq.a5.retention": "Dataopbevaring", - "go.faq.a5.retention30": "30 dage", - "go.faq.a5.retention0": "0 dage", - "go.faq.a5.used": "Brugt", - "go.faq.a5.notUsed": "Ikke brugt", - "go.faq.a5.noAgreement": "Ingen aftale", - "go.faq.a5.grokRetention": - "ZDR deaktiverer vigtige API-funktioner, der afhænger af lagrede data, herunder den tilstandsbevarende Responses API, Files and Collections og Batch API.", - "go.faq.a5.gptRetention": - "Logfiler til overvågning af misbrug genereres ved al brug af API-funktioner og opbevares i op til 30 dage.", - "go.faq.a5.learnMore": "Læs mere", - "go.faq.a5.deepseekRetention": - "ZDR-aftalen fornyes månedligt. Den nuværende aftale er gyldig til og med 31. august 2026.", - + "go.faq.a5.body": + "Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Vores udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning.", "go.faq.a5.beforeExceptions": "Go-modeller hostes i USA. Udbydere følger en nulopbevaringspolitik og bruger ikke dine data til modeltræning, med de", "go.faq.a5.exceptionsLink": "følgende undtagelser", @@ -366,7 +353,7 @@ export const dict = { "go.faq.q9": "Hvad er forskellen på gratis modeller og Go?", "go.faq.a9": - "Gratis modeller inkluderer Big Pickle plus kampagnemodeller, der er tilgængelige på det pågældende tidspunkt, med en kvote på 200 forespørgsler/dag. Go tilbyder et kurateret modeludvalg med højere forespørgselskvoter håndhævet over rullende perioder (5 timer, ugentligt og månedligt), omtrent svarende til $12 pr. 5 timer, $30 pr. uge og $60 pr. måned (det faktiske antal forespørgsler 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -375,7 +362,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Udbyder {{provider}} understøttes ikke", "zen.api.error.missingApiKey": "Manglende API-nøgle.", "zen.api.error.invalidApiKey": "Ugyldig API-nøgle.", - "zen.api.error.requestBlockedByUpstreamProvider": "Anmodningen blev blokeret af upstream-udbyderen.", "zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igen om {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Forbrugsgrænsen for 5 timer er nået. Nulstilles om {{retryIn}}. For at fortsætte med at bruge denne model nu, aktivér forbrug fra din tilgængelige saldo: {{consoleGoUrl}}", @@ -391,7 +377,7 @@ export const dict = { "Du har nået din månedlige forbrugsgrænse på ${{amount}}. Administrer dine grænser her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktiveret", "zen.api.error.regionNotAllowed": - "Den nyeste version af denne model er kun tilgængelig som hostet i Kina og kræver, at du aktivt tilvælger den: {{consoleGoUrl}}", + "Denne model hostes i Kina. Hvis du vil bruge denne model, skal du aktivere den i dine indstillinger: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Den gratis kampagne for {{model}} er afsluttet. Du kan fortsætte med at bruge modellen ved at abonnere på OpenCode Go - {{link}}", @@ -683,7 +669,7 @@ export const dict = { "workspace.lite.promo.price": "$5 for den første måned", "workspace.lite.promo.modelsTitle": "Hvad er inkluderet", "workspace.lite.promo.footer": - "Planen er primært udviklet til internationale brugere og giver stabil adgang i hele verden. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af de første brugserfaringer og tilbagemeldinger.", + "Planen er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for stabil global adgang. Priser og forbrugsgrænser kan ændre sig, efterhånden som vi lærer af tidlig brug og feedback.", "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", "workspace.lite.promo.otherMethods": "Andre betalingsmetoder", diff --git a/packages/console/app/src/i18n/de.ts b/packages/console/app/src/i18n/de.ts index 61184358da..59abd02cc1 100644 --- a/packages/console/app/src/i18n/de.ts +++ b/packages/console/app/src/i18n/de.ts @@ -256,9 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "folgenden Ausnahmen", "go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle", - "go.banner.text": "GPT 5.6 Luna erhält für begrenzte Zeit 2x Nutzungslimits", + "go.banner.text": "Kimi K3 erhält für begrenzte Zeit 2x Nutzungslimits", "go.meta.description": - "Go beginnt bei $5 für deinen ersten Monat, danach $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.", + "Go beginnt bei $5 für den ersten Monat, danach $10/Monat, mit großzügigen 5-Stunden-Anfragelimits für Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -305,7 +305,8 @@ export const dict = { "go.problem.item1": "Kostengünstiges Abonnement", "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": "Eine kuratierte, für Agentic Coding getestete Modellauswahl", + "go.problem.item4": + "Beinhaltet Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -331,7 +332,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 deinen ersten Monat beginnt, danach $10/Monat, mit großzügigen Limits und zuverlässigem Zugang zu einer kuratierten Modellauswahl.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -340,21 +341,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "Konto verwalten", "go.faq.a4.p3": "Jederzeit kündbar.", "go.faq.q5": "Was ist mit Daten und Privatsphäre?", - "go.faq.a5.model": "Modell", - "go.faq.a5.training": "Modelltraining", - "go.faq.a5.retention": "Datenaufbewahrung", - "go.faq.a5.retention30": "30 Tage", - "go.faq.a5.retention0": "0 Tage", - "go.faq.a5.used": "Verwendet", - "go.faq.a5.notUsed": "Nicht verwendet", - "go.faq.a5.noAgreement": "Keine Vereinbarung", - "go.faq.a5.grokRetention": - "ZDR deaktiviert wichtige API-Funktionen, die von gespeicherten Daten abhängen, einschließlich der zustandsbehafteten Responses API, Files and Collections und der Batch API.", - "go.faq.a5.gptRetention": - "Für die Nutzung aller API-Funktionen werden Protokolle zur Missbrauchsüberwachung erstellt und bis zu 30 Tage lang aufbewahrt.", - "go.faq.a5.learnMore": "Mehr erfahren", - "go.faq.a5.deepseekRetention": - "Die ZDR-Vereinbarung wird monatlich erneuert. Die aktuelle Vereinbarung gilt bis einschließlich 31. August 2026.", + "go.faq.a5.body": + "Der Plan ist primär für internationale Nutzer konzipiert, mit Modellen gehostet in den USA, der EU und Singapur für stabilen globalen Zugang. Unsere Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen.", "go.faq.a5.beforeExceptions": "Go-Modelle werden in den USA gehostet. Anbieter verfolgen eine Zero-Retention-Politik und nutzen deine Daten nicht für das Training von Modellen, mit den", "go.faq.a5.exceptionsLink": "folgenden Ausnahmen", @@ -368,7 +356,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 bietet eine kuratierte Modellauswahl 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -377,7 +365,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Anbieter {{provider}} wird nicht unterstützt", "zen.api.error.missingApiKey": "Fehlender API-Key.", "zen.api.error.invalidApiKey": "Ungültiger API-Key.", - "zen.api.error.requestBlockedByUpstreamProvider": "Anfrage vom vorgelagerten Anbieter blockiert.", "zen.api.error.subscriptionQuotaExceeded": "Abonnement-Quote überschritten. Erneuter Versuch in {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5-Stunden-Nutzungslimit erreicht. Wird in {{retryIn}} zurückgesetzt. Um dieses Modell jetzt weiter zu nutzen, aktiviere die Nutzung über dein verfügbares Guthaben: {{consoleGoUrl}}", @@ -393,7 +380,7 @@ export const dict = { "Du hast dein monatliches Ausgabenlimit von ${{amount}} erreicht. Verwalte deine Limits hier: {{membersUrl}}", "zen.api.error.modelDisabled": "Modell ist deaktiviert", "zen.api.error.regionNotAllowed": - "Die neueste Version dieses Modells ist nur verfügbar, wenn sie in China gehostet wird, und muss ausdrücklich aktiviert werden: {{consoleGoUrl}}", + "Dieses Modell wird in China gehostet. Wenn du dieses Modell verwenden möchtest, aktiviere es in deinen Einstellungen: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Die kostenlose Aktion für {{model}} ist beendet. Du kannst das Modell weiterhin nutzen, indem du OpenCode Go abonnierst - {{link}}", @@ -685,7 +672,7 @@ export const dict = { "workspace.lite.promo.price": "$5 im ersten Monat", "workspace.lite.promo.modelsTitle": "Was enthalten ist", "workspace.lite.promo.footer": - "Der Plan richtet sich in erster Linie an internationale Nutzer und bietet stabilen weltweiten Zugriff. Preise und Nutzungslimits können sich ändern, wenn wir Erkenntnisse aus der ersten Nutzung und dem Feedback gewinnen.", + "Der Plan wurde hauptsächlich für internationale Nutzer entwickelt, wobei die Modelle in den USA, der EU und Singapur gehostet werden, um einen stabilen weltweiten Zugriff zu gewährleisten. Preise und Nutzungslimits können sich ändern, während wir aus der frühen Nutzung und dem Feedback lernen.", "workspace.lite.promo.subscribe": "Go abonnieren", "workspace.lite.promo.subscribing": "Leite weiter...", "workspace.lite.promo.otherMethods": "Andere Zahlungsmethoden", diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts index 813473c367..413037022a 100644 --- a/packages/console/app/src/i18n/en.ts +++ b/packages/console/app/src/i18n/en.ts @@ -253,9 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "following exceptions", "go.title": "OpenCode Go | Low cost coding models for everyone", - "go.banner.text": "GPT 5.6 Luna gets 2× usage limits for a limited time", + "go.banner.text": "Kimi K3 gets 2× usage limits for a limited time", "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous usage limits and reliable access to leading coding models.", + "Go starts at $5 for your first month, then $10/month, with generous 5-hour request limits for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, and DeepSeek V4 Flash.", "go.hero.title": "Low cost coding models for everyone", "go.hero.body": "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", @@ -270,7 +270,6 @@ export const dict = { "go.graph.freePill": "Big Pickle and free models", "go.graph.go": "Go", "go.graph.label": "Requests per 5 hour", - "go.graph.tick": "{{n}}x", "go.graph.usageLimits": "Usage limits", "go.graph.aria": "Requests per 5h: {{free}} vs {{go}}", @@ -302,7 +301,8 @@ export const dict = { "go.problem.item1": "Low cost subscription pricing", "go.problem.item2": "Generous limits and reliable access", "go.problem.item3": "Built for as many programmers as possible", - "go.problem.item4": "A curated model lineup tested for agentic coding", + "go.problem.item4": + "Includes Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -327,7 +327,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 a curated model lineup.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -336,21 +336,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "account", "go.faq.a4.p3": "Cancel any time.", "go.faq.q5": "What about data and privacy?", - "go.faq.a5.model": "Model", - "go.faq.a5.training": "Model training", - "go.faq.a5.retention": "Data retention", - "go.faq.a5.retention30": "30 days", - "go.faq.a5.retention0": "0 days", - "go.faq.a5.used": "Used", - "go.faq.a5.notUsed": "Not used", - "go.faq.a5.noAgreement": "No agreement", - "go.faq.a5.grokRetention": - "ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API.", - "go.faq.a5.gptRetention": - "Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days.", - "go.faq.a5.deepseekRetention": - "ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026.", - "go.faq.a5.learnMore": "Learn more", + "go.faq.a5.body": + "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Our providers follow a zero-retention policy and do not use your data for model training.", "go.faq.a5.beforeExceptions": "Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the", @@ -364,7 +351,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 offers a curated model lineup 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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}} is not supported", @@ -373,7 +360,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Provider {{provider}} not supported", "zen.api.error.missingApiKey": "Missing API key.", "zen.api.error.invalidApiKey": "Invalid API key.", - "zen.api.error.requestBlockedByUpstreamProvider": "Request blocked by upstream provider.", "zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5-hour usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}", @@ -389,7 +375,7 @@ export const dict = { "You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}", "zen.api.error.modelDisabled": "Model is disabled", "zen.api.error.regionNotAllowed": - "The latest version of this model is only available hosted in China and requires explicit opt in: {{consoleGoUrl}}", + "This model is hosted in China. If you would like to use this model, enable it in your settings: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}", @@ -681,7 +667,7 @@ export const dict = { "workspace.lite.promo.price": "$5 for your first month", "workspace.lite.promo.modelsTitle": "What's Included", "workspace.lite.promo.footer": - "The plan is designed primarily for international users and provides stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", + "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", "workspace.lite.promo.subscribe": "Subscribe to Go", "workspace.lite.promo.subscribing": "Redirecting...", "workspace.lite.promo.otherMethods": "Other payment methods", diff --git a/packages/console/app/src/i18n/es.ts b/packages/console/app/src/i18n/es.ts index be5c24685c..544baaa185 100644 --- a/packages/console/app/src/i18n/es.ts +++ b/packages/console/app/src/i18n/es.ts @@ -257,9 +257,9 @@ export const dict = { "zen.privacy.exceptionsLink": "siguientes excepciones", "go.title": "OpenCode Go | Modelos de programación de bajo coste para todos", - "go.banner.text": "GPT 5.6 Luna tiene límites de uso 2x mayores por tiempo limitado", + "go.banner.text": "Kimi K3 tiene límites de uso 2x mayores por tiempo limitado", "go.meta.description": - "Go comienza en $5 el primer mes, luego 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.", + "Go comienza en $5 el primer mes, luego 10 $/mes, con generosos límites de solicitudes de 5 horas para Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -307,7 +307,8 @@ export const dict = { "go.problem.item1": "Precios de suscripción de bajo coste", "go.problem.item2": "Límites generosos y acceso fiable", "go.problem.item3": "Creado para tantos programadores como sea posible", - "go.problem.item4": "Una selección de modelos probados para programación agéntica", + "go.problem.item4": + "Incluye Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -332,7 +333,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 de pago por uso, mientras que Go comienza en $5 el primer mes, luego 10 $/mes, con límites generosos y acceso fiable a una selección de modelos.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -341,21 +342,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "cuenta", "go.faq.a4.p3": "Cancela en cualquier momento.", "go.faq.q5": "¿Qué pasa con los datos y la privacidad?", - "go.faq.a5.model": "Modelo", - "go.faq.a5.training": "Entrenamiento del modelo", - "go.faq.a5.retention": "Retención de datos", - "go.faq.a5.retention30": "30 días", - "go.faq.a5.retention0": "0 días", - "go.faq.a5.used": "Utilizado", - "go.faq.a5.notUsed": "No utilizado", - "go.faq.a5.noAgreement": "Sin acuerdo", - "go.faq.a5.grokRetention": - "ZDR deshabilita funciones importantes de la API que dependen de datos almacenados, incluidas la Responses API con estado, Files and Collections y la Batch API.", - "go.faq.a5.gptRetention": - "Se generan registros de supervisión de abusos para todo el uso de funciones de la API y se conservan durante un máximo de 30 días.", - "go.faq.a5.learnMore": "Más información", - "go.faq.a5.deepseekRetention": - "El acuerdo de ZDR se renueva mensualmente. El acuerdo actual es válido hasta el 31 de agosto de 2026.", + "go.faq.a5.body": + "El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., UE y Singapur para un acceso global estable. Nuestros proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos.", "go.faq.a5.beforeExceptions": "Los modelos de Go están alojados en EE. UU. Los proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos, con las", "go.faq.a5.exceptionsLink": "siguientes excepciones", @@ -369,7 +357,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 y los modelos promocionales disponibles en ese momento, con una cuota de 200 solicitudes/día. Go ofrece una selección de modelos con cuotas de solicitudes más altas aplicadas en ventanas móviles (de 5 horas, semanales y mensuales), aproximadamente equivalentes a 12 $ por 5 horas, 30 $ por semana y 60 $ por mes (la cantidad real de solicitudes varía 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -378,7 +366,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Proveedor {{provider}} no soportado", "zen.api.error.missingApiKey": "Falta la clave API.", "zen.api.error.invalidApiKey": "Clave API inválida.", - "zen.api.error.requestBlockedByUpstreamProvider": "El proveedor externo bloqueó la solicitud.", "zen.api.error.subscriptionQuotaExceeded": "Cuota de suscripción excedida. Reintenta en {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Límite de uso de 5 horas alcanzado. Se restablece en {{retryIn}}. Para seguir usando este modelo ahora, habilita el uso desde tu saldo disponible: {{consoleGoUrl}}", @@ -394,7 +381,7 @@ export const dict = { "Has alcanzado tu límite de gasto mensual de ${{amount}}. Gestiona tus límites aquí: {{membersUrl}}", "zen.api.error.modelDisabled": "El modelo está deshabilitado", "zen.api.error.regionNotAllowed": - "La versión más reciente de este modelo solo está disponible alojada en China y requiere una aceptación explícita: {{consoleGoUrl}}", + "Este modelo está alojado en China. Si quieres usar este modelo, actívalo en tu configuración: {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promoción gratuita de {{model}} ha finalizado. Puedes seguir usando el modelo suscribiéndote a OpenCode Go - {{link}}", @@ -686,7 +673,7 @@ export const dict = { "workspace.lite.promo.price": "$5 el primer mes", "workspace.lite.promo.modelsTitle": "Qué incluye", "workspace.lite.promo.footer": - "El plan está diseñado principalmente para usuarios internacionales y ofrece un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y de los comentarios recibidos.", + "El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. Los precios y los límites de uso pueden cambiar a medida que aprendemos del uso inicial y los comentarios.", "workspace.lite.promo.subscribe": "Suscribirse a Go", "workspace.lite.promo.subscribing": "Redirigiendo...", "workspace.lite.promo.otherMethods": "Otros métodos de pago", diff --git a/packages/console/app/src/i18n/fr.ts b/packages/console/app/src/i18n/fr.ts index 37bf2d5eb3..0219952e9d 100644 --- a/packages/console/app/src/i18n/fr.ts +++ b/packages/console/app/src/i18n/fr.ts @@ -258,9 +258,9 @@ export const dict = { "zen.privacy.exceptionsLink": "exceptions suivantes", "go.title": "OpenCode Go | Modèles de code à faible coût pour tous", - "go.banner.text": "GPT 5.6 Luna bénéficie de limites d’utilisation 2x supérieures pour une durée limitée", + "go.banner.text": "Kimi K3 bénéficie de limites d’utilisation 2x supérieures pour une durée limitée", "go.meta.description": - "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.", + "Go commence à $5 pour le premier mois, puis 10 $/mois, avec des limites de requêtes généreuses sur 5 heures pour Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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é.", @@ -307,7 +307,8 @@ export const dict = { "go.problem.item1": "Prix d'abonnement bas", "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": "Une sélection de modèles testés pour le codage agentique", + "go.problem.item4": + "Inclut Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -333,7 +334,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 à une sélection de modèles.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -342,22 +343,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "compte", "go.faq.a4.p3": "Annulez à tout moment.", "go.faq.q5": "Et pour les données et la confidentialité ?", - "go.faq.a5.model": "Modèle", - "go.faq.a5.training": "Entraînement des modèles", - "go.faq.a5.retention": "Conservation des données", - "go.faq.a5.retention30": "30 jours", - "go.faq.a5.retention0": "0 jour", - "go.faq.a5.used": "Utilisé", - "go.faq.a5.notUsed": "Non utilisé", - "go.faq.a5.noAgreement": "Aucun accord", - "go.faq.a5.grokRetention": - "Le ZDR désactive d'importantes fonctionnalités API qui dépendent des données stockées, notamment Responses API avec état, Files and Collections et Batch API.", - "go.faq.a5.gptRetention": - "Des journaux de surveillance des abus sont générés pour toute utilisation des fonctionnalités API et conservés pendant un maximum de 30 jours.", - "go.faq.a5.learnMore": "En savoir plus", - "go.faq.a5.deepseekRetention": - "L’accord ZDR est renouvelé chaque mois. L’accord actuel est valable jusqu’au 31 août 2026.", - + "go.faq.a5.body": + "Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Nos fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles.", "go.faq.a5.beforeExceptions": "Les modèles Go sont hébergés aux États-Unis. Les fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour l'entraînement des modèles, avec les", "go.faq.a5.exceptionsLink": "exceptions suivantes", @@ -370,7 +357,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 les modèles promotionnels disponibles à ce moment-là, avec un quota de 200 requêtes/jour. Go propose une sélection de modèles avec des quotas de requêtes plus élevés appliqués sur des fenêtres glissantes (5 heures, hebdomadaire et mensuelle), à peu près équivalents à 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -379,7 +366,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Fournisseur {{provider}} non pris en charge", "zen.api.error.missingApiKey": "Clé API manquante.", "zen.api.error.invalidApiKey": "Clé API invalide.", - "zen.api.error.requestBlockedByUpstreamProvider": "Requête bloquée par le fournisseur en amont.", "zen.api.error.subscriptionQuotaExceeded": "Quota d'abonnement dépassé. Réessayez dans {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Limite d'utilisation sur 5 heures atteinte. Réinitialisation dans {{retryIn}}. Pour continuer à utiliser ce modèle dès maintenant, activez l'utilisation depuis votre solde disponible : {{consoleGoUrl}}", @@ -395,7 +381,7 @@ export const dict = { "Vous avez atteint votre limite de dépense mensuelle de {{amount}} $. Gérez vos limites ici : {{membersUrl}}", "zen.api.error.modelDisabled": "Le modèle est désactivé", "zen.api.error.regionNotAllowed": - "La dernière version de ce modèle est uniquement disponible avec un hébergement en Chine et nécessite votre consentement explicite : {{consoleGoUrl}}", + "Ce modèle est hébergé en Chine. Si vous souhaitez utiliser ce modèle, activez-le dans vos paramètres : {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promotion gratuite de {{model}} est terminée. Vous pouvez continuer à utiliser le modèle en vous abonnant à OpenCode Go - {{link}}", @@ -693,7 +679,7 @@ export const dict = { "workspace.lite.promo.price": "$5 le premier mois", "workspace.lite.promo.modelsTitle": "Ce qui est inclus", "workspace.lite.promo.footer": - "Ce forfait est principalement conçu pour les utilisateurs internationaux et offre un accès mondial stable. Les tarifs et les limites d'utilisation peuvent évoluer à mesure que nous tirons les enseignements des premières utilisations et des retours reçus.", + "Le plan est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. Les tarifs et les limites d'utilisation peuvent changer à mesure que nous apprenons des premières utilisations et des commentaires.", "workspace.lite.promo.subscribe": "S'abonner à Go", "workspace.lite.promo.subscribing": "Redirection...", "workspace.lite.promo.otherMethods": "Autres méthodes de paiement", diff --git a/packages/console/app/src/i18n/it.ts b/packages/console/app/src/i18n/it.ts index 501d012333..06a643508a 100644 --- a/packages/console/app/src/i18n/it.ts +++ b/packages/console/app/src/i18n/it.ts @@ -254,9 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "seguenti eccezioni", "go.title": "OpenCode Go | Modelli di coding a basso costo per tutti", - "go.banner.text": "GPT 5.6 Luna offre limiti di utilizzo 2x superiori per un periodo limitato", + "go.banner.text": "Kimi K3 offre limiti di utilizzo 2x superiori per un periodo limitato", "go.meta.description": - "Go inizia a $5 per il primo mese, poi $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.", + "Go inizia a $5 per il primo mese, poi $10/mese, con generosi limiti di richiesta di 5 ore per Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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à.", @@ -303,7 +303,8 @@ export const dict = { "go.problem.item1": "Prezzo di abbonamento a basso costo", "go.problem.item2": "Limiti generosi e accesso affidabile", "go.problem.item3": "Costruito per il maggior numero possibile di programmatori", - "go.problem.item4": "Una selezione curata di modelli testati per il coding agentico", + "go.problem.item4": + "Include Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -328,7 +329,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 un accesso affidabile a una selezione curata di modelli.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -337,21 +338,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "account", "go.faq.a4.p3": "Annulla in qualsiasi momento.", "go.faq.q5": "E per quanto riguarda dati e privacy?", - "go.faq.a5.model": "Modello", - "go.faq.a5.training": "Addestramento del modello", - "go.faq.a5.retention": "Conservazione dei dati", - "go.faq.a5.retention30": "30 giorni", - "go.faq.a5.retention0": "0 giorni", - "go.faq.a5.used": "Utilizzato", - "go.faq.a5.notUsed": "Non utilizzato", - "go.faq.a5.noAgreement": "Nessun accordo", - "go.faq.a5.grokRetention": - "ZDR disabilita importanti funzionalità API che dipendono dai dati archiviati, tra cui la Responses API con stato, Files and Collections e Batch API.", - "go.faq.a5.gptRetention": - "I log di monitoraggio degli abusi vengono generati per l'utilizzo di tutte le funzionalità API e conservati per un massimo di 30 giorni.", - "go.faq.a5.learnMore": "Scopri di più", - "go.faq.a5.deepseekRetention": - "L'accordo ZDR viene rinnovato mensilmente. L'accordo attuale è valido fino al 31 agosto 2026.", + "go.faq.a5.body": + "Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, UE e Singapore per un accesso globale stabile. I nostri provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli.", "go.faq.a5.beforeExceptions": "I modelli Go sono ospitati negli Stati Uniti. I provider seguono una policy di zero-retention e non usano i tuoi dati per l'addestramento dei modelli, con le", "go.faq.a5.exceptionsLink": "seguenti eccezioni", @@ -365,7 +353,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ù i modelli promozionali disponibili al momento, con una quota di 200 richieste/giorno. Go offre una selezione curata di modelli 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -374,7 +362,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Provider {{provider}} non supportato", "zen.api.error.missingApiKey": "Chiave API mancante.", "zen.api.error.invalidApiKey": "Chiave API non valida.", - "zen.api.error.requestBlockedByUpstreamProvider": "Richiesta bloccata dal provider upstream.", "zen.api.error.subscriptionQuotaExceeded": "Quota dell'abbonamento superata. Riprova tra {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Limite di utilizzo di 5 ore raggiunto. Si reimposta tra {{retryIn}}. Per continuare a usare questo modello ora, abilita l'utilizzo dal tuo saldo disponibile: {{consoleGoUrl}}", @@ -390,7 +377,7 @@ export const dict = { "Hai raggiunto il tuo limite di spesa mensile di ${{amount}}. Gestisci i tuoi limiti qui: {{membersUrl}}", "zen.api.error.modelDisabled": "Il modello è disabilitato", "zen.api.error.regionNotAllowed": - "La versione più recente di questo modello è disponibile solo con hosting in Cina e richiede un consenso esplicito: {{consoleGoUrl}}", + "Questo modello è ospitato in Cina. Se vuoi usare questo modello, abilitalo nelle tue impostazioni: {{consoleGoUrl}}", "zen.api.error.trialEnded": "La promozione gratuita di {{model}} è terminata. Puoi continuare a usare il modello abbonandoti a OpenCode Go - {{link}}", @@ -684,7 +671,7 @@ export const dict = { "workspace.lite.promo.price": "$5 il primo mese", "workspace.lite.promo.modelsTitle": "Cosa è incluso", "workspace.lite.promo.footer": - "Il piano è pensato principalmente per gli utenti internazionali e offre un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare in base a quanto apprenderemo dall'utilizzo iniziale e dai feedback.", + "Il piano è progettato principalmente per gli utenti internazionali, con modelli ospitati in US, EU e Singapore per un accesso globale stabile. I prezzi e i limiti di utilizzo potrebbero cambiare man mano che impariamo dall'utilizzo iniziale e dal feedback.", "workspace.lite.promo.subscribe": "Abbonati a Go", "workspace.lite.promo.subscribing": "Reindirizzamento...", "workspace.lite.promo.otherMethods": "Altri metodi di pagamento", diff --git a/packages/console/app/src/i18n/ja.ts b/packages/console/app/src/i18n/ja.ts index e8f8fa5661..a15acfb884 100644 --- a/packages/console/app/src/i18n/ja.ts +++ b/packages/console/app/src/i18n/ja.ts @@ -253,9 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "以下の例外", "go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル", - "go.banner.text": "GPT 5.6 Lunaの利用上限が期間限定で2倍に", + "go.banner.text": "Kimi K3の利用上限が期間限定で2倍に", "go.meta.description": - "Goは最初の月$5、その後$10/月で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。", + "Goは最初の月$5、その後$10/月で、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashに対して5時間のゆとりあるリクエスト上限があります。", "go.hero.title": "すべての人のための低価格なコーディングモデル", "go.hero.body": "Goは、世界中のプログラマーにエージェント型コーディングをもたらします。最も高性能なオープンソースモデルへの十分な制限と安定したアクセスを提供し、コストや可用性を気にすることなく強力なエージェントで構築できます。", @@ -303,7 +303,8 @@ export const dict = { "go.problem.item1": "低価格なサブスクリプション料金", "go.problem.item2": "十分な制限と安定したアクセス", "go.problem.item3": "できるだけ多くのプログラマーのために構築", - "go.problem.item4": "エージェント型コーディング向けにテストされた厳選モデルラインナップ", + "go.problem.item4": + "Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashを含む", "go.how.title": "Goの仕組み", "go.how.body": "Goは最初の月$5、その後$10/月で始まります。OpenCodeまたは任意のエージェントで使えます。", "go.how.step1.title": "アカウントを作成", @@ -328,7 +329,7 @@ export const dict = { "go.faq.a2": "Go には、十分な利用上限と安定したアクセスを備えた、以下のモデルが含まれます。", "go.faq.q3": "GoはZenと同じですか?", "go.faq.a3": - "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で、厳選されたモデルラインナップへのゆとりある上限と安定したアクセスを提供します。", + "いいえ。Zenは従量課金制ですが、Goは最初の月$5、その後$10/月で始まり、Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashのオープンソースモデルに対して、ゆとりある上限と信頼できるアクセスを提供します。", "go.faq.q4": "Goの料金は?", "go.faq.a4.p1.beforePricing": "Goは", "go.faq.a4.p1.pricingLink": "最初の月$5", @@ -337,19 +338,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "アカウント", "go.faq.a4.p3": "いつでもキャンセル可能です。", "go.faq.q5": "データとプライバシーは?", - "go.faq.a5.model": "モデル", - "go.faq.a5.training": "モデルのトレーニング", - "go.faq.a5.retention": "データ保持", - "go.faq.a5.retention30": "30日", - "go.faq.a5.retention0": "0日", - "go.faq.a5.used": "使用あり", - "go.faq.a5.notUsed": "使用なし", - "go.faq.a5.noAgreement": "契約なし", - "go.faq.a5.grokRetention": - "ZDRでは、保存データに依存する重要なAPI機能(ステートフルなResponses API、Files and Collections、Batch APIなど)が無効になります。", - "go.faq.a5.gptRetention": "不正使用監視ログはすべてのAPI機能の使用時に生成され、最大30日間保持されます。", - "go.faq.a5.learnMore": "詳しく見る", - "go.faq.a5.deepseekRetention": "ZDR契約は毎月更新されます。現在の契約は2026年8月31日まで有効です。", + "go.faq.a5.body": + "このプランは主に海外ユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。プロバイダーはゼロ保持ポリシーに従い、お客様のデータをモデルのトレーニングに使用しません。", "go.faq.a5.beforeExceptions": "Goのモデルは米国でホストされています。プロバイダーはゼロ保持ポリシーに従い、モデルのトレーニングにデータを使用しません(", "go.faq.a5.exceptionsLink": "以下の例外", @@ -363,7 +353,7 @@ export const dict = { "go.faq.q9": "無料モデルとGoの違いは何ですか?", "go.faq.a9": - "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。Goでは厳選されたモデルラインナップを利用でき、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", + "無料モデルにはBig Pickleと、その時点で利用可能なプロモーションモデルが含まれ、1日200リクエストの制限があります。GoにはGrok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro、DeepSeek V4 Flashが含まれ、ローリングウィンドウ(5時間、週間、月間)全体でより高いリクエスト制限が適用されます。これは概算で5時間あたり$12、週間$30、月間$60相当です(実際のリクエスト数はモデルと使用状況により異なります)。", "zen.api.error.rateLimitExceeded": "レート制限を超えました。後でもう一度お試しください。", "zen.api.error.modelNotSupported": "モデル {{model}} はサポートされていません", @@ -372,7 +362,6 @@ export const dict = { "zen.api.error.providerNotSupported": "プロバイダー {{provider}} はサポートされていません", "zen.api.error.missingApiKey": "APIキーがありません。", "zen.api.error.invalidApiKey": "無効なAPIキーです。", - "zen.api.error.requestBlockedByUpstreamProvider": "上流プロバイダーによりリクエストがブロックされました。", "zen.api.error.subscriptionQuotaExceeded": "サブスクリプションの制限を超えました。{{retryIn}} 後に再試行してください。", "zen.api.error.goSubscriptionRollingLimitExceeded": @@ -389,7 +378,7 @@ export const dict = { "月額の利用上限 ${{amount}} に達しました。こちらから上限を管理してください: {{membersUrl}}", "zen.api.error.modelDisabled": "モデルが無効です", "zen.api.error.regionNotAllowed": - "このモデルの最新バージョンは中国でのみホスト提供されており、利用するには明示的なオプトインが必要です: {{consoleGoUrl}}", + "このモデルは中国でホストされています。このモデルを使用したい場合は、設定で有効にしてください: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} の無料プロモーションは終了しました。OpenCode Go を購読するとモデルを引き続き使用できます - {{link}}", @@ -682,7 +671,7 @@ export const dict = { "workspace.lite.promo.price": "初月$5", "workspace.lite.promo.modelsTitle": "含まれるもの", "workspace.lite.promo.footer": - "このプランは主に海外のユーザー向けに設計されており、世界中から安定してご利用いただけます。料金と利用上限は、初期の利用状況やフィードバックを踏まえて変更される場合があります。", + "このプランは主にグローバルユーザー向けに設計されており、米国、EU、シンガポールでホストされたモデルにより安定したグローバルアクセスを提供します。料金と利用制限は、初期の利用状況やフィードバックに基づいて変更される可能性があります。", "workspace.lite.promo.subscribe": "Goを購読する", "workspace.lite.promo.subscribing": "リダイレクト中...", "workspace.lite.promo.otherMethods": "その他の支払い方法", diff --git a/packages/console/app/src/i18n/ko.ts b/packages/console/app/src/i18n/ko.ts index ab76958985..2ade66381a 100644 --- a/packages/console/app/src/i18n/ko.ts +++ b/packages/console/app/src/i18n/ko.ts @@ -250,9 +250,9 @@ export const dict = { "zen.privacy.exceptionsLink": "다음 예외", "go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델", - "go.banner.text": "GPT 5.6 Luna 사용 한도가 한시적으로 2배 확대됩니다", + "go.banner.text": "Kimi K3 사용 한도가 한시적으로 2배 확대됩니다", "go.meta.description": - "Go는 첫 달 $5, 이후 $10/월로 시작하며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.", + "Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash에 대해 넉넉한 5시간 요청 한도를 제공합니다.", "go.hero.title": "모두를 위한 저비용 코딩 모델", "go.hero.body": "Go는 전 세계 프로그래머들에게 에이전트 코딩을 제공합니다. 가장 유능한 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공하므로, 비용이나 가용성 걱정 없이 강력한 에이전트로 빌드할 수 있습니다.", @@ -300,7 +300,8 @@ export const dict = { "go.problem.item1": "저렴한 구독 가격", "go.problem.item2": "넉넉한 한도와 안정적인 액세스", "go.problem.item3": "가능한 한 많은 프로그래머를 위해 제작됨", - "go.problem.item4": "에이전트 코딩용으로 테스트된 엄선된 모델 라인업", + "go.problem.item4": + "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 포함", "go.how.title": "Go 작동 방식", "go.how.body": "Go는 첫 달 $5, 이후 $10/월로 시작합니다. OpenCode 또는 어떤 에이전트와도 함께 사용할 수 있습니다.", "go.how.step1.title": "계정 생성", @@ -324,7 +325,7 @@ export const dict = { "go.faq.a2": "Go에는 넉넉한 한도와 안정적인 액세스를 제공하는 아래 모델이 포함됩니다.", "go.faq.q3": "Go는 Zen과 같은가요?", "go.faq.a3": - "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, 엄선된 모델 라인업에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", + "아니요. Zen은 종량제인 반면, Go는 첫 달 $5, 이후 $10/월로 시작하며, Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash 오픈 소스 모델에 대한 넉넉한 한도와 안정적인 액세스를 제공합니다.", "go.faq.q4": "Go 비용은 얼마인가요?", "go.faq.a4.p1.beforePricing": "Go 비용은", "go.faq.a4.p1.pricingLink": "첫 달 $5", @@ -333,19 +334,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "계정", "go.faq.a4.p3": "언제든지 취소할 수 있습니다.", "go.faq.q5": "데이터와 프라이버시는 어떤가요?", - "go.faq.a5.model": "모델", - "go.faq.a5.training": "모델 학습", - "go.faq.a5.retention": "데이터 보존", - "go.faq.a5.retention30": "30일", - "go.faq.a5.retention0": "0일", - "go.faq.a5.used": "사용됨", - "go.faq.a5.notUsed": "사용되지 않음", - "go.faq.a5.noAgreement": "합의 없음", - "go.faq.a5.grokRetention": - "ZDR은 저장된 데이터에 의존하는 중요한 API 기능(상태 저장형 Responses API, Files and Collections, Batch API 포함)을 비활성화합니다.", - "go.faq.a5.gptRetention": "모든 API 기능 사용에 대해 악용 모니터링 로그가 생성되며 최대 30일 동안 보존됩니다.", - "go.faq.a5.learnMore": "자세히 알아보기", - "go.faq.a5.deepseekRetention": "ZDR 계약은 매월 갱신됩니다. 현재 계약은 2026년 8월 31일까지 유효합니다.", + "go.faq.a5.body": + "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU, 싱가포르에 모델이 호스팅되어 있습니다. 당사의 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다.", "go.faq.a5.beforeExceptions": "Go 모델은 미국에서 호스팅됩니다. 제공자들은 데이터 보존 금지 정책을 따르며 모델 학습에 데이터를 사용하지 않습니다. 단,", "go.faq.a5.exceptionsLink": "다음 예외", @@ -358,7 +348,7 @@ export const dict = { "go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?", "go.faq.a9": - "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 엄선된 모델 라인업을 제공하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", + "무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro, DeepSeek V4 Flash를 포함하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 이는 대략 5시간당 $12, 주당 $30, 월 $60에 해당합니다(실제 요청 수는 모델 및 사용량에 따라 다름).", "zen.api.error.rateLimitExceeded": "속도 제한을 초과했습니다. 나중에 다시 시도해 주세요.", "zen.api.error.modelNotSupported": "{{model}} 모델은 지원되지 않습니다", @@ -367,7 +357,6 @@ export const dict = { "zen.api.error.providerNotSupported": "{{provider}} 제공자는 지원되지 않습니다", "zen.api.error.missingApiKey": "API 키가 누락되었습니다.", "zen.api.error.invalidApiKey": "유효하지 않은 API 키입니다.", - "zen.api.error.requestBlockedByUpstreamProvider": "상위 제공자가 요청을 차단했습니다.", "zen.api.error.subscriptionQuotaExceeded": "구독 할당량을 초과했습니다. {{retryIn}} 후 다시 시도해 주세요.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5시간 사용 한도에 도달했습니다. {{retryIn}} 후 초기화됩니다. 이 모델을 지금 계속 사용하려면 사용 가능한 잔액에서 사용을 활성화하세요: {{consoleGoUrl}}", @@ -383,7 +372,7 @@ export const dict = { "월간 지출 한도인 ${{amount}}에 도달했습니다. 한도 관리를 여기서 하세요: {{membersUrl}}", "zen.api.error.modelDisabled": "모델이 비활성화되었습니다", "zen.api.error.regionNotAllowed": - "이 모델의 최신 버전은 중국에서 호스팅되는 경우에만 사용할 수 있으며, 명시적으로 사용에 동의해야 합니다: {{consoleGoUrl}}", + "이 모델은 중국에서 호스팅됩니다. 이 모델을 사용하려면 설정에서 활성화하세요: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}}의 무료 프로모션이 종료되었습니다. OpenCode Go를 구독하면 모델을 계속 사용할 수 있습니다 - {{link}}", @@ -674,7 +663,7 @@ export const dict = { "workspace.lite.promo.price": "첫 달 $5", "workspace.lite.promo.modelsTitle": "포함 내역", "workspace.lite.promo.footer": - "이 플랜은 주로 해외 사용자를 위해 설계되었으며, 전 세계에서 안정적으로 이용할 수 있습니다. 초기 이용 현황과 피드백을 반영하는 과정에서 가격과 사용 한도가 변경될 수 있습니다.", + "이 플랜은 주로 글로벌 사용자를 위해 설계되었으며, 안정적인 글로벌 액세스를 위해 미국, EU 및 싱가포르에 모델이 호스팅되어 있습니다. 가격 및 사용 한도는 초기 사용을 통해 학습하고 피드백을 수집함에 따라 변경될 수 있습니다.", "workspace.lite.promo.subscribe": "Go 구독하기", "workspace.lite.promo.subscribing": "리디렉션 중...", "workspace.lite.promo.otherMethods": "기타 결제 수단", diff --git a/packages/console/app/src/i18n/no.ts b/packages/console/app/src/i18n/no.ts index 5e7e025032..4536284428 100644 --- a/packages/console/app/src/i18n/no.ts +++ b/packages/console/app/src/i18n/no.ts @@ -254,9 +254,9 @@ export const dict = { "zen.privacy.exceptionsLink": "følgende unntak", "go.title": "OpenCode Go | Rimelige kodemodeller for alle", - "go.banner.text": "GPT 5.6 Luna får 2x bruksgrense i en begrenset periode", + "go.banner.text": "Kimi K3 får 2x bruksgrense i en begrenset periode", "go.meta.description": - "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.", + "Go starter på $5 for den første måneden, deretter $10/måned, med sjenerøse 5-timers forespørselsgrenser for Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -303,7 +303,8 @@ export const dict = { "go.problem.item1": "Rimelig abonnementspris", "go.problem.item2": "Rause grenser og pålitelig tilgang", "go.problem.item3": "Bygget for så mange programmerere som mulig", - "go.problem.item4": "Et kuratert modellutvalg testet for agent-koding", + "go.problem.item4": + "Inkluderer Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -329,7 +330,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 et kuratert modellutvalg.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -338,22 +339,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "konto", "go.faq.a4.p3": "Avslutt når som helst.", "go.faq.q5": "Hva med data og personvern?", - "go.faq.a5.model": "Modell", - "go.faq.a5.training": "Modelltrening", - "go.faq.a5.retention": "Dataoppbevaring", - "go.faq.a5.retention30": "30 dager", - "go.faq.a5.retention0": "0 dager", - "go.faq.a5.used": "Brukes", - "go.faq.a5.notUsed": "Brukes ikke", - "go.faq.a5.noAgreement": "Ingen avtale", - "go.faq.a5.grokRetention": - "ZDR deaktiverer viktige API-funksjoner som er avhengige av lagrede data, inkludert den tilstandsbaserte Responses API, Files and Collections og Batch API.", - "go.faq.a5.gptRetention": - "Logger for overvåking av misbruk genereres for all bruk av API-funksjoner og oppbevares i opptil 30 dager.", - "go.faq.a5.learnMore": "Les mer", - "go.faq.a5.deepseekRetention": - "ZDR-avtalen fornyes månedlig. Den gjeldende avtalen er gyldig til og med 31. august 2026.", - + "go.faq.a5.body": + "Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Våre leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening.", "go.faq.a5.beforeExceptions": "Go-modeller hostes i USA. Leverandører følger en policy om null oppbevaring og bruker ikke dataene dine til modelltrening, med", "go.faq.a5.exceptionsLink": "følgende unntak", @@ -367,7 +354,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 som er tilgjengelige på det tidspunktet, med en kvote på 200 forespørsler/dag. Go tilbyr et kuratert modellutvalg med høyere forespørselskvoter som håndheves 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -376,7 +363,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Leverandør {{provider}} støttes ikke", "zen.api.error.missingApiKey": "Mangler API-nøkkel.", "zen.api.error.invalidApiKey": "Ugyldig API-nøkkel.", - "zen.api.error.requestBlockedByUpstreamProvider": "Forespørselen ble blokkert av leverandøren.", "zen.api.error.subscriptionQuotaExceeded": "Abonnementskvote overskredet. Prøv igjen om {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5-timers bruksgrense nådd. Tilbakestilles om {{retryIn}}. For å fortsette å bruke denne modellen nå, aktiver bruk fra din tilgjengelige saldo: {{consoleGoUrl}}", @@ -392,7 +378,7 @@ export const dict = { "Du har nådd din månedlige utgiftsgrense på ${{amount}}. Administrer grensene dine her: {{membersUrl}}", "zen.api.error.modelDisabled": "Modellen er deaktivert", "zen.api.error.regionNotAllowed": - "Den nyeste versjonen av denne modellen er bare tilgjengelig som en tjeneste driftet i Kina og krever at du uttrykkelig samtykker: {{consoleGoUrl}}", + "Denne modellen hostes i Kina. Hvis du vil bruke denne modellen, aktiver den i innstillingene dine: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Den gratis kampanjen for {{model}} er avsluttet. Du kan fortsette å bruke modellen ved å abonnere på OpenCode Go - {{link}}", @@ -684,7 +670,7 @@ export const dict = { "workspace.lite.promo.price": "$5 for den første måneden", "workspace.lite.promo.modelsTitle": "Hva som er inkludert", "workspace.lite.promo.footer": - "Planen er primært utviklet for internasjonale brukere og gir stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer av tidlig bruk og tilbakemeldinger.", + "Planen er primært designet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. Priser og bruksgrenser kan endres etter hvert som vi lærer fra tidlig bruk og tilbakemeldinger.", "workspace.lite.promo.subscribe": "Abonner på Go", "workspace.lite.promo.subscribing": "Omdirigerer...", "workspace.lite.promo.otherMethods": "Andre betalingsmetoder", diff --git a/packages/console/app/src/i18n/pl.ts b/packages/console/app/src/i18n/pl.ts index 2aa536cd82..dc3b593caa 100644 --- a/packages/console/app/src/i18n/pl.ts +++ b/packages/console/app/src/i18n/pl.ts @@ -255,9 +255,9 @@ export const dict = { "zen.privacy.exceptionsLink": "następującymi wyjątkami", "go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego", - "go.banner.text": "GPT 5.6 Luna oferuje 2x wyższe limity użycia przez ograniczony czas", + "go.banner.text": "Kimi K3 oferuje 2x wyższe limity użycia przez ograniczony czas", "go.meta.description": - "Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.", + "Go zaczyna się od $5 za pierwszy miesiąc, potem $10/miesiąc, z hojnymi 5-godzinnymi limitami zapytań dla Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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ść.", @@ -304,7 +304,8 @@ export const dict = { "go.problem.item1": "Niskokosztowa cena subskrypcji", "go.problem.item2": "Hojne limity i niezawodny dostęp", "go.problem.item3": "Stworzony dla jak największej liczby programistów", - "go.problem.item4": "Starannie dobrany zestaw modeli przetestowanych pod kątem kodowania z agentami", + "go.problem.item4": + "Zawiera Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -330,7 +331,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 działa w modelu płatności za użycie, natomiast Go kosztuje $5 za pierwszy miesiąc, a następnie $10/miesiąc, oferując hojne limity i niezawodny dostęp do starannie dobranego zestawu modeli.", + "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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -339,21 +340,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "koncie", "go.faq.a4.p3": "Anuluj w dowolnym momencie.", "go.faq.q5": "A co z danymi i prywatnością?", - "go.faq.a5.model": "Model", - "go.faq.a5.training": "Trenowanie modelu", - "go.faq.a5.retention": "Retencja danych", - "go.faq.a5.retention30": "30 dni", - "go.faq.a5.retention0": "0 dni", - "go.faq.a5.used": "Wykorzystywane", - "go.faq.a5.notUsed": "Niewykorzystywane", - "go.faq.a5.noAgreement": "Brak umowy", - "go.faq.a5.grokRetention": - "ZDR wyłącza ważne funkcje API zależne od przechowywanych danych, w tym stanowy Responses API, Files and Collections oraz Batch API.", - "go.faq.a5.gptRetention": - "Dzienniki monitorowania nadużyć są generowane dla każdego użycia funkcji API i przechowywane przez maksymalnie 30 dni.", - "go.faq.a5.learnMore": "Dowiedz się więcej", - "go.faq.a5.deepseekRetention": "Umowa ZDR jest odnawiana co miesiąc. Obecna umowa obowiązuje do 31 sierpnia 2026 r.", - + "go.faq.a5.body": + "Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Nasi dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli.", "go.faq.a5.beforeExceptions": "Modele Go są hostowane w USA. Dostawcy stosują politykę zerowej retencji i nie używają Twoich danych do trenowania modeli, z", "go.faq.a5.exceptionsLink": "następującymi wyjątkami", @@ -367,7 +355,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 oferuje starannie dobrany zestaw modeli z wyższymi limitami zapytań egzekwowanymi w oknach kroczących (5-godzinnych, tygodniowych i miesięcznych), odpowiadającymi w przybliżeniu $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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -376,7 +364,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Dostawca {{provider}} nie jest obsługiwany", "zen.api.error.missingApiKey": "Brak klucza API.", "zen.api.error.invalidApiKey": "Nieprawidłowy klucz API.", - "zen.api.error.requestBlockedByUpstreamProvider": "Żądanie zablokowane przez dostawcę zewnętrznego.", "zen.api.error.subscriptionQuotaExceeded": "Przekroczono limit subskrypcji. Spróbuj ponownie za {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Osiągnięto 5-godzinny limit użycia. Resetuje się za {{retryIn}}. Aby nadal korzystać z tego modelu, włącz użycie z dostępnego salda: {{consoleGoUrl}}", @@ -392,7 +379,7 @@ export const dict = { "Osiągnąłeś swój miesięczny limit wydatków w wysokości ${{amount}}. Zarządzaj swoimi limitami tutaj: {{membersUrl}}", "zen.api.error.modelDisabled": "Model jest wyłączony", "zen.api.error.regionNotAllowed": - "Najnowsza wersja tego modelu jest dostępna wyłącznie jako usługa hostowana w Chinach i wymaga jawnej zgody użytkownika: {{consoleGoUrl}}", + "Ten model jest hostowany w Chinach. Jeśli chcesz korzystać z tego modelu, włącz go w swoich ustawieniach: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Bezpłatna promocja {{model}} dobiegła końca. Możesz dalej korzystać z modelu, subskrybując OpenCode Go - {{link}}", @@ -684,7 +671,7 @@ export const dict = { "workspace.lite.promo.price": "$5 za pierwszy miesiąc", "workspace.lite.promo.modelsTitle": "Co zawiera", "workspace.lite.promo.footer": - "Plan został opracowany przede wszystkim z myślą o użytkownikach z całego świata i zapewnia stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę zdobywania doświadczeń na podstawie początkowego korzystania z usługi i otrzymywanych opinii.", + "Plan został zaprojektowany głównie dla użytkowników międzynarodowych, z modelami hostowanymi w USA, UE i Singapurze, aby zapewnić stabilny globalny dostęp. Ceny i limity użycia mogą ulec zmianie w miarę analizy wczesnego użycia i zbierania opinii.", "workspace.lite.promo.subscribe": "Subskrybuj Go", "workspace.lite.promo.subscribing": "Przekierowywanie...", "workspace.lite.promo.otherMethods": "Inne metody płatności", diff --git a/packages/console/app/src/i18n/ru.ts b/packages/console/app/src/i18n/ru.ts index 330713cd19..2ec2cd2950 100644 --- a/packages/console/app/src/i18n/ru.ts +++ b/packages/console/app/src/i18n/ru.ts @@ -258,9 +258,9 @@ export const dict = { "zen.privacy.exceptionsLink": "следующими исключениями", "go.title": "OpenCode Go | Недорогие модели для кодинга для всех", - "go.banner.text": "GPT 5.6 Luna получает 2x лимиты использования на ограниченное время", + "go.banner.text": "Kimi K3 получает 2x лимиты использования на ограниченное время", "go.meta.description": - "Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.", + "Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами запросов за 5 часов для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", "go.hero.title": "Недорогие модели для кодинга для всех", "go.hero.body": "Go открывает доступ к агентам-программистам разработчикам по всему миру. Предлагая щедрые лимиты и надежный доступ к наиболее способным моделям с открытым исходным кодом, вы можете создавать проекты с мощными агентами, не беспокоясь о затратах или доступности.", @@ -308,7 +308,8 @@ export const dict = { "go.problem.item1": "Недорогая подписка", "go.problem.item2": "Щедрые лимиты и надежный доступ", "go.problem.item3": "Создан для максимального числа программистов", - "go.problem.item4": "Отобранные модели, протестированные для агентного программирования", + "go.problem.item4": + "Включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash", "go.how.title": "Как работает Go", "go.how.body": "Go начинается с $5 за первый месяц, затем $10/месяц. Вы можете использовать его с OpenCode или любым агентом.", @@ -334,7 +335,7 @@ export const dict = { "go.faq.a2": "Go включает перечисленные ниже модели с щедрыми лимитами и надежным доступом.", "go.faq.q3": "Go — это то же самое, что и Zen?", "go.faq.a3": - "Нет. Zen оплачивается по мере использования, а Go стоит $5 за первый месяц, затем $10/месяц и предлагает щедрые лимиты и надежный доступ к отобранным моделям.", + "Нет. Zen - это оплата по мере использования, в то время как Go начинается с $5 за первый месяц, затем $10/месяц, с щедрыми лимитами и надежным доступом к моделям с открытым исходным кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash.", "go.faq.q4": "Сколько стоит Go?", "go.faq.a4.p1.beforePricing": "Go стоит", "go.faq.a4.p1.pricingLink": "$5 за первый месяц", @@ -343,22 +344,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "аккаунте", "go.faq.a4.p3": "Отмена в любое время.", "go.faq.q5": "Как насчет данных и приватности?", - "go.faq.a5.model": "Модель", - "go.faq.a5.training": "Обучение моделей", - "go.faq.a5.retention": "Хранение данных", - "go.faq.a5.retention30": "30 дней", - "go.faq.a5.retention0": "0 дней", - "go.faq.a5.used": "Используется", - "go.faq.a5.notUsed": "Не используется", - "go.faq.a5.noAgreement": "Нет соглашения", - "go.faq.a5.grokRetention": - "ZDR отключает важные функции API, зависящие от сохраненных данных, включая Responses API с сохранением состояния, Files and Collections и Batch API.", - "go.faq.a5.gptRetention": - "Журналы мониторинга злоупотреблений создаются при любом использовании функций API и хранятся до 30 дней.", - "go.faq.a5.learnMore": "Подробнее", - "go.faq.a5.deepseekRetention": - "Соглашение ZDR продлевается ежемесячно. Текущее соглашение действует до 31 августа 2026 года.", - + "go.faq.a5.body": + "План разработан в первую очередь для международных пользователей, с моделями, размещенными в США, ЕС и Сингапуре для стабильного глобального доступа. Наши провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей.", "go.faq.a5.beforeExceptions": "Модели Go размещены в США. Провайдеры следуют политике нулевого хранения и не используют ваши данные для обучения моделей, за", "go.faq.a5.exceptionsLink": "следующими исключениями", @@ -372,7 +359,7 @@ export const dict = { "go.faq.q9": "В чем разница между бесплатными моделями и Go?", "go.faq.a9": - "Бесплатные модели включают Big Pickle и доступные на данный момент промо-модели с квотой 200 запросов/день. Go предлагает набор отобранных моделей с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", + "Бесплатные модели включают Big Pickle плюс промо-модели, доступные на данный момент, с квотой 200 запросов/день. Go включает Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro и DeepSeek V4 Flash с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно $12 за 5 часов, $30 в неделю и $60 в месяц (фактическое количество запросов зависит от модели и использования).", "zen.api.error.rateLimitExceeded": "Превышен лимит запросов. Пожалуйста, попробуйте позже.", "zen.api.error.modelNotSupported": "Модель {{model}} не поддерживается", @@ -381,7 +368,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Провайдер {{provider}} не поддерживается", "zen.api.error.missingApiKey": "Отсутствует API ключ.", "zen.api.error.invalidApiKey": "Неверный API ключ.", - "zen.api.error.requestBlockedByUpstreamProvider": "Запрос заблокирован вышестоящим провайдером.", "zen.api.error.subscriptionQuotaExceeded": "Квота подписки превышена. Повторите попытку через {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Достигнут лимит использования за 5 часов. Сбросится через {{retryIn}}. Чтобы продолжить использовать эту модель сейчас, включите оплату с доступного баланса: {{consoleGoUrl}}", @@ -397,7 +383,7 @@ export const dict = { "Вы достигли ежемесячного лимита расходов в ${{amount}}. Управляйте лимитами здесь: {{membersUrl}}", "zen.api.error.modelDisabled": "Модель отключена", "zen.api.error.regionNotAllowed": - "Последняя версия этой модели размещена только в Китае. Чтобы использовать её, необходимо явно подтвердить согласие: {{consoleGoUrl}}", + "Эта модель размещена в Китае. Если вы хотите использовать эту модель, включите её в настройках: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Бесплатная акция для {{model}} завершена. Вы можете продолжить использование модели, подписавшись на OpenCode Go - {{link}}", @@ -691,7 +677,7 @@ export const dict = { "workspace.lite.promo.price": "$5 за первый месяц", "workspace.lite.promo.modelsTitle": "Что включено", "workspace.lite.promo.footer": - "План предназначен в первую очередь для пользователей по всему миру и обеспечивает стабильный глобальный доступ. Цены и лимиты использования могут меняться по мере изучения первых результатов использования и отзывов.", + "План предназначен в первую очередь для международных пользователей. Модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. Цены и лимиты использования могут меняться по мере того, как мы изучаем раннее использование и собираем отзывы.", "workspace.lite.promo.subscribe": "Подписаться на Go", "workspace.lite.promo.subscribing": "Перенаправление...", "workspace.lite.promo.otherMethods": "Другие способы оплаты", diff --git a/packages/console/app/src/i18n/th.ts b/packages/console/app/src/i18n/th.ts index d3ff91e97a..3a3fda4945 100644 --- a/packages/console/app/src/i18n/th.ts +++ b/packages/console/app/src/i18n/th.ts @@ -253,9 +253,9 @@ export const dict = { "zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้", "go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", - "go.banner.text": "GPT 5.6 Luna เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด", + "go.banner.text": "Kimi K3 เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด", "go.meta.description": - "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้", + "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดคำขอ 5 ชั่วโมงที่เอื้อเฟื้อสำหรับ Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", "go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน", "go.hero.body": "Go นำการเขียนโค้ดแบบเอเจนต์มาสู่นักเขียนโปรแกรมทั่วโลก เสนอขีดจำกัดที่กว้างขวางและการเข้าถึงโมเดลโอเพนซอร์สที่มีความสามารถสูงสุดได้อย่างน่าเชื่อถือ เพื่อให้คุณสามารถสร้างสรรค์ด้วยเอเจนต์ที่ทรงพลังโดยไม่ต้องกังวลเรื่องค่าใช้จ่ายหรือความพร้อมใช้งาน", @@ -301,7 +301,8 @@ export const dict = { "go.problem.item1": "ราคาการสมัครสมาชิกที่ต่ำ", "go.problem.item2": "ขีดจำกัดที่กว้างขวางและการเข้าถึงที่เชื่อถือได้", "go.problem.item3": "สร้างขึ้นเพื่อโปรแกรมเมอร์จำนวนมากที่สุดเท่าที่จะเป็นไปได้", - "go.problem.item4": "ชุดโมเดลที่คัดสรรและผ่านการทดสอบสำหรับการเขียนโค้ดแบบเอเจนต์", + "go.problem.item4": + "รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash", "go.how.title": "Go ทำงานอย่างไร", "go.how.body": "Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน คุณสามารถใช้กับ OpenCode หรือเอเจนต์ใดก็ได้", "go.how.step1.title": "สร้างบัญชี", @@ -326,7 +327,7 @@ export const dict = { "go.faq.a2": "Go รวมโมเดลด้านล่างนี้ พร้อมขีดจำกัดที่มากและการเข้าถึงที่เชื่อถือได้", "go.faq.q3": "Go เหมือนกับ Zen หรือไม่?", "go.faq.a3": - "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงชุดโมเดลที่คัดสรรอย่างเชื่อถือได้", + "ไม่ Zen เป็นแบบจ่ายตามการใช้งาน ในขณะที่ Go เริ่มต้นที่ $5 สำหรับเดือนแรก จากนั้น $10/เดือน พร้อมขีดจำกัดที่เอื้อเฟื้อและการเข้าถึงโมเดลโอเพนซอร์ส Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash อย่างเชื่อถือได้", "go.faq.q4": "Go ราคาเท่าไหร่?", "go.faq.a4.p1.beforePricing": "Go ราคา", "go.faq.a4.p1.pricingLink": "$5 เดือนแรก", @@ -335,21 +336,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "บัญชีของคุณ", "go.faq.a4.p3": "ยกเลิกได้ตลอดเวลา", "go.faq.q5": "แล้วเรื่องข้อมูลและความเป็นส่วนตัวล่ะ?", - "go.faq.a5.model": "โมเดล", - "go.faq.a5.training": "การฝึกโมเดล", - "go.faq.a5.retention": "การเก็บรักษาข้อมูล", - "go.faq.a5.retention30": "30 วัน", - "go.faq.a5.retention0": "0 วัน", - "go.faq.a5.used": "นำไปใช้", - "go.faq.a5.notUsed": "ไม่นำไปใช้", - "go.faq.a5.noAgreement": "ไม่มีข้อตกลง", - "go.faq.a5.grokRetention": - "ZDR ปิดใช้งานฟีเจอร์ API สำคัญที่ต้องอาศัยข้อมูลที่จัดเก็บไว้ ซึ่งรวมถึง Responses API แบบมีสถานะ, Files and Collections และ Batch API", - "go.faq.a5.gptRetention": - "ระบบจะสร้างบันทึกการตรวจสอบการใช้งานในทางที่ผิดสำหรับการใช้งานฟีเจอร์ API ทั้งหมด และเก็บรักษาไว้นานสูงสุด 30 วัน", - "go.faq.a5.learnMore": "ดูข้อมูลเพิ่มเติม", - "go.faq.a5.deepseekRetention": "ข้อตกลง ZDR จะต่ออายุทุกเดือน ข้อตกลงปัจจุบันมีผลใช้ถึงวันที่ 31 สิงหาคม 2026", - + "go.faq.a5.body": + "แผนนี้ออกแบบมาเพื่อผู้ใช้งานระหว่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร ผู้ให้บริการของเราปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูลและไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล", "go.faq.a5.beforeExceptions": "โมเดล Go โฮสต์ในสหรัฐอเมริกา ผู้ให้บริการปฏิบัติตามนโยบายไม่เก็บรักษาข้อมูล (zero-retention policy) และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมี", "go.faq.a5.exceptionsLink": "ข้อยกเว้นดังนี้", @@ -362,7 +350,7 @@ export const dict = { "go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?", "go.faq.a9": - "โมเดลฟรีประกอบด้วย Big Pickle และโมเดลโปรโมชันที่มีให้บริการในขณะนั้น โดยมีโควตา 200 คำขอ/วัน Go นำเสนอชุดโมเดลที่คัดสรร พร้อมโควตาคำขอที่สูงกว่าซึ่งบังคับใช้ตามกรอบเวลาแบบต่อเนื่อง (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงแตกต่างกันไปตามโมเดลและการใช้งาน)", + "โมเดลฟรีรวมถึง Big Pickle บวกกับโมเดลโปรโมชั่นที่มีให้ในขณะนั้น ด้วยโควต้า 200 คำขอ/วัน Go รวมถึง Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro และ DeepSeek V4 Flash ที่มีโควต้าคำขอสูงกว่า ซึ่งบังคับใช้ผ่านช่วงเวลาหมุนเวียน (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน (จำนวนคำขอจริงจะแตกต่างกันไปตามโมเดลและการใช้งาน)", "zen.api.error.rateLimitExceeded": "เกินขีดจำกัดอัตราการใช้งาน กรุณาลองใหม่ในภายหลัง", "zen.api.error.modelNotSupported": "ไม่รองรับโมเดล {{model}}", @@ -371,7 +359,6 @@ export const dict = { "zen.api.error.providerNotSupported": "ไม่รองรับผู้ให้บริการ {{provider}}", "zen.api.error.missingApiKey": "ไม่มี API key", "zen.api.error.invalidApiKey": "API key ไม่ถูกต้อง", - "zen.api.error.requestBlockedByUpstreamProvider": "คำขอถูกบล็อกโดยผู้ให้บริการต้นทาง", "zen.api.error.subscriptionQuotaExceeded": "โควต้าการสมัครสมาชิกเกินขีดจำกัด ลองใหม่ในอีก {{retryIn}}", "zen.api.error.goSubscriptionRollingLimitExceeded": "ถึงขีดจำกัดการใช้งานในรอบ 5 ชั่วโมงแล้ว จะรีเซ็ตในอีก {{retryIn}} หากต้องการใช้โมเดลนี้ต่อทันที ให้เปิดใช้งานจากยอดเงินคงเหลือของคุณ: {{consoleGoUrl}}", @@ -387,7 +374,7 @@ export const dict = { "คุณถึงขีดจำกัดการใช้จ่ายรายเดือนที่ ${{amount}} แล้ว จัดการขีดจำกัดของคุณที่นี่: {{membersUrl}}", "zen.api.error.modelDisabled": "โมเดลถูกปิดใช้งาน", "zen.api.error.regionNotAllowed": - "โมเดลเวอร์ชันล่าสุดนี้ให้บริการเฉพาะผ่านระบบที่โฮสต์ในประเทศจีน และต้องให้ความยินยอมอย่างชัดแจ้งก่อนใช้งาน: {{consoleGoUrl}}", + "โมเดลนี้โฮสต์อยู่ในประเทศจีน หากคุณต้องการใช้โมเดลนี้ ให้เปิดใช้งานในการตั้งค่าของคุณ: {{consoleGoUrl}}", "zen.api.error.trialEnded": "โปรโมชันฟรีสำหรับ {{model}} สิ้นสุดแล้ว คุณสามารถใช้โมเดลต่อได้โดยสมัครสมาชิก OpenCode Go - {{link}}", @@ -679,7 +666,7 @@ export const dict = { "workspace.lite.promo.price": "$5 สำหรับเดือนแรก", "workspace.lite.promo.modelsTitle": "สิ่งที่รวมอยู่ด้วย", "workspace.lite.promo.footer": - "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลักและให้การเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจเปลี่ยนแปลงได้ตามสิ่งที่เราเรียนรู้จากการใช้งานและข้อเสนอแนะในช่วงแรก", + "แผนนี้ออกแบบมาสำหรับผู้ใช้งานต่างประเทศเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงที่เสถียรทั่วโลก ราคาและขีดจำกัดการใช้งานอาจมีการเปลี่ยนแปลงตามที่เราได้เรียนรู้จากการใช้งานในช่วงแรกและข้อเสนอแนะ", "workspace.lite.promo.subscribe": "สมัครสมาชิก Go", "workspace.lite.promo.subscribing": "กำลังเปลี่ยนเส้นทาง...", "workspace.lite.promo.otherMethods": "วิธีการชำระเงินอื่นๆ", diff --git a/packages/console/app/src/i18n/tr.ts b/packages/console/app/src/i18n/tr.ts index 5d0d2819dd..1e55ba91c9 100644 --- a/packages/console/app/src/i18n/tr.ts +++ b/packages/console/app/src/i18n/tr.ts @@ -256,9 +256,9 @@ export const dict = { "zen.privacy.exceptionsLink": "aşağıdaki istisnalar", "go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri", - "go.banner.text": "GPT 5.6 Luna sınırlı bir süre için 2x kullanım limiti sunuyor", + "go.banner.text": "Kimi K3 sınırlı bir süre için 2x kullanım limiti sunuyor", "go.meta.description": - "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.", + "Go ilk ay $5, sonrasında ayda 10$ fiyatıyla başlar; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -306,7 +306,8 @@ export const dict = { "go.problem.item1": "Düşük maliyetli abonelik fiyatlandırması", "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": "Ajan tabanlı kodlama için test edilmiş, özenle seçilmiş model seçenekleri", + "go.problem.item4": + "Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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.", @@ -332,7 +333,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 ve özenle seçilmiş model seçeneklerine cömert limitlerle 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; Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -341,22 +342,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "hesabınızdan", "go.faq.a4.p3": "yönetebilirsiniz. İstediğiniz zaman iptal edin.", "go.faq.q5": "Veri ve gizlilik ne olacak?", - "go.faq.a5.model": "Model", - "go.faq.a5.training": "Model eğitimi", - "go.faq.a5.retention": "Veri saklama", - "go.faq.a5.retention30": "30 gün", - "go.faq.a5.retention0": "0 gün", - "go.faq.a5.used": "Kullanılır", - "go.faq.a5.notUsed": "Kullanılmaz", - "go.faq.a5.noAgreement": "Anlaşma yok", - "go.faq.a5.grokRetention": - "ZDR, durum bilgisi tutan Responses API, Files and Collections ve Batch API dahil olmak üzere saklanan verilere bağlı önemli API özelliklerini devre dışı bırakır.", - "go.faq.a5.gptRetention": - "Tüm API özelliklerinin kullanımı için kötüye kullanım izleme günlükleri oluşturulur ve 30 güne kadar saklanır.", - "go.faq.a5.learnMore": "Daha fazla bilgi", - "go.faq.a5.deepseekRetention": - "ZDR anlaşması aylık olarak yenilenir. Mevcut anlaşma 31 Ağustos 2026 tarihine kadar geçerlidir.", - + "go.faq.a5.body": + "Bu plan öncelikle uluslararası kullanıcılar için tasarlanmış olup, istikrarlı küresel erişim için modeller ABD, AB ve Singapur'da barındırılmaktadır. Sağlayıcılarımız sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz.", "go.faq.a5.beforeExceptions": "Go modelleri ABD'de barındırılmaktadır. Sağlayıcılar sıfır saklama politikası izler ve verilerinizi model eğitimi için kullanmaz; şu", "go.faq.a5.exceptionsLink": "aşağıdaki istisnalar", @@ -370,7 +357,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ıyla Big Pickle'ı ve o sırada mevcut olan promosyonel modelleri içerir. Go ise kayan zaman aralıklarında (5 saatlik, haftalık ve aylık) uygulanan daha yüksek istek kotalarıyla özenle seçilmiş model seçenekleri sunar. 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 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, 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", @@ -379,7 +366,6 @@ export const dict = { "zen.api.error.providerNotSupported": "{{provider}} sağlayıcısı desteklenmiyor", "zen.api.error.missingApiKey": "API anahtarı eksik.", "zen.api.error.invalidApiKey": "Geçersiz API anahtarı.", - "zen.api.error.requestBlockedByUpstreamProvider": "İstek üst sağlayıcı tarafından engellendi.", "zen.api.error.subscriptionQuotaExceeded": "Abonelik kotası aşıldı. {{retryIn}} içinde tekrar deneyin.", "zen.api.error.goSubscriptionRollingLimitExceeded": "5 saatlik kullanım limitine ulaşıldı. {{retryIn}} içinde sıfırlanır. Bu modeli şimdi kullanmaya devam etmek için kullanılabilir bakiyenizden kullanımı etkinleştirin: {{consoleGoUrl}}", @@ -395,7 +381,7 @@ export const dict = { "Aylık ${{amount}} harcama limitinize ulaştınız. Limitlerinizi buradan yönetin: {{membersUrl}}", "zen.api.error.modelDisabled": "Model devre dışı", "zen.api.error.regionNotAllowed": - "Bu modelin en son sürümü yalnızca Çin'de barındırılıyor ve kullanabilmek için açıkça onay vermeniz gerekiyor: {{consoleGoUrl}}", + "Bu model Çin'de barındırılıyor. Bu modeli kullanmak istiyorsanız ayarlarınızdan etkinleştirin: {{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} için ücretsiz promosyon sona erdi. OpenCode Go'ya abone olarak modeli kullanmaya devam edebilirsiniz - {{link}}", @@ -687,7 +673,7 @@ export const dict = { "workspace.lite.promo.price": "İlk ay $5", "workspace.lite.promo.modelsTitle": "Neler Dahil", "workspace.lite.promo.footer": - "Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır ve istikrarlı küresel erişim sağlar. Erken kullanım ve geri bildirimlerden öğrendiklerimiz doğrultusunda fiyatlandırma ve kullanım limitleri değişebilir.", + "Plan öncelikle uluslararası kullanıcılar için tasarlanmıştır; modeller istikrarlı küresel erişim için ABD, AB ve Singapur'da barındırılmaktadır. Erken kullanımdan öğrendikçe ve geri bildirim topladıkça fiyatlandırma ve kullanım limitleri değişebilir.", "workspace.lite.promo.subscribe": "Go'ya Abone Ol", "workspace.lite.promo.subscribing": "Yönlendiriliyor...", "workspace.lite.promo.otherMethods": "Diğer ödeme yöntemleri", diff --git a/packages/console/app/src/i18n/uk.ts b/packages/console/app/src/i18n/uk.ts index 48a0dbe787..a1e8561c43 100644 --- a/packages/console/app/src/i18n/uk.ts +++ b/packages/console/app/src/i18n/uk.ts @@ -255,9 +255,9 @@ export const dict = { "zen.privacy.exceptionsLink": "такими винятками", "go.title": "OpenCode Go | Недорогі моделі кодування для всіх", - "go.banner.text": "GPT 5.6 Luna отримує 2x ліміти використання протягом обмеженого часу", + "go.banner.text": "Kimi K3 отримує 2x ліміти використання протягом обмеженого часу", "go.meta.description": - "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.", + "Go починається від $5 за перший місяць, потім $10/місяць, зі щедрими 5-годинними лімітами запитів для Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", "go.hero.title": "Недорогі моделі кодування для всіх", "go.hero.body": "Go надає агентне програмування програмістам у всьому світі, пропонуючи щедрі ліміти та надійний доступ до найкращих моделей з відкритим кодом.", @@ -304,7 +304,8 @@ export const dict = { "go.problem.item1": "Недорога підписка", "go.problem.item2": "Щедрі ліміти та надійний доступ", "go.problem.item3": "Створено для якомога більшої кількості програмістів", - "go.problem.item4": "Добірка моделей, протестованих для агентного кодування", + "go.problem.item4": + "Включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash", "go.how.title": "Як працює Go", "go.how.body": "Go починається від $5 за перший місяць, потім $10/місяць. Використовуйте з OpenCode або будь-яким агентом.", @@ -330,7 +331,7 @@ export const dict = { "go.faq.a2": "Go включає моделі, перелічені нижче, із щедрими лімітами та надійним доступом.", "go.faq.q3": "Чи Go те саме, що Zen?", "go.faq.a3": - "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до добірки моделей.", + "Ні. Zen — це плата за використання, тоді як Go починається від $5 за перший місяць, потім $10/місяць, із щедрими лімітами та надійним доступом до моделей з відкритим кодом Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash.", "go.faq.q4": "Скільки коштує Go?", "go.faq.a4.p1.beforePricing": "Go коштує", "go.faq.a4.p1.pricingLink": "$5 за перший місяць", @@ -339,21 +340,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "обліковому записі", "go.faq.a4.p3": "Скасуйте в будь-який час.", "go.faq.q5": "А як щодо даних та конфіденційності?", - "go.faq.a5.model": "Модель", - "go.faq.a5.training": "Навчання моделей", - "go.faq.a5.retention": "Зберігання даних", - "go.faq.a5.retention30": "30 днів", - "go.faq.a5.retention0": "0 днів", - "go.faq.a5.used": "Використовується", - "go.faq.a5.notUsed": "Не використовується", - "go.faq.a5.noAgreement": "Немає угоди", - "go.faq.a5.grokRetention": - "ZDR вимикає важливі функції API, які залежать від збережених даних, зокрема Responses API зі збереженням стану, Files and Collections та Batch API.", - "go.faq.a5.gptRetention": - "Журнали моніторингу зловживань створюються для всіх випадків використання функцій API та зберігаються до 30 днів.", - "go.faq.a5.learnMore": "Докладніше", - "go.faq.a5.deepseekRetention": "Угода ZDR поновлюється щомісяця. Поточна угода дійсна до 31 серпня 2026 року.", - + "go.faq.a5.body": + "План розроблений переважно для міжнародних користувачів, з моделями в США, ЄС та Сінгапурі. Провайдери дотримуються політики нульового зберігання.", "go.faq.a5.beforeExceptions": "Моделі Go розміщені в США. Провайдери дотримуються політики нульового зберігання та не використовують ваші дані для навчання моделей, за", "go.faq.a5.exceptionsLink": "такими винятками", @@ -366,7 +354,7 @@ export const dict = { "go.faq.q9": "Яка різниця між безкоштовними моделями та Go?", "go.faq.a9": - "Безкоштовні моделі включають Big Pickle та доступні на той момент акційні моделі з квотою 200 запитів/день. Go пропонує добірку моделей із вищими квотами запитів, що застосовуються протягом ковзних періодів (5 годин, тижня та місяця), приблизно еквівалентними $12 за 5 годин, $30 на тиждень і $60 на місяць (фактична кількість запитів залежить від моделі та використання).", + "Безкоштовні моделі включають Big Pickle та акційні моделі з лімітом 200 запитів/день. Go включає Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code, Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7, MiniMax M3, DeepSeek V4 Pro та DeepSeek V4 Flash із вищими лімітами.", "zen.api.error.rateLimitExceeded": "Перевищено ліміт запитів. Спробуйте пізніше.", "zen.api.error.modelNotSupported": "Модель {{model}} не підтримується", @@ -375,7 +363,6 @@ export const dict = { "zen.api.error.providerNotSupported": "Провайдер {{provider}} не підтримується", "zen.api.error.missingApiKey": "Відсутній ключ API.", "zen.api.error.invalidApiKey": "Недійсний ключ API.", - "zen.api.error.requestBlockedByUpstreamProvider": "Запит заблоковано зовнішнім провайдером.", "zen.api.error.subscriptionQuotaExceeded": "Перевищено квоту підписки. Повторіть через {{retryIn}}.", "zen.api.error.goSubscriptionRollingLimitExceeded": "Досягнуто 5-годинного ліміту використання. Скидається через {{retryIn}}. Щоб продовжити, увімкніть використання з доступного балансу: {{consoleGoUrl}}", @@ -391,7 +378,7 @@ export const dict = { "Ви досягли місячного ліміту витрат ${{amount}}. Керуйте лімітами: {{membersUrl}}", "zen.api.error.modelDisabled": "Модель вимкнено", "zen.api.error.regionNotAllowed": - "Остання версія цієї моделі доступна лише на серверах у Китаї, і для її використання потрібно надати явну згоду: {{consoleGoUrl}}", + "Ця модель розміщена в Китаї. Якщо ви хочете використовувати цю модель, увімкніть її в налаштуваннях: {{consoleGoUrl}}", "zen.api.error.trialEnded": "Безкоштовна акція для {{model}} закінчилася. Ви можете продовжити використання, підписавшись на OpenCode Go — {{link}}", @@ -679,8 +666,7 @@ export const dict = { "workspace.lite.promo.description": "OpenCode Go починається від {{price}}, потім $10/місяць, із щедрими лімітами.", "workspace.lite.promo.price": "$5 за перший місяць", "workspace.lite.promo.modelsTitle": "Що включено", - "workspace.lite.promo.footer": - "План призначений насамперед для міжнародних користувачів і забезпечує стабільний глобальний доступ. Ціни та ліміти використання можуть змінюватися з урахуванням перших даних про використання та відгуків.", + "workspace.lite.promo.footer": "План призначений для міжнародних користувачів. Ціни можуть змінюватися.", "workspace.lite.promo.subscribe": "Підписатися на Go", "workspace.lite.promo.subscribing": "Перенаправлення...", "workspace.lite.promo.otherMethods": "Інші способи оплати", diff --git a/packages/console/app/src/i18n/zh.ts b/packages/console/app/src/i18n/zh.ts index 78f2081aeb..2fa8a8ca51 100644 --- a/packages/console/app/src/i18n/zh.ts +++ b/packages/console/app/src/i18n/zh.ts @@ -244,8 +244,9 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情况除外", "go.title": "OpenCode Go | 人人可用的低成本编程模型", - "go.banner.text": "GPT 5.6 Luna 限时享受 2 倍使用额度", - "go.meta.description": "Go 首月 $5,之后 $10/月,提供充裕的使用限额,并可可靠访问领先的编程模型。", + "go.banner.text": "Kimi K3 限时享受 2 倍使用额度", + "go.meta.description": + "Go 首月 $5,之后 $10/月,提供对 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小时充裕请求额度。", "go.hero.title": "人人可用的低成本编程模型", "go.hero.body": "Go 将代理编程带给全世界的程序员。提供充裕的限额和对最强大的开源模型的可靠访问,让您可以利用强大的代理进行构建,而无需担心成本或可用性。", @@ -291,7 +292,8 @@ export const dict = { "go.problem.item1": "低成本订阅定价", "go.problem.item2": "充裕的限额和可靠的访问", "go.problem.item3": "为尽可能多的程序员打造", - "go.problem.item4": "经过代理编程测试的精选模型阵容", + "go.problem.item4": + "包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash", "go.how.title": "Go 如何工作", "go.how.body": "Go 起价为首月 $5,之后 $10/月。您可以将其与 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "创建账户", @@ -312,7 +314,8 @@ export const dict = { "go.faq.q2": "Go 包含哪些模型?", "go.faq.a2": "Go 包含下方列出的模型,提供充足的限额和可靠的访问。", "go.faq.q3": "Go 和 Zen 一样吗?", - "go.faq.a3": "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的限额,并可可靠访问精选模型阵容。", + "go.faq.a3": + "不。Zen 是按量付费,而 Go 首月 $5,之后 $10/月,提供充裕的额度,并可可靠地访问 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等开源模型。", "go.faq.q4": "Go 多少钱?", "go.faq.a4.p1.beforePricing": "Go 费用为", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -321,19 +324,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "账户", "go.faq.a4.p3": "中管理订阅。随时取消。", "go.faq.q5": "数据和隐私如何?", - "go.faq.a5.model": "模型", - "go.faq.a5.training": "模型训练", - "go.faq.a5.retention": "数据留存", - "go.faq.a5.retention30": "30 天", - "go.faq.a5.retention0": "0 天", - "go.faq.a5.used": "使用", - "go.faq.a5.notUsed": "不使用", - "go.faq.a5.noAgreement": "无协议", - "go.faq.a5.grokRetention": - "ZDR 会禁用依赖所存储数据的重要 API 功能,包括有状态的 Responses API、Files and Collections 和 Batch API。", - "go.faq.a5.gptRetention": "所有 API 功能的使用都会生成滥用监控日志,并最多保留 30 天。", - "go.faq.a5.learnMore": "了解更多", - "go.faq.a5.deepseekRetention": "ZDR 协议每月续签。当前协议有效期至 2026 年 8 月 31 日。", + "go.faq.a5.body": + "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保稳定的全球访问。我们的提供商遵循零留存政策,不使用您的数据进行模型训练。", "go.faq.a5.beforeExceptions": "Go 模型托管在美国。提供商遵循零留存政策,不使用您的数据进行模型训练,", "go.faq.a5.exceptionsLink": "以下例外情况除外", "go.faq.q6": "我可以充值余额吗?", @@ -345,7 +337,7 @@ export const dict = { "go.faq.q9": "免费模型和 Go 之间的区别是什么?", "go.faq.a9": - "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 提供精选模型阵容,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", + "免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 包含 Grok 4.5, GLM-5.2, GLM-5.1, Kimi K3, Kimi K2.7 Code、Kimi K2.6, MiMo-V2.5-Pro, MiMo-V2.5, Qwen3.7 Max, Qwen3.7 Plus, Qwen3.6 Plus, MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60(实际请求计数因模型和使用情况而异)。", "zen.api.error.rateLimitExceeded": "超出速率限制。请稍后重试。", "zen.api.error.modelNotSupported": "不支持模型 {{model}}", @@ -354,7 +346,6 @@ export const dict = { "zen.api.error.providerNotSupported": "不支持提供商 {{provider}}", "zen.api.error.missingApiKey": "缺少 API 密钥。", "zen.api.error.invalidApiKey": "无效的 API 密钥。", - "zen.api.error.requestBlockedByUpstreamProvider": "请求被上游提供商阻止。", "zen.api.error.subscriptionQuotaExceeded": "超出订阅配额。请在 {{retryIn}} 后重试。", "zen.api.error.goSubscriptionRollingLimitExceeded": "已达到 5 小时使用限额。将在 {{retryIn}} 后重置。如需立即继续使用该模型,请启用从可用余额扣费:{{consoleGoUrl}}", @@ -368,7 +359,7 @@ export const dict = { "您的工作区已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "您已达到每月支出限额 ${{amount}}。请在此处管理您的限额:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已禁用", - "zen.api.error.regionNotAllowed": "该模型的最新版本仅在中国提供托管服务,需明确选择启用:{{consoleGoUrl}}", + "zen.api.error.regionNotAllowed": "该模型部署在中国。如果你想使用该模型,请在设置中启用它:{{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} 的限免活动已结束。您可以订阅 OpenCode Go 继续使用该模型 - {{link}}", "black.meta.title": "OpenCode Black | 访问全球顶尖编程模型", @@ -654,7 +645,7 @@ export const dict = { "workspace.lite.promo.price": "首月 $5", "workspace.lite.promo.modelsTitle": "包含模型", "workspace.lite.promo.footer": - "该计划主要面向国际用户,提供稳定的全球访问体验。随着我们持续了解早期使用情况并收集反馈,定价和使用限额可能会有所调整。", + "该计划主要面向国际用户设计,模型部署在美国、欧盟和新加坡,以确保全球范围内的稳定访问体验。定价和使用额度可能会根据早期用户的使用情况和反馈持续调整与优化。", "workspace.lite.promo.subscribe": "订阅 Go", "workspace.lite.promo.subscribing": "正在重定向...", "workspace.lite.promo.otherMethods": "其他付款方式", diff --git a/packages/console/app/src/i18n/zht.ts b/packages/console/app/src/i18n/zht.ts index 190a927d3f..b7096b3d05 100644 --- a/packages/console/app/src/i18n/zht.ts +++ b/packages/console/app/src/i18n/zht.ts @@ -244,8 +244,9 @@ export const dict = { "zen.privacy.exceptionsLink": "以下例外情況", "go.title": "OpenCode Go | 低成本全民編碼模型", - "go.banner.text": "GPT 5.6 Luna 限時享有 2 倍使用額度", - "go.meta.description": "Go 首月 $5,之後 $10/月,提供充裕的使用限額,並可穩定存取領先的編碼模型。", + "go.banner.text": "Kimi K3 限時享有 2 倍使用額度", + "go.meta.description": + "Go 首月 $5,之後 $10/月,提供對 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 的 5 小時充裕請求額度。", "go.hero.title": "低成本全民編碼模型", "go.hero.body": "Go 將代理編碼帶給全世界的程式設計師。提供寬裕的限額以及對最強大開源模型的穩定存取,讓你可以使用強大的代理進行構建,而無需擔心成本或可用性。", @@ -291,7 +292,8 @@ export const dict = { "go.problem.item1": "低成本訂閱定價", "go.problem.item2": "寬裕的限額與穩定存取", "go.problem.item3": "專為盡可能多的程式設計師打造", - "go.problem.item4": "針對代理編碼測試的精選模型陣容", + "go.problem.item4": + "包含 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash", "go.how.title": "Go 如何運作", "go.how.body": "Go 起價為首月 $5,之後 $10/月。您可以將其與 OpenCode 或任何代理搭配使用。", "go.how.step1.title": "建立帳號", @@ -312,7 +314,8 @@ export const dict = { "go.faq.q2": "Go 包含哪些模型?", "go.faq.a2": "Go 包含下方列出的模型,提供充足的額度與穩定的存取。", "go.faq.q3": "Go 與 Zen 一樣嗎?", - "go.faq.a3": "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的限額,並可穩定存取精選模型陣容。", + "go.faq.a3": + "不。Zen 是按量付費,而 Go 首月 $5,之後 $10/月,提供充裕的額度,並可可靠地存取 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 和 DeepSeek V4 Flash 等開源模型。", "go.faq.q4": "Go 費用是多少?", "go.faq.a4.p1.beforePricing": "Go 費用為", "go.faq.a4.p1.pricingLink": "首月 $5", @@ -321,19 +324,8 @@ export const dict = { "go.faq.a4.p2.accountLink": "帳戶", "go.faq.a4.p3": "中管理訂閱。隨時取消。", "go.faq.q5": "資料與隱私怎麼辦?", - "go.faq.a5.model": "模型", - "go.faq.a5.training": "模型訓練", - "go.faq.a5.retention": "資料保留", - "go.faq.a5.retention30": "30 天", - "go.faq.a5.retention0": "0 天", - "go.faq.a5.used": "使用", - "go.faq.a5.notUsed": "不使用", - "go.faq.a5.noAgreement": "無協議", - "go.faq.a5.grokRetention": - "ZDR 會停用依賴儲存資料的重要 API 功能,包括具狀態的 Responses API、Files and Collections 與 Batch API。", - "go.faq.a5.gptRetention": "所有 API 功能的使用都會產生濫用監控日誌,並保留最多 30 天。", - "go.faq.a5.learnMore": "了解更多", - "go.faq.a5.deepseekRetention": "ZDR 協議每月續簽。目前的協議有效至 2026 年 8 月 31 日。", + "go.faq.a5.body": + "該方案主要面向國際用戶設計,模型託管在美國、歐盟和新加坡,以確保全球穩定存取。我們的供應商遵循零留存政策,不會將你的資料用於模型訓練。", "go.faq.a5.beforeExceptions": "Go 模型託管在美國。供應商遵循零留存政策,不會將你的資料用於模型訓練,但有", "go.faq.a5.exceptionsLink": "以下例外", "go.faq.q6": "我可以儲值額度嗎?", @@ -345,7 +337,7 @@ export const dict = { "go.faq.q9": "免費模型與 Go 有什麼區別?", "go.faq.a9": - "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 提供精選模型陣容,並在滾動視窗(5 小時、每週和每月)內提供更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", + "免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 包括 Grok 4.5、GLM-5.2、GLM-5.1、Kimi K3、Kimi K2.7 Code、Kimi K2.6、MiMo-V2.5-Pro、MiMo-V2.5、Qwen3.7 Max、Qwen3.7 Plus、Qwen3.6 Plus、MiniMax M2.7、MiniMax M3、DeepSeek V4 Pro 與 DeepSeek V4 Flash,並在滾動視窗(5 小時、每週和每月)內執行更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60(實際請求數因模型和使用情況而異)。", "zen.api.error.rateLimitExceeded": "超出頻率限制。請稍後再試。", "zen.api.error.modelNotSupported": "不支援模型 {{model}}", @@ -354,7 +346,6 @@ export const dict = { "zen.api.error.providerNotSupported": "不支援供應商 {{provider}}", "zen.api.error.missingApiKey": "缺少 API 金鑰。", "zen.api.error.invalidApiKey": "無效的 API 金鑰。", - "zen.api.error.requestBlockedByUpstreamProvider": "請求遭上游供應商封鎖。", "zen.api.error.subscriptionQuotaExceeded": "超出訂閱配額。請在 {{retryIn}} 後重試。", "zen.api.error.goSubscriptionRollingLimitExceeded": "已達 5 小時使用上限,將在 {{retryIn}} 後重置。若要立即繼續使用此模型,請從可用餘額啟用使用量:{{consoleGoUrl}}", @@ -368,7 +359,7 @@ export const dict = { "你的工作區已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{billingUrl}}", "zen.api.error.userMonthlyLimitReached": "你已達到每月支出限額 ${{amount}}。請在此處管理你的限額:{{membersUrl}}", "zen.api.error.modelDisabled": "模型已停用", - "zen.api.error.regionNotAllowed": "此模型的最新版本僅於中國託管,且需明確選擇啟用:{{consoleGoUrl}}", + "zen.api.error.regionNotAllowed": "此模型部署於中國。如果你想使用此模型,請在設定中啟用它:{{consoleGoUrl}}", "zen.api.error.trialEnded": "{{model}} 的限免活动已結束。您可以訂閱 OpenCode Go 繼續使用該模型 - {{link}}", "black.meta.title": "OpenCode Black | 存取全球最佳編碼模型", @@ -654,7 +645,7 @@ export const dict = { "workspace.lite.promo.price": "首月 $5", "workspace.lite.promo.modelsTitle": "包含模型", "workspace.lite.promo.footer": - "此方案主要為國際使用者設計,提供穩定的全球存取服務。隨著我們從初期使用情況和回饋中持續了解需求,價格和使用額度可能會有所調整。", + "該計畫主要面向國際用戶設計,模型部署在美國、歐盟和新加坡,以確保全球範圍內的穩定存取體驗。定價和使用額度可能會根據早期用戶的使用情況和回饋持續調整與優化。", "workspace.lite.promo.subscribe": "訂閱 Go", "workspace.lite.promo.subscribing": "重新導向中...", "workspace.lite.promo.otherMethods": "其他付款方式", diff --git a/packages/console/app/src/routes/brand/index.css b/packages/console/app/src/routes/brand/index.css index 0e39f3742f..8a32651591 100644 --- a/packages/console/app/src/routes/brand/index.css +++ b/packages/console/app/src/routes/brand/index.css @@ -376,6 +376,7 @@ align-items: center; gap: 16px; opacity: 0; + transition: opacity 0.2s ease; @media (max-width: 40rem) { position: static; diff --git a/packages/console/app/src/routes/go/index.css b/packages/console/app/src/routes/go/index.css index bcf7c896a4..bde8c42986 100644 --- a/packages/console/app/src/routes/go/index.css +++ b/packages/console/app/src/routes/go/index.css @@ -504,33 +504,6 @@ body { pointer-events: none; } - [data-slot="xlabels"] { - position: absolute; - inset: 0; - pointer-events: none; - } - - [data-slot="xlabels"] [data-xlabel] { - position: absolute; - left: var(--x); - top: var(--y); - transform: translate(-50%, -50%); - color: var(--color-text-weak); - font-size: 12px; - line-height: 1; - white-space: nowrap; - - @media (prefers-color-scheme: light) { - color: color-mix(in srgb, var(--color-text-weak) 82%, var(--color-text-strong)); - } - - @media (max-width: 60rem) { - &:not([data-tick="1"], [data-tick="25"], [data-tick="100"], [data-tick="250"]) { - display: none; - } - } - } - [data-slot="ylabels"] [data-ylabel] { position: absolute; left: var(--x); @@ -614,12 +587,6 @@ body { display: block; } - [data-grid] { - stroke: var(--color-border); - stroke-width: 1; - opacity: 0.6; - } - [data-tick] { fill: var(--color-text-weak); font-size: 12px; @@ -1060,46 +1027,7 @@ body { margin-bottom: 32px; [data-slot="faq-models"] { - margin: 16px 0 0; - padding-left: 20px; - - li { - margin-bottom: 0; - list-style: disc; - } - } - - [data-slot="faq-model-table"] { - max-width: 560px; margin-top: 16px; - overflow-x: auto; - - table { - width: 100%; - border-collapse: collapse; - line-height: 150%; - } - - th, - td { - padding: 8px 12px; - border-bottom: 1px solid var(--color-border-weak); - text-align: left; - white-space: nowrap; - } - - th { - color: var(--color-text-strong); - font-weight: 500; - } - } - - [data-slot="faq-retention-notes"] { - margin-top: 16px; - - p { - margin: 8px 0 0; - } } } } diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 17c78f214d..9dedcdcbb4 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -23,25 +23,22 @@ const checkLoggedIn = query(async () => { }, "checkLoggedIn.get") const models = [ - { name: "Grok 4.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, - { name: "GPT 5.6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, - { name: "GLM-5.2", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "GLM-5.1", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Kimi K3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Kimi K2.7 Code", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Kimi K2.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "MiMo-V2.5-Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "MiMo-V2.5", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Qwen3.8 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Qwen3.7 Max", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Qwen3.7 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Qwen3.6 Plus", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "MiniMax M3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "MiniMax M2.7", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "DeepSeek V4 Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "DeepSeek V4 Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, - { name: "Hy3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, -] as const + "Grok 4.5", + "GLM-5.2", + "GLM-5.1", + "Kimi K3", + "Kimi K2.7 Code", + "Kimi K2.6", + "MiMo-V2.5-Pro", + "MiMo-V2.5", + "Qwen3.7 Max", + "Qwen3.7 Plus", + "Qwen3.6 Plus", + "MiniMax M3", + "MiniMax M2.7", + "DeepSeek V4 Pro", + "DeepSeek V4 Flash", +] function LimitsGraph(props: { href: string }) { let root!: HTMLElement @@ -67,14 +64,14 @@ function LimitsGraph(props: { href: string }) { const baseline = 100 const graph = [ { id: "grok-4.5", name: "Grok 4.5", req: 120, d: "50ms" }, - { id: "kimi-k3", name: "Kimi K3", req: 110, d: "75ms" }, - { id: "qwen3.8-max", name: "Qwen3.8 Max", req: 160, d: "90ms" }, + { id: "kimi-k3", name: "Kimi K3 (2x usage)", req: 220, baseReq: 110, d: "75ms" }, { id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" }, + { id: "qwen3.7-max", name: "Qwen3.7 Max", req: 950, d: "110ms" }, + { id: "kimi-k2.7-code", name: "Kimi K2.7 Code", req: 1150, d: "150ms" }, { id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" }, + { id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", req: 3250, d: "240ms" }, { id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", req: 3450, d: "270ms" }, - { id: "gpt-5.6-luna", name: "GPT 5.6 Luna (2x usage)", req: 4100, baseReq: 2050, d: "290ms" }, { id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" }, - { id: "hy3", name: "Hy3", req: 4300, d: "320ms" }, { id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" }, { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 31650, d: "340ms" }, ] @@ -83,7 +80,7 @@ function LimitsGraph(props: { href: string }) { const left = 40 const right = 60 const top = 18 - const bottom = 44 + const bottom = 18 const plot = w - left - right const ratio = (n: number) => n / baseline @@ -92,24 +89,6 @@ function LimitsGraph(props: { href: string }) { const base = 24 const p = 2.2 const x = (r: number) => left + base + Math.pow(log(r) / log(rmax), p) * (plot - base) - const ticks = [1, 5, 10, 25, 50, 100, 250].filter((t) => t <= rmax) - const labels = (() => { - const set = new Set() - let last = -Infinity - for (const t of ticks) { - if (t === 1) { - set.add(t) - last = x(t) - continue - } - const pos = x(t) - if (pos - last < 44) continue - set.add(t) - last = pos - } - return set - })() - const shown = ticks.filter((t) => labels.has(t)) const bh = 8 const gap = 20 const step = bh + gap @@ -119,7 +98,6 @@ function LimitsGraph(props: { href: string }) { const px = (n: number) => `${(n / w) * 100}%` const py = (n: number) => `${(n / h) * 100}%` const lx = px(left - 16) - const ty = py(h - 18) return (
- - diff --git a/packages/session-ui/src/components/message-file.test.ts b/packages/session-ui/src/components/message-file.test.ts index a769ae01bb..3882be027e 100644 --- a/packages/session-ui/src/components/message-file.test.ts +++ b/packages/session-ui/src/components/message-file.test.ts @@ -21,7 +21,7 @@ describe("message-file", () => { expect(attached(file())).toBe(false) }) - test("keeps data-backed file mentions inline", () => { + test("treats only non-attachment source ranges as inline references", () => { expect( inline( file({ @@ -34,16 +34,18 @@ describe("message-file", () => { ), ).toBe(true) - const mentioned = file({ - url: "data:text/plain;base64,SGVsbG8=", - source: { - type: "file", - path: "/repo/README.txt", - text: { value: "@README.txt", start: 0, end: 11 }, - }, - }) - expect(inline(mentioned)).toBe(true) - expect(attached(mentioned)).toBe(false) + expect( + inline( + file({ + url: "data:text/plain;base64,SGVsbG8=", + source: { + type: "file", + path: "/repo/README.txt", + text: { value: "@README.txt", start: 0, end: 11 }, + }, + }), + ), + ).toBe(false) }) test("separates image and file attachment kinds", () => { diff --git a/packages/session-ui/src/components/message-file.ts b/packages/session-ui/src/components/message-file.ts index e09269f33f..81ce97827f 100644 --- a/packages/session-ui/src/components/message-file.ts +++ b/packages/session-ui/src/components/message-file.ts @@ -3,10 +3,11 @@ import { getFilename } from "@opencode-ai/core/util/path" import type { FilePart } from "@opencode-ai/sdk/v2" export function attached(part: FilePart) { - return part.url.startsWith("data:") && !inline(part) + return part.url.startsWith("data:") } export function inline(part: FilePart) { + if (attached(part)) return false return part.source?.text?.start !== undefined && part.source?.text?.end !== undefined } diff --git a/packages/session-ui/src/components/message-part.tsx b/packages/session-ui/src/components/message-part.tsx index e63b5b5685..601d7839ef 100644 --- a/packages/session-ui/src/components/message-part.tsx +++ b/packages/session-ui/src/components/message-part.tsx @@ -62,6 +62,7 @@ import { AnimatedCountList } from "./tool-count-summary" import { ToolStatusTitle } from "./tool-status-title" import { patchFiles } from "./apply-patch-file" import { animate } from "motion" +import { useLocation } from "@solidjs/router" import { attached, inline, kind, typeLabel } from "./message-file" import { readPartText } from "./message-part-text" import { SessionProgressIndicatorV2 } from "../v2/components/session-progress-indicator-v2" @@ -520,7 +521,6 @@ export function getToolInfo( } } case "bash": - case "shell": return { icon: "console", title: i18n.t("ui.tool.shell"), @@ -538,7 +538,6 @@ export function getToolInfo( title: i18n.t("ui.messagePart.title.write"), subtitle: input.filePath ? getFilename(input.filePath) : undefined, } - case "patch": case "apply_patch": return { icon: "code-lines", @@ -582,18 +581,29 @@ function urls(text: string | undefined) { }) } -function sessionLink(id: string | undefined, href?: (id: string) => string | undefined) { - if (!id) return undefined - return href?.(id) +function sessionLink(id: string | undefined, path: string, href?: (id: string) => string | undefined) { + if (!id) return + + const direct = href?.(id) + if (direct) return direct + + const idx = path.indexOf("/session") + if (idx === -1) return + return `${path.slice(0, idx)}/session/${id}` +} + +function currentSession(path: string) { + return path.match(/\/session\/([^/?#]+)/)?.[1] } function taskSession( input: Record, - parentID: string | undefined, + path: string, sessions: Session[] | undefined, agents?: readonly { name: string; color?: string }[], ) { - if (!parentID) return undefined + const parentID = currentSession(path) + if (!parentID) return const description = typeof input.description === "string" ? input.description : "" const agent = taskAgent(input.subagent_type, agents).name return (sessions ?? []) @@ -719,8 +729,8 @@ export function renderable(part: PartType, showReasoningSummaries = true) { } function toolDefaultOpen(tool: string, shell = false, edit = false) { - if (tool === "bash" || tool === "shell") return shell - if (tool === "edit" || tool === "write" || tool === "patch" || tool === "apply_patch") return edit + if (tool === "bash") return shell + if (tool === "edit" || tool === "write" || tool === "apply_patch") return edit } export function partDefaultOpen(part: PartType, shell = false, edit = false) { @@ -1496,7 +1506,7 @@ export function registerTool(input: { name: string; render?: ToolComponent }) { } export function getTool(name: string) { - return state[name === "apply_patch" ? "patch" : name === "bash" ? "shell" : name]?.render + return state[name]?.render } export const ToolRegistry = { @@ -1563,7 +1573,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { }) const taskHref = createMemo(() => { if (part().tool !== "task") return - return sessionLink(taskId(), data.sessionHref) + return sessionLink(taskId(), useLocation().pathname, data.sessionHref) }) const taskSubtitle = createMemo(() => { if (part().tool !== "task") return undefined @@ -1602,14 +1612,6 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { onOpenChange={props.onToolOpenChange ? handleToolOpenChange : undefined} subtitle={taskSubtitle()} href={taskHref()} - onSubtitleClick={(event) => { - if (!data.navigateToSession) return - if (event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return - const id = taskId() - if (!id) return - event.preventDefault() - data.navigateToSession(id) - }} /> ) }} @@ -1990,10 +1992,11 @@ ToolRegistry.register({ render(props) { const data = useData() const i18n = useI18n() + const location = useLocation() const childSessionId = createMemo(() => { const value = props.metadata.sessionId if (typeof value === "string" && value) return value - return taskSession(props.input, data.sessionID, data.store.session, data.store.agent) + return taskSession(props.input, location.pathname, data.store.session, data.store.agent) }) const agent = createMemo(() => taskAgent(props.input.subagent_type, data.store.agent)) const title = createMemo(() => agent().name ?? i18n.t("ui.tool.agent.default")) @@ -2010,13 +2013,18 @@ ToolRegistry.register({ }) const running = createMemo(() => props.status === "pending" || props.status === "running") - const href = createMemo(() => sessionLink(childSessionId(), data.sessionHref)) + const href = createMemo(() => sessionLink(childSessionId(), location.pathname, data.sessionHref)) const clickable = createMemo(() => !!(childSessionId() && (data.navigateToSession || href()))) const open = () => { const id = childSessionId() if (!id) return - data.navigateToSession?.(id) + if (data.navigateToSession) { + data.navigateToSession(id) + return + } + const value = href() + if (value) window.location.assign(value) } const navigate = (event: MouseEvent) => { @@ -2093,7 +2101,7 @@ ToolRegistry.register({ }) ToolRegistry.register({ - name: "shell", + name: "bash", render(props) { const i18n = useI18n() const pending = () => props.status === "pending" || props.status === "running" @@ -2118,14 +2126,13 @@ ToolRegistry.register({ (
- +
@@ -2329,7 +2336,7 @@ ToolRegistry.register({ }) ToolRegistry.register({ - name: "patch", + name: "apply_patch", render(props) { const i18n = useI18n() const fileComponent = useFileComponent() diff --git a/packages/session-ui/src/components/session-diff.ts b/packages/session-ui/src/components/session-diff.ts index 2fbd022235..48e8eee310 100644 --- a/packages/session-ui/src/components/session-diff.ts +++ b/packages/session-ui/src/components/session-diff.ts @@ -1,7 +1,6 @@ import { parseDiffFromFile, parsePatchFiles, type FileDiffMetadata } from "@pierre/diffs" import { parsePatch } from "diff" import type { SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" type LegacyDiff = { file: string @@ -14,7 +13,7 @@ type LegacyDiff = { } type SnapshotDiff = SnapshotFileDiff & { file: string } -type ReviewDiff = SnapshotDiff | FileDiffInfo | VcsFileDiff | LegacyDiff +type ReviewDiff = SnapshotDiff | VcsFileDiff | LegacyDiff export type DiffSource = Pick export type ViewDiff = { diff --git a/packages/session-ui/src/components/session-review.tsx b/packages/session-ui/src/components/session-review.tsx index 1585a8aa32..8db21f025b 100644 --- a/packages/session-ui/src/components/session-review.tsx +++ b/packages/session-ui/src/components/session-review.tsx @@ -16,7 +16,6 @@ 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" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" import { type SelectedLineRange } from "@pierre/diffs" import { Dynamic } from "solid-js/web" @@ -63,10 +62,10 @@ export type SessionReviewCommentActions = { export type SessionReviewFocus = { file: string; id: string } -type RawReviewDiff = (SnapshotFileDiff | FileDiffInfo | VcsFileDiff) & { +type RawReviewDiff = (SnapshotFileDiff | VcsFileDiff) & { preloaded?: PreloadMultiFileDiffResult } -type ReviewDiff = ((SnapshotFileDiff & { file: string }) | FileDiffInfo | VcsFileDiff) & { +type ReviewDiff = ((SnapshotFileDiff & { file: string }) | VcsFileDiff) & { preloaded?: PreloadMultiFileDiffResult } type Item = ViewDiff & { preloaded?: PreloadMultiFileDiffResult } diff --git a/packages/session-ui/src/components/session-turn.tsx b/packages/session-ui/src/components/session-turn.tsx index 301a74d3f0..75274fc50e 100644 --- a/packages/session-ui/src/components/session-turn.tsx +++ b/packages/session-ui/src/components/session-turn.tsx @@ -4,7 +4,6 @@ import { Message as MessageType, Part as PartType, } from "@opencode-ai/sdk/v2/client" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import type { SessionStatus } from "@opencode-ai/sdk/v2" import { useData } from "../context" import { useFileComponent } from "@opencode-ai/ui/context/file" @@ -91,7 +90,7 @@ function list(value: T[] | undefined | null, fallback: T[]) { return fallback } -type SummaryDiff = (SnapshotFileDiff & { file: string }) | FileDiffInfo +type SummaryDiff = SnapshotFileDiff & { file: string } function summaryDiff(value: SnapshotFileDiff): value is SummaryDiff { return typeof value.file === "string" diff --git a/packages/session-ui/src/components/tool-error-card.tsx b/packages/session-ui/src/components/tool-error-card.tsx index 2251641857..35720a2753 100644 --- a/packages/session-ui/src/components/tool-error-card.tsx +++ b/packages/session-ui/src/components/tool-error-card.tsx @@ -16,7 +16,6 @@ export interface ToolErrorCardProps extends Omit, "c onOpenChange?: (open: boolean) => void subtitle?: string href?: string - onSubtitleClick?: (event: MouseEvent) => void } export function ToolErrorCard(props: ToolErrorCardProps) { @@ -36,7 +35,6 @@ export function ToolErrorCard(props: ToolErrorCardProps) { "onOpenChange", "subtitle", "href", - "onSubtitleClick", ]) const setOpen = (value: boolean) => { if (props.open === undefined) setState("open", value) @@ -53,8 +51,6 @@ export function ToolErrorCard(props: ToolErrorCardProps) { webfetch: "ui.tool.webfetch", websearch: "ui.tool.websearch", bash: "ui.tool.shell", - shell: "ui.tool.shell", - patch: "ui.tool.patch", apply_patch: "ui.tool.patch", question: "ui.tool.questions", } @@ -115,10 +111,7 @@ export function ToolErrorCard(props: ToolErrorCardProps) { data-slot="basic-tool-tool-subtitle" class="clickable subagent-link" href={split.href!} - onClick={(event) => { - event.stopPropagation() - split.onSubtitleClick?.(event) - }} + onClick={(e) => e.stopPropagation()} > {subtitle()} diff --git a/packages/session-ui/src/context/data.tsx b/packages/session-ui/src/context/data.tsx index 76eacb3336..999ff510d5 100644 --- a/packages/session-ui/src/context/data.tsx +++ b/packages/session-ui/src/context/data.tsx @@ -1,5 +1,4 @@ import type { Message, Session, Part, SnapshotFileDiff, SessionStatus, Provider } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { createSimpleContext } from "@opencode-ai/ui/context" import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" @@ -22,7 +21,7 @@ type Data = { [sessionID: string]: SessionStatus } session_diff: { - [sessionID: string]: (SnapshotFileDiff | FileDiffInfo)[] + [sessionID: string]: SnapshotFileDiff[] } session_diff_preload?: { [sessionID: string]: PreloadMultiFileDiffResult[] @@ -47,7 +46,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ init: (props: { data: Data directory: string - sessionID?: string onNavigateToSession?: NavigateToSessionFn onSessionHref?: SessionHrefFn }) => { @@ -58,9 +56,6 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ get directory() { return props.directory }, - get sessionID() { - return props.sessionID - }, navigateToSession: props.onNavigateToSession, sessionHref: props.onSessionHref, } diff --git a/packages/session-ui/src/v2/components/prompt-input/attachments.css b/packages/session-ui/src/v2/components/prompt-input/attachments.css deleted file mode 100644 index 3b8173d105..0000000000 --- a/packages/session-ui/src/v2/components/prompt-input/attachments.css +++ /dev/null @@ -1,44 +0,0 @@ -@keyframes prompt-input-v2-attachments-fade-left { - from { - visibility: hidden; - } - to { - visibility: visible; - } -} - -@keyframes prompt-input-v2-attachments-fade-right { - from { - visibility: visible; - } - to { - visibility: hidden; - } -} - -[data-component="prompt-input-v2-attachments"] { - timeline-scope: --prompt-input-v2-attachments-scroll; - - [data-slot^="prompt-attachments-fade-"] { - visibility: hidden; - } -} - -@supports (animation-timeline: --prompt-input-v2-attachments-scroll) and - (timeline-scope: --prompt-input-v2-attachments-scroll) { - [data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-scroll"] { - scroll-timeline: --prompt-input-v2-attachments-scroll x; - } - - [data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-fade-left"] { - animation: prompt-input-v2-attachments-fade-left linear both; - animation-timeline: --prompt-input-v2-attachments-scroll; - animation-range: 0 0.1px; - } - - [data-component="prompt-input-v2-attachments"] [data-slot="prompt-attachments-fade-right"] { - animation: prompt-input-v2-attachments-fade-right linear both; - animation-timeline: --prompt-input-v2-attachments-scroll; - animation-range: calc(100% - 1.1px) calc(100% - 1px); - } -} diff --git a/packages/session-ui/src/v2/components/prompt-input/attachments.ts b/packages/session-ui/src/v2/components/prompt-input/attachments.ts index 89f38ec4a3..8c7b12e9ae 100644 --- a/packages/session-ui/src/v2/components/prompt-input/attachments.ts +++ b/packages/session-ui/src/v2/components/prompt-input/attachments.ts @@ -74,11 +74,9 @@ export type PromptInputV2AttachmentConfig = { directory: () => string isDialogActive: () => boolean warn: () => void - duplicate: () => void onError: (error: unknown) => void readClipboardImage?: () => Promise getPathForFile?: (file: File) => string - store?: (file: File) => Promise<{ id: string; url: string }> } export function createPromptInputV2Attachments( @@ -96,38 +94,22 @@ export function createPromptInputV2Attachments( if (!editor) return return { prompt, cursor: prompt.cursor() ?? cursorPosition(editor) } } - const add = async (file: File, toast = true, target = capture(), clipboard = false) => { + const add = async (file: File, toast = true, target = capture()) => { if (!target) return false const mime = await attachmentMime(file) if (!mime) { if (toast) input.warn() return false } - const blob = input.store ? await input.store(file) : await blobReference(file) - const sourcePath = input.getPathForFile?.(file) || undefined - // Native clipboard images arrive with a fresh timestamped filename on every paste, so identical - // clipboard content is matched on bytes alone. - const duplicate = target.prompt - .current() - .some( - (part) => - part.type === "image" && - part.blob.id === blob.id && - (sourcePath - ? part.sourcePath === sourcePath - : !part.sourcePath && (clipboard || part.filename === file.name)), - ) - if (duplicate) { - input.duplicate() - return true - } + const url = await dataUrl(file, mime) + if (!url) return false const attachment: PromptInputV2Attachment = { type: "image", id: globalThis.crypto?.randomUUID?.() ?? Math.random().toString(16).slice(2), filename: file.name, - sourcePath, + sourcePath: input.getPathForFile?.(file) || undefined, mime, - blob, + dataUrl: url, } target.prompt.set([...target.prompt.current(), attachment], target.cursor) return true @@ -159,7 +141,7 @@ export function createPromptInputV2Attachments( const plainText = clipboardData.getData("text/plain") ?? "" if (input.readClipboardImage && !plainText) { const file = await input.readClipboardImage() - if (file && (await add(file, true, target, true))) return + if (file && (await add(file, true, target))) return } if (!plainText) return const text = plainText.includes("\r") ? plainText.replace(/\r\n?/g, "\n") : plainText @@ -219,14 +201,20 @@ export function createPromptInputV2Attachments( } } -const imageMimes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]) - -async function blobReference(file: File) { - const id = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", await file.arrayBuffer()))) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") - return { id, url: URL.createObjectURL(file) } +function dataUrl(file: File, mime: string) { + return new Promise((resolve) => { + const reader = new FileReader() + reader.addEventListener("error", () => resolve("")) + reader.addEventListener("load", () => { + const value = typeof reader.result === "string" ? reader.result : "" + const index = value.indexOf(",") + resolve(index === -1 ? value : `data:${mime};base64,${value.slice(index + 1)}`) + }) + reader.readAsDataURL(file) + }) } + +const imageMimes = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]) const imageExtensions = new Map([ ["gif", "image/gif"], ["jpeg", "image/jpeg"], diff --git a/packages/session-ui/src/v2/components/prompt-input/index.tsx b/packages/session-ui/src/v2/components/prompt-input/index.tsx index d29fd4df23..9b011f03e9 100644 --- a/packages/session-ui/src/v2/components/prompt-input/index.tsx +++ b/packages/session-ui/src/v2/components/prompt-input/index.tsx @@ -21,7 +21,6 @@ import type { PromptInputV2Suggestion, } from "./types" import type { PromptInputV2Interaction, PromptInputV2SelectControl } from "./interaction" -import "./attachments.css" export type { PromptInputV2Attachment, @@ -40,7 +39,6 @@ export type PromptInputV2Props = { borderUnderlay?: boolean class?: string modelControl?: JSX.Element - variantControlVisible?: boolean attachKeybind?: string[] attachShortcut?: string } @@ -108,7 +106,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
- + {(control) => ( - + )} + {(control) => ( )} @@ -234,13 +232,13 @@ export function PromptInputV2(props: PromptInputV2Props) { > {props.modelControl} - + {(control) => ( - 1}> + 1}> )} @@ -379,7 +377,7 @@ export function PromptInputV2Attachments(props: { }) { return ( 0 || (props.comments?.length ?? 0) > 0}> -
+
{attachment.filename} props.onAttachmentClick?.(attachment)} @@ -445,14 +443,8 @@ export function PromptInputV2Attachments(props: { )}
-
-
+
+
) diff --git a/packages/session-ui/src/v2/components/prompt-input/interaction.ts b/packages/session-ui/src/v2/components/prompt-input/interaction.ts index f4a9fa74b7..989ebcac20 100644 --- a/packages/session-ui/src/v2/components/prompt-input/interaction.ts +++ b/packages/session-ui/src/v2/components/prompt-input/interaction.ts @@ -82,7 +82,9 @@ export function createPromptInputV2Controller(input: { draft.addMention(part) return true } - draft.addText(part.content) + const text = draft.state.prompt.map((item) => ("content" in item ? item.content : "")).join("") + const cursor = draft.state.cursor ?? text.length + draft.setText(text.slice(0, cursor) + part.content + text.slice(cursor)) return true } const attachments = input.attachments @@ -162,24 +164,21 @@ export function createPromptInputV2Controller(input: { function dispatch(event: PromptInputV2InteractionEvent) { const mode = state.mode const result = transitionPromptInputV2(state, event, draft.state) - const action = event.type === "popover.select" ? input.onSuggestionSelect?.(event.item) : undefined - if (event.type === "popover.select") { - if (!action || state.popover.type !== "command-menu") result.commands.forEach(execute) - if (action && event.item.kind === "command" && state.popover.type !== "command-menu") { - draft.setPrompt( - draft.state.prompt.filter((part): part is PromptInputV2Attachment => part.type === "image"), - 0, - ) - } - } setState(reconcile(result.state)) - if (event.type !== "popover.select") result.commands.forEach(execute) + result.commands.forEach(execute) if (mode !== result.state.mode) { if (result.state.mode === "shell") input.view.shell?.onOpen() if (result.state.mode === "normal") input.view.shell?.onClose() } if (event.type === "popover.select") { + const action = input.onSuggestionSelect?.(event.item) if (!action) return result.handled + if (event.item.kind === "command") { + draft.setPrompt( + draft.state.prompt.filter((part): part is PromptInputV2Attachment => part.type === "image"), + 0, + ) + } action() } return result.handled diff --git a/packages/session-ui/src/v2/components/prompt-input/machine.test.ts b/packages/session-ui/src/v2/components/prompt-input/machine.test.ts index e36982666e..4b75e08bcf 100644 --- a/packages/session-ui/src/v2/components/prompt-input/machine.test.ts +++ b/packages/session-ui/src/v2/components/prompt-input/machine.test.ts @@ -26,33 +26,6 @@ describe("prompt input v2 interaction machine", () => { expect(closed.state.popover).toEqual({ type: "closed" }) }) - test("completes nested slash command names", () => { - const open = transitionPromptInputV2( - createPromptInputV2InteractionState(), - { type: "input.changed", value: "/review/" }, - persisted(), - ) - const item = { ...command, label: "/review/nested" } - const selected = transitionPromptInputV2(open.state, { type: "popover.select", item }, persisted("/review/")) - - expect(open.state.popover).toEqual({ type: "command-inline", query: "review/" }) - expect(selected.commands).toContainEqual({ type: "draft.setText", value: "/review/nested " }) - }) - - test("opens context completion at the cursor", () => { - const value = "alpha @sr omega" - const input = persisted(value) - input.cursor = 9 - - const result = transitionPromptInputV2( - createPromptInputV2InteractionState(), - { type: "input.changed", value, persist: false }, - input, - ) - - expect(result.state.popover).toEqual({ type: "context", query: "sr" }) - }) - test("enters shell mode from an initial exclamation mark", () => { const result = transitionPromptInputV2( createPromptInputV2InteractionState(), diff --git a/packages/session-ui/src/v2/components/prompt-input/machine.ts b/packages/session-ui/src/v2/components/prompt-input/machine.ts index d2508e249c..7102b79318 100644 --- a/packages/session-ui/src/v2/components/prompt-input/machine.ts +++ b/packages/session-ui/src/v2/components/prompt-input/machine.ts @@ -61,7 +61,7 @@ export function transitionPromptInputV2( event: PromptInputV2InteractionEvent, persisted: PromptInputV2PersistedState, ): PromptInputV2Transition { - if (event.type === "input.changed") return inputChanged(state, event.value, event.persist !== false, persisted.cursor) + if (event.type === "input.changed") return inputChanged(state, event.value, event.persist !== false) if (event.type === "commands.open") return openCommands(state, persisted) if (event.type === "context.open") return openContext(state, persisted) if (event.type === "popover.query") return queryChanged(state, event.value) @@ -81,19 +81,14 @@ export function transitionPromptInputV2( return changed({ ...state, focus: "external" }) } -function inputChanged( - state: PromptInputV2InteractionState, - value: string, - persist: boolean, - cursor: number | undefined, -): PromptInputV2Transition { +function inputChanged(state: PromptInputV2InteractionState, value: string, persist: boolean): PromptInputV2Transition { const setText: PromptInputV2InteractionCommand[] = persist ? [{ type: "draft.setText", value }] : [] if (state.mode === "normal" && value === "!") { return changed({ ...state, mode: "shell", popover: { type: "closed" }, focus: "editor" }, [ { type: "draft.setText", value: "" }, ]) } - const context = value.slice(0, cursor ?? value.length).match(/(?:^|\s)@([^\s@]*)$/) + const context = value.match(/(?:^|\s)@([^\s@]*)$/) if (context) { const query = context[1] ?? "" return changed({ ...state, popover: { type: "context", query }, focus: "editor" }, [ @@ -102,7 +97,7 @@ function inputChanged( ]) } - const command = value.match(/^\/(\S*)$/) + const command = value.match(/^\/([^\s/]*)$/) if (command) { const query = command[1] ?? "" return changed({ ...state, popover: { type: "command-inline", query }, focus: "editor" }, [ @@ -240,7 +235,7 @@ function populated(persisted: PromptInputV2PersistedState) { } function replaceTrigger(value: string, trigger: "@" | "/", replacement: string) { - const index = trigger === "/" ? value.indexOf(trigger) : value.lastIndexOf(trigger) + const index = value.lastIndexOf(trigger) return index < 0 ? replacement : value.slice(0, index) + replacement } diff --git a/packages/session-ui/src/v2/components/prompt-input/prompt-input.stories.tsx b/packages/session-ui/src/v2/components/prompt-input/prompt-input.stories.tsx index 6c535b0284..525c264097 100644 --- a/packages/session-ui/src/v2/components/prompt-input/prompt-input.stories.tsx +++ b/packages/session-ui/src/v2/components/prompt-input/prompt-input.stories.tsx @@ -121,7 +121,7 @@ function ControlledPromptInput() { id: "attachment-1", filename: "requirements.md", mime: "text/markdown", - blob: { id: "requirements", url: "data:text/markdown;base64,IyBSZXF1aXJlbWVudHM=" }, + dataUrl: "data:text/markdown;base64,IyBSZXF1aXJlbWVudHM=", }, ], cursor: 0, @@ -199,7 +199,7 @@ function ControlledPromptInput() { id: `attachment-${store.state.prompt.filter((part) => part.type === "image").length + 1}`, filename, mime, - blob: { id: filename, url: `data:${mime};base64,` }, + dataUrl: `data:${mime};base64,`, }) } diff --git a/packages/session-ui/src/v2/components/prompt-input/store.test.ts b/packages/session-ui/src/v2/components/prompt-input/store.test.ts index 768a222450..99e5f6102e 100644 --- a/packages/session-ui/src/v2/components/prompt-input/store.test.ts +++ b/packages/session-ui/src/v2/components/prompt-input/store.test.ts @@ -13,7 +13,7 @@ function createPromptStore() { id: "attachment-1", filename: "notes.txt", mime: "text/plain", - blob: { id: "a", url: "blob:a" }, + dataUrl: "data:text/plain;base64,", }, ], cursor: 3, @@ -50,34 +50,12 @@ describe("prompt input v2 store", () => { id: "attachment-1", filename: "notes.txt", mime: "text/plain", - blob: { id: "a", url: "blob:a" }, + dataUrl: "data:text/plain;base64,", }, ]) expect(prompt.state.cursor).toBe(7) }) - test("inserts text without flattening structured mentions", () => { - const [state, setState] = createStore({ - prompt: [ - { type: "text", content: "A ", start: 0, end: 2 }, - { type: "file", path: "one", content: "@one", start: 2, end: 6 }, - { type: "text", content: " B", start: 6, end: 8 }, - ], - cursor: 2, - context: { items: [] }, - }) - const prompt = createPromptInputV2Store([state, setState]) - - prompt.addText("X\nY") - - expect(prompt.state.prompt).toEqual([ - { type: "text", content: "A X\nY", start: 0, end: 5 }, - { type: "file", path: "one", content: "@one", start: 5, end: 9 }, - { type: "text", content: " B", start: 9, end: 11 }, - ]) - expect(prompt.state.cursor).toBe(5) - }) - test("mutates context, attachments, and model through shared actions", () => { const prompt = createPromptStore() const context = { key: "file:src/index.ts", type: "file" as const, path: "src/index.ts" } diff --git a/packages/session-ui/src/v2/components/prompt-input/store.ts b/packages/session-ui/src/v2/components/prompt-input/store.ts index 5a27de6c4d..d924c554ac 100644 --- a/packages/session-ui/src/v2/components/prompt-input/store.ts +++ b/packages/session-ui/src/v2/components/prompt-input/store.ts @@ -47,13 +47,6 @@ export function createPromptInputV2Store(input: PromptInputV2StoreInput) { setStore()("cursor", content.length) }) }, - addText(content: string) { - const cursor = store().cursor ?? promptLength(store().prompt) - batch(() => { - setStore()("prompt", (prompt) => insertText(prompt, cursor, content)) - setStore()("cursor", cursor + content.length) - }) - }, reset() { batch(() => { setStore()("prompt", [{ type: "text", content: "", start: 0, end: 0 }]) @@ -93,27 +86,6 @@ export function createPromptInputV2Store(input: PromptInputV2StoreInput) { export type PromptInputV2Store = ReturnType -function insertText(prompt: PromptInputV2Prompt, cursor: number, content: string): PromptInputV2Prompt { - let position = 0 - let inserted = false - const parts = prompt.flatMap((part) => { - if (part.type === "image") return [part] - const start = position - position += part.content.length - if (inserted) return [part] - if (part.type === "text" && cursor >= start && cursor <= position) { - inserted = true - const offset = cursor - start - return [{ ...part, content: part.content.slice(0, offset) + content + part.content.slice(offset) }] - } - if (cursor > start) return [part] - inserted = true - return [{ type: "text", content, start: 0, end: 0 }, part] - }) - if (!inserted) parts.push({ type: "text", content, start: 0, end: 0 }) - return withOffsets(parts) -} - function insertMention( prompt: PromptInputV2Prompt, start: number, @@ -134,19 +106,11 @@ function insertMention( { type: "text" as const, content: ` ${after}`, start: 0, end: 0 }, ] }) - return withOffsets(parts) -} - -function withOffsets(prompt: PromptInputV2Prompt): PromptInputV2Prompt { let offset = 0 - return prompt.map((part) => { + return parts.map((part) => { if (part.type === "image") return part const next = { ...part, start: offset, end: offset + part.content.length } offset = next.end return next }) } - -function promptLength(prompt: PromptInputV2Prompt) { - return prompt.reduce((length, part) => length + ("content" in part ? part.content.length : 0), 0) -} diff --git a/packages/session-ui/src/v2/components/prompt-input/types.ts b/packages/session-ui/src/v2/components/prompt-input/types.ts index c6b47a92ce..a2630a5ea6 100644 --- a/packages/session-ui/src/v2/components/prompt-input/types.ts +++ b/packages/session-ui/src/v2/components/prompt-input/types.ts @@ -31,7 +31,7 @@ export type PromptInputV2Attachment = { filename: string sourcePath?: string mime: string - blob: { id: string; url: string } + dataUrl: string } export type PromptInputV2Prompt = ( diff --git a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx index ba276a8f52..9f42cd90fe 100644 --- a/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx @@ -7,7 +7,6 @@ import { useI18n } from "@opencode-ai/ui/context/i18n" import { mediaKindFromPath } from "../../pierre/media" import { cloneSelectedLineRange, previewSelectedLines } from "../../pierre/selection-bridge" import type { FileContent, SnapshotFileDiff, VcsFileDiff } from "@opencode-ai/sdk/v2" -import type { FileDiffInfo } from "@opencode-ai/client/promise" import { createEffect, createMemo, onCleanup, Show, untrack } from "solid-js" import { createStore } from "solid-js/store" import { Dynamic } from "solid-js/web" @@ -28,7 +27,7 @@ import { LineCommentV2OverflowIcon } from "@opencode-ai/ui/v2/line-comment-v2" import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2" import "./session-review-v2.css" -type ReviewDiff = (SnapshotFileDiff & { file: string }) | FileDiffInfo | VcsFileDiff +type ReviewDiff = (SnapshotFileDiff & { file: string }) | VcsFileDiff export type SessionReviewFilePreviewV2Props = { file: string diff --git a/packages/session-ui/src/v2/components/session-review-v2.css b/packages/session-ui/src/v2/components/session-review-v2.css index 704e8fd7d7..751c258a52 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.css +++ b/packages/session-ui/src/v2/components/session-review-v2.css @@ -63,7 +63,7 @@ align-items: center; justify-content: space-between; gap: 8px; - padding: 12px 16px 12px 8px; + padding: 12px 16px 8px 8px; flex-shrink: 0; } @@ -91,7 +91,7 @@ } [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-filter"] { - padding: 0 8px 4px; + padding: 0 8px 8px; flex-shrink: 0; min-width: 0; box-sizing: border-box; @@ -109,71 +109,10 @@ [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] { flex: 1; min-height: 0; - timeline-scope: --session-review-v2-sidebar-tree-scroll; -} - -@keyframes session-review-v2-sidebar-tree-fade-top { - from { - visibility: hidden; - } - to { - visibility: visible; - } -} - -@keyframes session-review-v2-sidebar-tree-fade-bottom { - from { - visibility: visible; - } - to { - visibility: hidden; - } -} - -[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before, -[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { - content: ""; - position: absolute; - inset-inline: 0; - z-index: 10; - height: 16px; - pointer-events: none; - visibility: hidden; -} - -[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before { - top: 0; - background: linear-gradient(to bottom, var(--v2-background-bg-base), transparent); -} - -[data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { - bottom: 0; - background: linear-gradient(to top, var(--v2-background-bg-base), transparent); -} - -@supports (animation-timeline: --session-review-v2-sidebar-tree-scroll) and - (timeline-scope: --session-review-v2-sidebar-tree-scroll) { - [data-component="session-review-v2-sidebar-root"] - [data-slot="session-review-v2-sidebar-tree"] - .scroll-view__viewport { - scroll-timeline: --session-review-v2-sidebar-tree-scroll y; - } - - [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::before { - animation: session-review-v2-sidebar-tree-fade-top linear both; - animation-timeline: --session-review-v2-sidebar-tree-scroll; - animation-range: 0 0.1px; - } - - [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"]::after { - animation: session-review-v2-sidebar-tree-fade-bottom linear both; - animation-timeline: --session-review-v2-sidebar-tree-scroll; - animation-range: calc(100% - 1.1px) calc(100% - 1px); - } } [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__viewport { - padding: 4px 8px 12px; + padding: 0 8px 12px; } [data-component="session-review-v2-sidebar-root"] [data-slot="session-review-v2-sidebar-tree"] .scroll-view__thumb { @@ -305,7 +244,7 @@ } [data-component="icon-button-v2"][data-variant="ghost"].session-review-v2-sidebar-toggle[aria-expanded="true"] { - background-color: var(--v2-background-bg-layer-02); + background-color: var(--v2-overlay-simple-overlay-pressed); } [data-component="session-review-v2"] [data-slot="session-review-v2-file-header"] { diff --git a/packages/session-ui/src/v2/components/session-review-v2.tsx b/packages/session-ui/src/v2/components/session-review-v2.tsx index d2f3b6b8a6..abbe90d112 100644 --- a/packages/session-ui/src/v2/components/session-review-v2.tsx +++ b/packages/session-ui/src/v2/components/session-review-v2.tsx @@ -165,13 +165,11 @@ export function SessionReviewV2(props: SessionReviewV2Props) { } const prev = () => { - if (!canCycle()) return - return props.files[(fileIndex() - 1 + props.files.length) % props.files.length] + return props.files[fileIndex() - 1] } const next = () => { - if (!canCycle()) return - return props.files[(fileIndex() + 1) % props.files.length] + return props.files[fileIndex() + 1] } const canCycle = () => props.files.length > 0 @@ -185,15 +183,15 @@ export function SessionReviewV2(props: SessionReviewV2Props) { props.onSelectFile(file) } - // Keep the advertised arrow keys working while the + // The prev/next tooltips advertise < and >; keep the keys working while the // pane is mounted, but never while typing in an input or comment editor. makeEventListener(document, "keydown", (event) => { if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return - if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return + if (event.key !== "<" && event.key !== ">") return const target = event.target if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input, textarea, select"))) return if (!props.hasDiffs || !canCycle()) return - const file = event.key === "ArrowLeft" ? prev() : next() + const file = event.key === "<" ? prev() : next() if (!file) return event.preventDefault() cycle(file) @@ -221,7 +219,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) { value={ <> {i18n.t("ui.sessionReviewV2.previousFile")} - + } > @@ -241,7 +239,7 @@ export function SessionReviewV2(props: SessionReviewV2Props) { value={ <> {i18n.t("ui.sessionReviewV2.nextFile")} - + "]} variant="neutral" /> } > @@ -333,7 +331,6 @@ export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled? class="session-review-v2-sidebar-toggle" aria-label={i18n.t("ui.sessionReviewV2.toggleSidebar")} aria-expanded={props.opened} - data-expanded={props.opened ? "" : undefined} disabled={props.disabled} onClick={props.onToggle} icon={} diff --git a/packages/slack/package.json b/packages/slack/package.json index 46d7d185e3..5f1eba75ef 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.11", + "version": "1.18.3", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index a04d6761a7..ee85693737 100644 --- a/packages/stats/app/package.json +++ b/packages/stats/app/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-app", - "version": "1.18.11", + "version": "1.18.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/app/src/routes/model-catalog.ts b/packages/stats/app/src/routes/model-catalog.ts index 47fa1cf347..efd6f6b2e0 100644 --- a/packages/stats/app/src/routes/model-catalog.ts +++ b/packages/stats/app/src/routes/model-catalog.ts @@ -1,8 +1,8 @@ import { query } from "@solidjs/router" -export const modelCatalogSourceUrl = "https://models.opencode.ai/catalog.json" -export const modelCatalogPricingUrl = "https://models.opencode.ai/api.json" -export const modelCatalogLabSourceUrl = "https://models.opencode.ai/labs" +export const modelCatalogSourceUrl = "https://models.dev/catalog.json" +export const modelCatalogPricingUrl = "https://models.dev/api.json" +export const modelCatalogLabSourceUrl = "https://models.dev/labs" export type ModelCatalogCost = { input: number diff --git a/packages/stats/core/package.json b/packages/stats/core/package.json index e02d8e1314..d942879028 100644 --- a/packages/stats/core/package.json +++ b/packages/stats/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-core", - "version": "1.18.11", + "version": "1.18.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/server/package.json b/packages/stats/server/package.json index 9eb222ec80..eba0143a2b 100644 --- a/packages/stats/server/package.json +++ b/packages/stats/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/stats-server", - "version": "1.18.11", + "version": "1.18.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx b/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx index 0741f7e4ef..0c7ff8bbdd 100644 --- a/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx +++ b/packages/storybook/.storybook/mocks/app/components/dialog-select-model.tsx @@ -1,8 +1,9 @@ -import { splitProps, type JSX } from "solid-js" +import { splitProps } from "solid-js" -export function ModelSelectorPopover(props: { trigger: (props: Record) => JSX.Element }) { - const [local] = splitProps(props, ["trigger"]) - return <>{local.trigger({})} +export function ModelSelectorPopover(props: { triggerAs: any; triggerProps?: Record; children: any }) { + const [local] = splitProps(props, ["triggerAs", "triggerProps", "children"]) + const Trigger = local.triggerAs + return {local.children} } export const ModelSelectorPopoverV2 = ModelSelectorPopover diff --git a/packages/storybook/.storybook/mocks/app/context/platform.ts b/packages/storybook/.storybook/mocks/app/context/platform.ts index bab923bff5..74233cd1e5 100644 --- a/packages/storybook/.storybook/mocks/app/context/platform.ts +++ b/packages/storybook/.storybook/mocks/app/context/platform.ts @@ -2,10 +2,13 @@ import type { Platform } from "../../../../../app/src/context/platform" const value: Platform = { platform: "web", - openExternal() {}, + openLink() {}, restart: async () => {}, + back() {}, + forward() {}, notify: async () => {}, fetch: globalThis.fetch.bind(globalThis), + parseMarkdown: async (markdown: string) => markdown, } export function usePlatform() { diff --git a/packages/tui/package.json b/packages/tui/package.json index e1bc700ac7..5addcdaa0b 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "1.18.11", + "version": "1.18.3", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index 957f3151e9..1681b66fab 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "1.18.11", + "version": "1.18.3", "type": "module", "license": "MIT", "repository": { @@ -104,7 +104,6 @@ "remend": "catalog:", "shiki": "catalog:", "solid-list": "catalog:", - "solid-sonner": "catalog:", "strip-ansi": "7.1.2" }, "peerDependencies": { diff --git a/packages/ui/src/v2/components/progress-circle-v2.css b/packages/ui/src/v2/components/progress-circle-v2.css index 350442f847..2f84ed4c29 100644 --- a/packages/ui/src/v2/components/progress-circle-v2.css +++ b/packages/ui/src/v2/components/progress-circle-v2.css @@ -8,6 +8,6 @@ } [data-component="progress-circle-v2"] [data-slot="progress-circle-v2-progress"] { - stroke: var(--v2-icon-icon-base); + stroke: var(--v2-icon-icon-muted); transition: stroke-dashoffset 0.35s cubic-bezier(0.65, 0, 0.35, 1); } diff --git a/packages/ui/src/v2/components/select-v2.tsx b/packages/ui/src/v2/components/select-v2.tsx index 57348ed392..7f9a2b5d37 100644 --- a/packages/ui/src/v2/components/select-v2.tsx +++ b/packages/ui/src/v2/components/select-v2.tsx @@ -121,7 +121,6 @@ export function SelectV2(props: SelectV2Props) { {...others} multiple={false} - allowDuplicateSelectionEvents={false} disabled={local.disabled} data-component="select-v2-root" placement={local.placement ?? (inline() ? "bottom-end" : "bottom-start")} diff --git a/packages/ui/src/v2/components/switch-v2.css b/packages/ui/src/v2/components/switch-v2.css index 8ee56319c3..48d18d2c25 100644 --- a/packages/ui/src/v2/components/switch-v2.css +++ b/packages/ui/src/v2/components/switch-v2.css @@ -30,7 +30,7 @@ border: none; background: linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%), - var(--v2-background-bg-layer-04); + var(--v2-background-bg-layer-03); box-shadow: var(--v2-elevation-switch-off); transition: background 90ms ease-out, @@ -90,7 +90,7 @@ background: linear-gradient(0deg, var(--v2-overlay-simple-overlay-hover), var(--v2-overlay-simple-overlay-hover)), linear-gradient(180deg, var(--v2-alpha-light-0) 0%, var(--v2-alpha-light-20) 100%), - var(--v2-background-bg-layer-04); + var(--v2-background-bg-layer-03); } &:hover:not([data-disabled], [data-readonly]) [data-slot="switch-thumb"] { diff --git a/packages/ui/src/v2/components/tabs-v2.css b/packages/ui/src/v2/components/tabs-v2.css index 70daef1395..70abf78606 100644 --- a/packages/ui/src/v2/components/tabs-v2.css +++ b/packages/ui/src/v2/components/tabs-v2.css @@ -199,6 +199,7 @@ width: 100%; height: 28px; border-radius: 4px; + border: 0.5px solid transparent; box-sizing: border-box; color: var(--v2-text-text-muted); } @@ -221,4 +222,5 @@ [data-slot="tabs-v2-trigger-wrapper"]:has([data-selected]) { background-color: var(--v2-background-bg-layer-03); color: var(--v2-text-text-base); + border: 0.5px solid var(--v2-border-border-muted); } diff --git a/packages/ui/src/v2/components/toast-v2.css b/packages/ui/src/v2/components/toast-v2.css index db173489a9..5bce87dbdc 100644 --- a/packages/ui/src/v2/components/toast-v2.css +++ b/packages/ui/src/v2/components/toast-v2.css @@ -1,182 +1,58 @@ -.toast-v2-region { +[data-component="toast-v2-region"] { + position: fixed; + bottom: 48px; + right: 32px; z-index: 1000; + display: flex; + flex-direction: column; + gap: 12px; max-width: min(320px, calc(100vw - 64px)); + max-height: none; + width: 100%; + overflow: visible; pointer-events: none; - user-select: none; - -webkit-user-select: none; + + [data-slot="toast-v2-list"] { + display: flex; + flex-direction: column; + gap: 12px; + list-style: none; + margin: 0; + padding: 0; + max-height: none; + overflow-y: visible; + overflow-x: visible; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } + } } -.toast-v2 { - display: grid; - grid-template-columns: minmax(0, 1fr) 20px; - column-gap: 12px; - row-gap: 12px; +[data-component="toast-v2"] { + position: relative; + display: flex; + flex-direction: column; + gap: 12px; width: 320px; - max-width: 100%; padding: 12px; max-height: min(420px, calc(100dvh - 96px)); overflow: hidden; pointer-events: auto; - box-sizing: border-box; - outline: none; - user-select: none; - -webkit-user-select: none; + transition: transform 140ms ease-out; + border-radius: 8px; color: var(--v2-text-text-base); background: var(--v2-background-bg-layer-01); box-shadow: var(--v2-elevation-floating); - transition: - transform 280ms cubic-bezier(0.2, 0, 0, 1), - opacity 160ms ease-out, - height 280ms cubic-bezier(0.2, 0, 0, 1), - box-shadow 160ms ease-out; - &:has(> [data-icon]) { - grid-template-columns: 16px minmax(0, 1fr) 20px; + &[data-opened] { + animation: toastV2PopIn 140ms ease-out; } - &:focus-visible { - box-shadow: - var(--v2-elevation-floating), - 0 0 0 2px var(--v2-border-border-focus); - } - - &[data-expanded="true"] { - overflow: visible; - } - - &[data-expanded="false"][data-front="false"] > * { - opacity: 0; - } - - &[data-expanded="false"][data-front="false"] { - --y: translateY(calc(-1 * var(--toasts-before) * 9.5px)) scale(calc(1 - var(--toasts-before) * 0.05)); - } - - > [data-icon] { - grid-column: 1; - grid-row: 1; - align-self: start; - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 20px; - color: var(--v2-icon-icon-base); - } - - > [data-content] { - grid-column: 1; - grid-row: 1; - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; - overflow: hidden; - } - - &:has(> [data-icon]) > [data-content] { - grid-column: 2; - } - - > [data-close-button] { - grid-column: 2; - grid-row: 1; - position: static; - align-self: start; - width: 20px; - height: 20px; - min-width: 20px; - min-height: 20px; - padding: 0; - border: 0; - border-radius: 4px; - transform: none; - background: transparent; - color: var(--v2-icon-icon-muted); - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - } - - &:has(> [data-icon]) > [data-close-button] { - grid-column: 3; - } - - > [data-close-button]:hover { - background: var(--v2-overlay-simple-overlay-hover); - color: var(--v2-icon-icon-base); - } - - > [data-close-button]:active { - background: var(--v2-overlay-simple-overlay-pressed); - } - - > [data-close-button]:focus-visible { - outline: 2px solid var(--v2-border-border-focus); - outline-offset: 2px; - } - - > [data-icon] > *, - > [data-icon] svg, - > [data-close-button] svg { - width: 16px; - height: 16px; - display: block; - flex-shrink: 0; - } - - [data-title], - [data-slot="toast-v2-title"] { - color: var(--v2-text-text-base); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 13px; - font-style: normal; - font-weight: 530; - line-height: 20px; - letter-spacing: -0.04px; - font-synthesis: none; - margin: 0; - } - - [data-description], - [data-slot="toast-v2-description"] { - color: var(--v2-text-text-muted); - text-wrap-style: pretty; - overflow-wrap: anywhere; - word-break: break-word; - font-size: 13px; - font-style: normal; - font-weight: 440; - line-height: 20px; - letter-spacing: -0.04px; - font-synthesis: none; - margin: 0; - } - - [data-slot="toast-v2-actions"] { - grid-column: 1; - grid-row: 2; - display: flex; - flex-wrap: wrap; - gap: 8px; - } - - &:has(> [data-icon]) [data-slot="toast-v2-actions"] { - grid-column: 2; - } - - [data-slot="toast-v2-actions"] [data-component="button-v2"] { - min-height: 24px; - font-size: 13px; - font-style: normal; - font-weight: 530; - line-height: 20px; - letter-spacing: -0.04px; - font-synthesis: none; + &[data-closed] { + animation: toastV2PopOut 100ms ease-in forwards; } [data-slot="toast-v2-header"] { @@ -193,7 +69,34 @@ justify-content: center; width: 16px; height: 20px; + min-width: 16px; + min-height: 20px; color: var(--v2-icon-icon-base); + + [data-component="icon"] { + color: var(--v2-icon-icon-base); + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + } + + > * { + width: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + svg { + width: 16px; + height: 16px; + display: block; + flex-shrink: 0; + } } [data-slot="toast-v2-content"] { @@ -201,25 +104,112 @@ display: flex; flex-direction: column; gap: 4px; + min-height: 0; min-width: 0; overflow: hidden; } -} -@media (prefers-reduced-motion: reduce) { - .toast-v2, - .toast-v2 > * { - animation: none !important; - transition: none !important; + [data-slot="toast-v2-title"] { + color: var(--v2-text-text-base); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; + font-style: normal; + font-weight: 530; + line-height: 20px; + letter-spacing: -0.04px; + font-synthesis: none; + margin: 0; + } + + [data-slot="toast-v2-description"] { + color: var(--v2-text-text-muted); + text-wrap-style: pretty; + overflow-wrap: anywhere; + word-break: break-word; + font-size: 13px; + font-style: normal; + font-weight: 440; + line-height: 20px; + letter-spacing: -0.04px; + font-synthesis: none; + margin: 0; + } + + [data-slot="toast-v2-actions"] { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding-left: 28px; + } + + [data-slot="toast-v2-actions"] [data-component="button-v2"] { + min-height: 24px; + font-size: 13px; + font-style: normal; + font-weight: 530; + line-height: 20px; + letter-spacing: -0.04px; + font-synthesis: none; + } + + [data-slot="toast-v2-close-button"] { + flex-shrink: 0; + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + padding: 0; + border: 0; + border-radius: 4px; + background: transparent; + color: var(--v2-icon-icon-muted); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + + &:hover { + background: var(--v2-overlay-simple-overlay-hover); + color: var(--v2-icon-icon-base); + } + + &:active { + background: var(--v2-overlay-simple-overlay-pressed); + } + + &:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: 2px; + } + + svg { + width: 16px; + height: 16px; + display: block; + } } } -@media (max-width: 600px) { - .toast-v2-region { - max-width: calc(100vw - 32px); +@keyframes toastV2PopIn { + from { + opacity: 0.8; + transform: translateY(6px); } - - .toast-v2 { - width: 100%; + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes toastV2PopOut { + from { + transform: translateY(0); + opacity: 1; + } + to { + transform: translateY(6px); + opacity: 0.8; } } diff --git a/packages/ui/src/v2/components/toast-v2.stories.tsx b/packages/ui/src/v2/components/toast-v2.stories.tsx index b2af8c038d..f096459739 100644 --- a/packages/ui/src/v2/components/toast-v2.stories.tsx +++ b/packages/ui/src/v2/components/toast-v2.stories.tsx @@ -19,7 +19,7 @@ Use brief titles/descriptions; limit actions to 1-2. - Toasts render in a portal and auto-dismiss unless persistent. ### Accessibility -- The region announces additions through an ARIA live region and expands with Alt+T. +- TODO: confirm aria-live behavior from Kobalte Toast. ### Theming/tokens - Uses \`data-component="toast-v2"\` and slot data attributes. @@ -57,10 +57,10 @@ export const AllExamples = { - - + + ), actions: [ diff --git a/packages/ui/src/v2/components/toast-v2.tsx b/packages/ui/src/v2/components/toast-v2.tsx index efb6ecd40c..d2dc1b3605 100644 --- a/packages/ui/src/v2/components/toast-v2.tsx +++ b/packages/ui/src/v2/components/toast-v2.tsx @@ -1,78 +1,40 @@ -import { Toaster, toast, type ToasterProps } from "solid-sonner" +import { Toast as Kobalte, toaster } from "@kobalte/core/toast" +import type { ToastRootProps, ToastCloseButtonProps, ToastTitleProps, ToastDescriptionProps } from "@kobalte/core/toast" import type { ComponentProps, JSX } from "solid-js" -import { createContext, onCleanup, onMount, splitProps, useContext } from "solid-js" +import { Show, children } from "solid-js" import { Portal } from "solid-js/web" -import "./button-v2.css" +import { ButtonV2 } from "./button-v2" import "./toast-v2.css" -export interface ToastV2RegionProps extends ToasterProps {} +export interface ToastV2RegionProps extends ComponentProps {} function ToastV2Region(props: ToastV2RegionProps) { - const [local, rest] = splitProps(props, ["class", "className", "style", "toastOptions", "swipeDirections"]) - onMount(() => { - const sync = () => { - document.querySelectorAll(".toast-v2-region .toast-v2").forEach((element) => { - const hidden = element.dataset.visible === "false" - element.inert = hidden - element.tabIndex = hidden ? -1 : 0 - }) - } - let connected = false - const connect = () => { - const regions = document.querySelectorAll(".toast-v2-region") - if (!regions.length) return - observer.disconnect() - regions.forEach((region) => { - observer.observe(region, { - subtree: true, - childList: true, - attributes: true, - attributeFilter: ["data-visible"], - }) - }) - connected = true - sync() - } - const observer = new MutationObserver(() => { - if (!connected) connect() - else sync() - }) - observer.observe(document.body, { subtree: true, childList: true }) - queueMicrotask(connect) - onCleanup(() => observer.disconnect()) - }) return ( - + + + ) } -const ToastV2Context = createContext() - -export interface ToastV2RootComponentProps { - toastId: number +export interface ToastV2RootComponentProps extends ToastRootProps { + class?: string + classList?: ComponentProps<"li">["classList"] children?: JSX.Element } function ToastV2Root(props: ToastV2RootComponentProps) { - return {props.children} + return ( + + ) } function ToastV2Icon(props: ComponentProps<"div">) { @@ -83,43 +45,26 @@ function ToastV2Content(props: ComponentProps<"div">) { return
} -function ToastV2Title(props: ComponentProps<"div">) { - return
+function ToastV2Title(props: ToastTitleProps & ComponentProps<"div">) { + return } -function ToastV2Description(props: ComponentProps<"div">) { - return
+function ToastV2Description(props: ToastDescriptionProps & ComponentProps<"div">) { + return } function ToastV2Actions(props: ComponentProps<"div">) { return
} -function ToastV2CloseButton(props: ComponentProps<"button">) { - const toastId = useContext(ToastV2Context) - const [local, rest] = splitProps(props, ["children", "onClick"]) +function ToastV2CloseButton(props: ToastCloseButtonProps & ComponentProps<"button">) { return ( - - ) -} - -function CloseIcon() { - return ( - + + + ) } @@ -133,29 +78,7 @@ export const ToastV2 = Object.assign(ToastV2Root, { CloseButton: ToastV2CloseButton, }) -let toastV2Id = 0 - -export const toasterV2 = { - show(render: (props: { toastId: number }) => JSX.Element, options?: { duration?: number; persistent?: boolean }) { - const toastId = --toastV2Id - toast.custom((id) => render({ toastId: Number(id) }), { - id: toastId, - className: "toast-v2", - duration: options?.persistent ? Number.POSITIVE_INFINITY : options?.duration, - unstyled: true, - }) - return toastId - }, - dismiss(toastId?: number) { - if (toastId === undefined) { - activeToastV2ByKey.clear() - activeToastV2ById.clear() - } else { - releaseToastV2(activeToastV2ById.get(toastId)) - } - return toast.dismiss(toastId) - }, -} +export { toaster as toasterV2 } export interface ToastV2Action { label: string @@ -167,107 +90,53 @@ export interface ToastV2Options { title?: string description?: string icon?: JSX.Element - variant?: "default" | "success" | "error" | "loading" duration?: number persistent?: boolean actions?: ToastV2Action[] } -interface ActiveToastV2 { - id: number - key: string - options: ToastV2Options - actions?: JSX.Element -} - -const activeToastV2ByKey = new Map() -const activeToastV2ById = new Map() - export function showToastV2(options: ToastV2Options | string) { - const opts: ToastV2Options = typeof options === "string" ? { description: options } : options - const key = JSON.stringify({ - title: opts.title, - description: opts.description, - variant: opts.variant, - duration: opts.duration, - persistent: opts.persistent, - actions: opts.actions?.map((action) => [action.label, action.variant]), + const opts = typeof options === "string" ? { description: options } : options + return toaster.show((props) => { + const resolvedIcon = children(() => opts.icon) + return ( + +
+ + {resolvedIcon()} + + + + {opts.title} + + + {opts.description} + + + +
+ + + {opts.actions!.map((action) => ( + { + if (typeof action.onClick === "function") { + action.onClick() + } + toaster.dismiss(props.toastId) + }} + > + {action.label} + + ))} + + +
+ ) }) - const active = activeToastV2ByKey.get(key) - const toasts = toast.getToasts() - - if (active && toasts.at(-1)?.id === active.id) { - active.options = opts - publishToastV2(active) - pulseToastV2(active.id) - return active.id - } - - if (active && toasts.some((item) => item.id === active.id)) toasterV2.dismiss(active.id) - releaseToastV2(active) - - const entry: ActiveToastV2 = { id: --toastV2Id, key, options: opts } - entry.actions = createToastV2Actions(entry) - activeToastV2ByKey.set(key, entry) - activeToastV2ById.set(entry.id, entry) - publishToastV2(entry) - return entry.id -} - -function publishToastV2(entry: ActiveToastV2) { - const release = () => releaseToastV2(entry) - toast(entry.options.title ?? "", { - id: entry.id, - description: entry.options.description, - icon: entry.options.icon, - action: entry.actions, - closeButton: true, - duration: entry.options.persistent ? Number.POSITIVE_INFINITY : entry.options.duration, - className: `toast-v2 toast-v2--${entry.options.variant ?? "default"}`, - testId: `toast-v2-${entry.id}`, - unstyled: true, - onDismiss: release, - onAutoClose: release, - }) -} - -function createToastV2Actions(entry: ActiveToastV2) { - if (!entry.options.actions?.length) return undefined - return ( - - {entry.options.actions.map((action, index) => ( - - ))} - - ) -} - -function pulseToastV2(toastId: number) { - if (typeof document === "undefined" || typeof requestAnimationFrame === "undefined") return - requestAnimationFrame(() => { - const element = document.querySelector(`[data-testid="toast-v2-${toastId}"]`) - if (!element || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return - element.animate([{ scale: 1 }, { scale: 1.025 }, { scale: 1 }], { duration: 160, easing: "ease-out" }) - }) -} - -function releaseToastV2(entry: ActiveToastV2 | undefined) { - if (!entry) return - if (activeToastV2ByKey.get(entry.key) === entry) activeToastV2ByKey.delete(entry.key) - if (activeToastV2ById.get(entry.id) === entry) activeToastV2ById.delete(entry.id) } export interface ToastV2PromiseOptions { diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 1d5af6934c..1e38adede2 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -45,7 +45,7 @@ function providerIconsPlugin() { } async function fetchProviderIcons() { - const url = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai" + const url = process.env.OPENCODE_MODELS_URL || "https://models.dev" const providers = await fetch(`${url}/api.json`) .then((res) => res.json()) .then((json) => Object.keys(json)) diff --git a/packages/web/package.json b/packages/web/package.json index c9637aee6b..6a9f1317dc 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.18.11", + "version": "1.18.3", "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 b0be0b6157..0597f3adf1 100644 --- a/packages/web/src/content/docs/ar/go.mdx +++ b/packages/web/src/content/docs/ar/go.mdx @@ -11,7 +11,7 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال يعمل Go مثل أي مزود آخر في OpenCode. تشترك في OpenCode Go وتحصل على مفتاح API الخاص بك. وهو **اختياري تمامًا**، ولا تحتاج إلى استخدامه لاستخدام OpenCode. -صُمّم أساسًا للمستخدمين الدوليين، ويوفّر وصولًا عالميًا مستقرًا. +صُمّم أساسًا للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة لضمان وصول عالمي مستقر. --- @@ -52,7 +52,6 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** قد تتغير قائمة النماذج مع استمرارنا في اختبار نماذج جديدة وإضافتها. @@ -87,40 +84,34 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Model | الطلبات لكل 5 ساعات | الطلبات في الأسبوع | الطلبات في الشهر | | ----------------- | ------------------- | ------------------ | ---------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | تستند التقديرات إلى أنماط الطلبات المرصودة: - Grok 4.5 — ‏1,100 input، و71,500 cached، و220 output tokens لكل طلب - GLM-5.2/5.1 — ‏700 input، و52,000 cached، و150 output tokens لكل طلب -- GPT 5.6 Luna — ‏1,000 توكن إدخال، و50,000 توكن مخزّن مؤقتًا، و220 توكن إخراج لكل طلب - Kimi K3 — ‏1,050 input، و76,500 cached، و300 output tokens لكل طلب - Kimi K2.7/K2.6 — ‏870 input، و55,000 cached، و200 output tokens لكل طلب - DeepSeek V4 Pro — ‏750 input، و82,000 cached، و290 output tokens لكل طلب - DeepSeek V4 Flash — ‏790 input، و68,000 cached، و280 output tokens لكل طلب - MiniMax M3 — ‏510 input، و56,000 cached، و190 output tokens لكل طلب - MiniMax M2.7 — ‏300 input، و55,000 cached، و125 output tokens لكل طلب -- Qwen3.8 Max — ‏420 input، و66,000 cached، و200 output tokens لكل طلب - Qwen3.7 Max — ‏420 input، و66,000 cached، و200 output tokens لكل طلب - Qwen3.7 Plus — ‏500 input، و57,000 cached، و190 output tokens لكل طلب - Qwen3.6 Plus — ‏500 input، و57,000 cached، و190 output tokens لكل طلب -- Hy3 — ‏830 input، و71,500 cached، و295 output tokens لكل طلب - MiMo-V2.5 — ‏830 input، و71,500 cached، و295 output tokens لكل طلب - MiMo-V2.5-Pro — ‏790 input، و86,000 cached، و305 output tokens لكل طلب @@ -129,8 +120,6 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | النموذج | الإدخال | الإخراج | القراءة المخزنة | الكتابة المخزنة | الاستخدام | | ---------------------------- | ------- | ------- | --------------- | --------------- | --------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | يمكنك تتبّع استخدامك الحالي في **console**. @@ -186,7 +173,6 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | Model | Model ID | Endpoint | AI SDK Package | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ OpenCode Go هو اشتراك منخفض التكلفة — **$5 للشهر ال | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | يستخدم [model id](/docs/config/#models) في إعدادات OpenCode لديك التنسيق `opencode-go/`. على سبيل المثال، بالنسبة إلى Kimi K3، ستستخدم `opencode-go/kimi-k3` في إعداداتك. @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## الخصوصية -| النموذج | تدريب النموذج | الاحتفاظ بالبيانات | -| ----------------- | ------------- | ------------------ | -| Grok 4.5 | غير مستخدَمة | 30 يومًا | -| GPT 5.6 Luna | غير مستخدَمة | 30 يومًا | -| GLM-5.2 | غير مستخدَمة | 0 أيام | -| GLM-5.1 | غير مستخدَمة | 0 أيام | -| Kimi K3 | غير مستخدَمة | 0 أيام | -| Kimi K2.7 Code | غير مستخدَمة | 0 أيام | -| Kimi K2.6 | غير مستخدَمة | 0 أيام | -| MiMo-V2.5-Pro | غير مستخدَمة | 0 أيام | -| MiMo-V2.5 | غير مستخدَمة | 0 أيام | -| Qwen3.8 Max | غير مستخدَمة | 0 أيام | -| Qwen3.7 Max | غير مستخدَمة | 0 أيام | -| Qwen3.7 Plus | غير مستخدَمة | 0 أيام | -| Qwen3.6 Plus | غير مستخدَمة | 0 أيام | -| MiniMax M3 | غير مستخدَمة | 0 أيام | -| MiniMax M2.7 | غير مستخدَمة | 0 أيام | -| DeepSeek V4 Pro | غير مستخدَمة | 0 أيام | -| DeepSeek V4 Flash | غير مستخدَمة | 0 أيام | -| Hy3 | غير مستخدَمة | 0 أيام | - -- **Grok 4.5:** تعطّل ZDR ميزات API مهمة تعتمد على البيانات المخزنة، بما في ذلك Responses API ذات الحالة، وFiles and Collections، وBatch API. [اعرف المزيد](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** تُنشأ سجلات مراقبة إساءة الاستخدام لكل استخدام لميزات API، ويُحتفظ بها لمدة تصل إلى 30 يومًا. [اعرف المزيد](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** تُجدَّد اتفاقية ZDR شهريًا. الاتفاقية الحالية سارية حتى 31 أغسطس 2026. +صُمّمت هذه الخطة أساسًا للمستخدمين الدوليين، مع استضافة النماذج في الولايات المتحدة والاتحاد الأوروبي وسنغافورة لضمان وصول عالمي مستقر. ويتّبع مزودونا سياسة عدم الاحتفاظ بالبيانات، ولا يستخدمون بياناتك لتدريب النماذج. --- diff --git a/packages/web/src/content/docs/ar/zen.mdx b/packages/web/src/content/docs/ar/zen.mdx index 0cc94de6ef..c90ee2d220 100644 --- a/packages/web/src/content/docs/ar/zen.mdx +++ b/packages/web/src/content/docs/ar/zen.mdx @@ -53,68 +53,62 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن يمكنك أيضا الوصول إلى نماذجنا عبر نقاط نهاية API التالية. -| النموذج | معرّف النموذج | نقطة النهاية | حزمة AI SDK | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| النموذج | معرّف النموذج | نقطة النهاية | حزمة AI SDK | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | يستخدم [معرّف النموذج](/docs/config/#models) في إعدادات OpenCode الصيغة `opencode/`. على سبيل المثال، بالنسبة إلى GPT 5.5، ستستخدم `opencode/gpt-5.5` في إعداداتك. @@ -139,8 +133,6 @@ https://opencode.ai/zen/v1/models | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -150,7 +142,6 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -160,7 +151,6 @@ https://opencode.ai/zen/v1/models | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -170,9 +160,7 @@ https://opencode.ai/zen/v1/models | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -181,10 +169,10 @@ https://opencode.ai/zen/v1/models | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -205,7 +193,7 @@ https://opencode.ai/zen/v1/models | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -قد تلاحظ [نماذج منخفضة التكلفة](/docs/config/#models)، مثل Haiku أو Nano أو Flash، في سجل الاستخدام. يستخدم OpenCode هذه النماذج لإنشاء عناوين الجلسات. +قد تلاحظ _Claude Haiku 3.5_ في سجل الاستخدام. هذا [نموذج منخفض التكلفة](/docs/config/#models) يُستخدم لتوليد عناوين جلساتك. :::note يتم تمرير رسوم بطاقات الائتمان بالتكلفة الفعلية (4.4% + $0.30 لكل معاملة)؛ ولا نفرض أي رسوم إضافية فوق ذلك. @@ -215,8 +203,6 @@ https://opencode.ai/zen/v1/models - DeepSeek V4 Flash Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج. - MiMo-V2.5 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج. -- Laguna S 2.1 Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج. -- Ling-3.0-flash Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج. - North Mini Code Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج. - Nemotron 3 Ultra Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج. - Big Pickle نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج. @@ -273,8 +259,6 @@ https://opencode.ai/zen/v1/models - Big Pickle: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج. - DeepSeek V4 Flash Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج. - MiMo-V2.5 Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج. -- Laguna S 2.1 Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج. -- Ling-3.0-flash Free: خلال فترته المجانية، قد تُستخدم البيانات المجمعة لتحسين النموذج. - North Mini Code Free: خلال فترته المجانية، قد يُحتفَظ بالبيانات المُجمَّعة وتُستخدم لتحسين النموذج. لا تُرسل بيانات شخصية أو سرية. راجع [شروط الاستخدام](https://cohere.com/terms-of-use) و[سياسة الخصوصية](https://cohere.com/privacy). - Nemotron 3 Ultra Free (نقاط نهاية NVIDIA المجانية): للاستخدام التجريبي فقط — لا ترسل بيانات شخصية أو سرية. يُسجَّل استخدامك لأغراض أمنية ولتحسين منتجات وخدمات NVIDIA. بيانات الجلسة المُسجَّلة لأغراض التحسين غير مرتبطة بهويتك أو بأي مُعرِّف دائم. لمزيد من المعلومات حول ممارسات معالجة البيانات لدينا، راجع [سياسة الخصوصية](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). بتفاعلك مع نقطة النهاية هذه، فإنك توافق على جمعنا لهذه المعلومات وتسجيلها واستخدامها وعلى [شروط خدمة النسخة التجريبية من واجهة NVIDIA API](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: يتم الاحتفاظ بالطلبات لمدة 30 يوما وفقا لـ [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/bs/go.mdx b/packages/web/src/content/docs/bs/go.mdx index 634b3a8685..26b5575a01 100644 --- a/packages/web/src/content/docs/bs/go.mdx +++ b/packages/web/src/content/docs/bs/go.mdx @@ -13,7 +13,7 @@ Go radi kao bilo koji drugi provajder u OpenCode-u. Pretplatite se na OpenCode G dobijete svoj API ključ. On je **potpuno opcionalan** i ne morate ga koristiti da biste koristili OpenCode. -Prvenstveno je namijenjen međunarodnim korisnicima i pruža stabilan globalni pristup. +Dizajniran je prvenstveno za međunarodne korisnike, sa modelima hostovanim u SAD-u, EU i Singapuru za stabilan globalni pristup. --- @@ -62,7 +62,6 @@ Trenutna lista modela uključuje: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -70,13 +69,11 @@ Trenutna lista modela uključuje: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** Lista modela se može mijenjati dok testiramo i dodajemo nove. @@ -97,40 +94,34 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori | Model | zahtjeva na 5 sati | zahtjeva sedmično | zahtjeva mjesečno | | ----------------- | ------------------ | ----------------- | ----------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Procjene se zasnivaju na zapaženim obrascima zahtjeva: - Grok 4.5 — 1,100 ulaznih, 71,500 keširanih, 220 izlaznih tokena po zahtjevu - GLM-5.2/5.1 — 700 ulaznih (input), 52,000 keširanih, 150 izlaznih (output) tokena po zahtjevu -- GPT 5.6 Luna — 1,000 ulaznih, 50,000 keširanih, 220 izlaznih tokena po zahtjevu - Kimi K3 — 1,050 ulaznih, 76,500 keširanih, 300 izlaznih tokena po zahtjevu - Kimi K2.7/K2.6 — 870 ulaznih, 55,000 keširanih, 200 izlaznih tokena po zahtjevu - DeepSeek V4 Pro — 750 ulaznih, 82,000 keširanih, 290 izlaznih tokena po zahtjevu - DeepSeek V4 Flash — 790 ulaznih, 68,000 keširanih, 280 izlaznih tokena po zahtjevu - MiniMax M3 — 510 ulaznih, 56,000 keširanih, 190 izlaznih tokena po zahtjevu - MiniMax M2.7 — 300 ulaznih, 55,000 keširanih, 125 izlaznih tokena po zahtjevu -- Qwen3.8 Max — 420 ulaznih, 66,000 keširanih, 200 izlaznih tokena po zahtjevu - Qwen3.7 Max — 420 ulaznih, 66,000 keširanih, 200 izlaznih tokena po zahtjevu - Qwen3.7 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu - Qwen3.6 Plus — 500 ulaznih, 57,000 keširanih, 190 izlaznih tokena po zahtjevu -- Hy3 — 830 ulaznih, 71,500 keširanih, 295 izlaznih tokena po zahtjevu - MiMo-V2.5 — 830 ulaznih, 71,500 keširanih, 295 izlaznih tokena po zahtjevu - MiMo-V2.5-Pro — 790 ulaznih, 86,000 keširanih, 305 izlaznih tokena po zahtjevu @@ -139,8 +130,6 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj | Model | Input | Output | Cached Read | Cached Write | Potrošnja | | ---------------------------- | ------ | ------ | ----------- | ------------ | --------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -151,7 +140,6 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -159,7 +147,6 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Svoju trenutnu potrošnju možete pratiti u **konzoli**. @@ -198,7 +185,6 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa. | Model | Model ID | Endpoint | AI SDK Paket | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -211,11 +197,9 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa. | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [Model id](/docs/config/#models) u vašoj OpenCode konfiguraciji koristi format `opencode-go/`. Na primjer, za Kimi K3, koristili biste @@ -235,30 +219,7 @@ https://opencode.ai/zen/go/v1/models ## Privatnost -| Model | Treniranje modela | Zadržavanje podataka | -| ----------------- | ----------------- | -------------------- | -| Grok 4.5 | Ne koristi se | 30 dana | -| GPT 5.6 Luna | Ne koristi se | 30 dana | -| GLM-5.2 | Ne koristi se | 0 dana | -| GLM-5.1 | Ne koristi se | 0 dana | -| Kimi K3 | Ne koristi se | 0 dana | -| Kimi K2.7 Code | Ne koristi se | 0 dana | -| Kimi K2.6 | Ne koristi se | 0 dana | -| MiMo-V2.5-Pro | Ne koristi se | 0 dana | -| MiMo-V2.5 | Ne koristi se | 0 dana | -| Qwen3.8 Max | Ne koristi se | 0 dana | -| Qwen3.7 Max | Ne koristi se | 0 dana | -| Qwen3.7 Plus | Ne koristi se | 0 dana | -| Qwen3.6 Plus | Ne koristi se | 0 dana | -| MiniMax M3 | Ne koristi se | 0 dana | -| MiniMax M2.7 | Ne koristi se | 0 dana | -| DeepSeek V4 Pro | Ne koristi se | 0 dana | -| DeepSeek V4 Flash | Ne koristi se | 0 dana | -| Hy3 | Ne koristi se | 0 dana | - -- **Grok 4.5:** ZDR onemogućava važne API funkcije koje zavise od pohranjenih podataka, uključujući Responses API s očuvanjem stanja, Files and Collections i Batch API. [Saznajte više](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Zapisi o nadzoru zloupotrebe generišu se za svako korištenje API funkcija i čuvaju do 30 dana. [Saznajte više](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** ZDR sporazum obnavlja se mjesečno. Trenutni sporazum važi do 31. augusta 2026. +Plan je prvenstveno namijenjen međunarodnim korisnicima, a modeli su smješteni u US, EU i Singaporeu radi stabilnog globalnog pristupa. Naši pružaoci usluga primjenjuju politiku nultog zadržavanja podataka i ne koriste vaše podatke za treniranje modela. --- diff --git a/packages/web/src/content/docs/bs/zen.mdx b/packages/web/src/content/docs/bs/zen.mdx index 17cf73581b..2d5205613b 100644 --- a/packages/web/src/content/docs/bs/zen.mdx +++ b/packages/web/src/content/docs/bs/zen.mdx @@ -58,68 +58,62 @@ Naplata se vrši po zahtjevu i možete dodavati kredit na svoj račun. Našim modelima možete pristupiti i preko sljedećih API endpointa. -| Model | Model ID | Endpoint | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | Model ID | Endpoint | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5 u konfiguraciji biste @@ -146,8 +140,6 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,7 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**. | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -U historiji korištenja možete primijetiti [jeftinije modele](/docs/config/#models), kao što su Haiku, Nano ili Flash. OpenCode koristi ove modele za generisanje naslova sesija. +Možda ćete primijetiti _Claude Haiku 3.5_ u historiji korištenja. To je [low cost model](/docs/config/#models) koji se koristi za generisanje naslova vaših sesija. :::note Naknade za kreditne kartice prosljeđujemo po stvarnom trošku (4.4% + $0.30 po transakciji); ne naplaćujemo ništa preko toga. @@ -222,8 +210,6 @@ Besplatni modeli: - DeepSeek V4 Flash Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model. - MiMo-V2.5 Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model. -- Laguna S 2.1 Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model. -- Ling-3.0-flash Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model. - North Mini Code Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model. - Nemotron 3 Ultra Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model. - Big Pickle je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model. @@ -285,8 +271,6 @@ i ne koriste vaše podatke za treniranje modela, uz sljedeće izuzetke: - Big Pickle: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela. - DeepSeek V4 Flash Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela. - MiMo-V2.5 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela. -- Laguna S 2.1 Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela. -- Ling-3.0-flash Free: Tokom besplatnog perioda, prikupljeni podaci mogu se koristiti za poboljšanje modela. - North Mini Code Free: Tokom besplatnog perioda, prikupljeni podaci mogu biti zadržani i korišteni za poboljšanje modela. Nemojte slati lične ili povjerljive podatke. Pogledajte naše [Uslove korištenja](https://cohere.com/terms-of-use) i [Politiku privatnosti](https://cohere.com/privacy). - Nemotron 3 Ultra Free (besplatni NVIDIA endpointi): Samo za probnu upotrebu — nemojte slati lične ili povjerljive podatke. Vaše korištenje se bilježi radi sigurnosti i poboljšanja NVIDIA proizvoda i usluga. Zabilježeni podaci sesije koji se koriste u svrhu poboljšanja nisu povezani s vašim identitetom niti bilo kojim trajnim identifikatorom. Za više informacija o našim praksama obrade podataka pogledajte našu [Politiku privatnosti](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Interakcijom s ovim endpointom pristajete na naše prikupljanje, bilježenje i korištenje takvih informacija te na [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/da/go.mdx b/packages/web/src/content/docs/da/go.mdx index 5aabcc7c43..280891deee 100644 --- a/packages/web/src/content/docs/da/go.mdx +++ b/packages/web/src/content/docs/da/go.mdx @@ -13,7 +13,7 @@ Go fungerer som enhver anden udbyder i OpenCode. Du abonnerer på OpenCode Go og får din API-nøgle. Det er **helt valgfrit**, og du behøver ikke at bruge det for at bruge OpenCode. -Det er primært udviklet til internationale brugere og giver stabil global adgang. +Det er primært designet til internationale brugere, med modeller hostet i USA, EU og Singapore for at sikre stabil global adgang. --- @@ -62,7 +62,6 @@ Den nuværende liste over modeller inkluderer: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -70,13 +69,11 @@ Den nuværende liste over modeller inkluderer: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** Listen over modeller kan ændre sig, efterhånden som vi tester og tilføjer nye. @@ -97,40 +94,34 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo | Model | anmodninger pr. 5 timer | anmodninger pr. uge | anmodninger pr. måned | | ----------------- | ----------------------- | ------------------- | --------------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Estimaterne er baseret på observerede anmodningsmønstre: - Grok 4.5 — 1.100 input, 71.500 cachelagrede, 220 output-tokens pr. anmodning - GLM-5.2/5.1 — 700 input, 52.000 cachelagrede, 150 output-tokens pr. anmodning -- GPT 5.6 Luna — 1.000 input, 50.000 cachelagrede, 220 output-tokens pr. anmodning - Kimi K3 — 1.050 input, 76.500 cachelagrede, 300 output-tokens pr. anmodning - Kimi K2.7/K2.6 — 870 input, 55.000 cachelagrede, 200 output-tokens pr. anmodning - DeepSeek V4 Pro — 750 input, 82.000 cachelagrede, 290 output-tokens pr. anmodning - DeepSeek V4 Flash — 790 input, 68.000 cachelagrede, 280 output-tokens pr. anmodning - MiniMax M3 — 510 input, 56.000 cachelagrede, 190 output-tokens pr. anmodning - MiniMax M2.7 — 300 input, 55.000 cachelagrede, 125 output-tokens pr. anmodning -- Qwen3.8 Max — 420 input, 66.000 cachelagrede, 200 output-tokens pr. anmodning - Qwen3.7 Max — 420 input, 66.000 cachelagrede, 200 output-tokens pr. anmodning - Qwen3.7 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning - Qwen3.6 Plus — 500 input, 57.000 cachelagrede, 190 output-tokens pr. anmodning -- Hy3 — 830 input, 71.500 cachelagrede, 295 output-tokens pr. anmodning - MiMo-V2.5 — 830 input, 71.500 cachelagrede, 295 output-tokens pr. anmodning - MiMo-V2.5-Pro — 790 input, 86.000 cachelagrede, 305 output-tokens pr. anmodning @@ -139,8 +130,6 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig | Model | Input | Output | Cached Read | Cached Write | Forbrug | | ---------------------------- | ------ | ------ | ----------- | ------------ | ------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -151,7 +140,6 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -159,7 +147,6 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kan spore dit nuværende forbrug i **konsollen**. @@ -198,7 +185,6 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints. | Model | Model ID | Endpoint | AI SDK Package | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -211,11 +197,9 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints. | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Dit [model id](/docs/config/#models) i din OpenCode config bruger formatet `opencode-go/`. For eksempel for Kimi K3, vil du @@ -235,30 +219,7 @@ https://opencode.ai/zen/go/v1/models ## Privatliv -| Model | Modeltræning | Dataopbevaring | -| ----------------- | ------------ | -------------- | -| Grok 4.5 | Ikke brugt | 30 dage | -| GPT 5.6 Luna | Ikke brugt | 30 dage | -| GLM-5.2 | Ikke brugt | 0 dage | -| GLM-5.1 | Ikke brugt | 0 dage | -| Kimi K3 | Ikke brugt | 0 dage | -| Kimi K2.7 Code | Ikke brugt | 0 dage | -| Kimi K2.6 | Ikke brugt | 0 dage | -| MiMo-V2.5-Pro | Ikke brugt | 0 dage | -| MiMo-V2.5 | Ikke brugt | 0 dage | -| Qwen3.8 Max | Ikke brugt | 0 dage | -| Qwen3.7 Max | Ikke brugt | 0 dage | -| Qwen3.7 Plus | Ikke brugt | 0 dage | -| Qwen3.6 Plus | Ikke brugt | 0 dage | -| MiniMax M3 | Ikke brugt | 0 dage | -| MiniMax M2.7 | Ikke brugt | 0 dage | -| DeepSeek V4 Pro | Ikke brugt | 0 dage | -| DeepSeek V4 Flash | Ikke brugt | 0 dage | -| Hy3 | Ikke brugt | 0 dage | - -- **Grok 4.5:** ZDR deaktiverer vigtige API-funktioner, der afhænger af lagrede data, herunder den tilstandsbevarende Responses API, Files and Collections og Batch API. [Læs mere](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Logfiler til overvågning af misbrug genereres ved al brug af API-funktioner og opbevares i op til 30 dage. [Læs mere](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** ZDR-aftalen fornyes månedligt. Den nuværende aftale er gyldig til og med 31. august 2026. +Planen er primært designet til internationale brugere med modeller hostet i US, EU og Singapore for stabil global adgang. Vores udbydere følger en zero-retention-policy og bruger ikke dine data til modeltræning. --- diff --git a/packages/web/src/content/docs/da/zen.mdx b/packages/web/src/content/docs/da/zen.mdx index e200e14e60..7acbd3f95a 100644 --- a/packages/web/src/content/docs/da/zen.mdx +++ b/packages/web/src/content/docs/da/zen.mdx @@ -58,68 +58,62 @@ Du bliver opkrævet pr. anmodning, og du kan tilføje kredit til din konto. Du kan også få adgang til vores modeller gennem følgende API-endpoints. -| Model | Model ID | Endpoint | AI SDK-pakke | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | Model ID | Endpoint | AI SDK-pakke | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5 @@ -146,8 +140,6 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,7 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**. | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Du vil måske bemærke [lavprismodeller](/docs/config/#models), såsom Haiku, Nano eller Flash, i din brugshistorik. OpenCode bruger disse modeller til at generere sessionstitler. +Du vil måske bemærke _Claude Haiku 3.5_ i din brugshistorik. Det er en [lavprismodel](/docs/config/#models), som bruges til at generere titlerne på dine sessioner. :::note Kreditkortgebyrer videregives til kostpris (4.4% + $0.30 pr. transaktion); vi opkræver ikke noget ud over det. @@ -222,8 +210,6 @@ De gratis modeller: - DeepSeek V4 Flash Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen. - MiMo-V2.5 Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen. -- Laguna S 2.1 Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen. -- Ling-3.0-flash Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen. - North Mini Code Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen. - Nemotron 3 Ultra Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen. - Big Pickle er en stealth-model, som er gratis på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen. @@ -283,8 +269,6 @@ Alle vores modeller hostes i US. Vores udbydere følger en nul-opbevaringspoliti - Big Pickle: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen. - DeepSeek V4 Flash Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen. - MiMo-V2.5 Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen. -- Laguna S 2.1 Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen. -- Ling-3.0-flash Free: I den gratis periode kan indsamlede data blive brugt til at forbedre modellen. - North Mini Code Free: I gratisperioden kan indsamlede data blive opbevaret og brugt til at forbedre modellen. Indsend ikke personlige eller fortrolige oplysninger. Se vores [Brugsvilkår](https://cohere.com/terms-of-use) og [Privatlivspolitik](https://cohere.com/privacy). - Nemotron 3 Ultra Free (gratis NVIDIA-endpoints): Kun til prøvebrug — indsend ikke personlige eller fortrolige data. Din brug logges af sikkerhedshensyn og for at forbedre NVIDIAs produkter og tjenester. De loggede sessionsdata, der bruges til forbedringsformål, er ikke knyttet til din identitet eller nogen vedvarende identifikator. For mere information om vores databehandlingspraksis, se vores [privatlivspolitik](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Ved at interagere med dette endpoint giver du samtykke til vores indsamling, registrering og brug af sådanne oplysninger samt [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: Anmodninger opbevares i 30 dage i overensstemmelse med [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/de/go.mdx b/packages/web/src/content/docs/de/go.mdx index 73a11d454f..2bb3f0b742 100644 --- a/packages/web/src/content/docs/de/go.mdx +++ b/packages/web/src/content/docs/de/go.mdx @@ -13,7 +13,7 @@ Go funktioniert wie jeder andere Provider in OpenCode. Du abonnierst OpenCode Go erhältst deinen API-Key. Es ist **völlig optional** und du musst es nicht nutzen, um OpenCode zu verwenden. -Es wurde in erster Linie für internationale Nutzer entwickelt und bietet einen stabilen weltweiten Zugriff. +Es wurde primär für internationale Nutzer entwickelt, wobei die Modelle für einen stabilen weltweiten Zugriff in den USA, der EU und Singapur gehostet werden. --- @@ -54,7 +54,6 @@ Die aktuelle Liste der Modelle umfasst: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -62,13 +61,11 @@ Die aktuelle Liste der Modelle umfasst: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** Die Liste der Modelle kann sich ändern, während wir neue testen und hinzufügen. @@ -89,40 +86,34 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty | Model | Anfragen pro 5 Stunden | Anfragen pro Woche | Anfragen pro Monat | | ----------------- | ---------------------- | ------------------ | ------------------ | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Die Schätzungen basieren auf beobachteten Anfragemustern: - Grok 4.5 — 1.100 Input-, 71.500 Cached-, 220 Output-Tokens pro Anfrage - GLM-5.2/5.1 — 700 Input-, 52.000 Cached-, 150 Output-Tokens pro Anfrage -- GPT 5.6 Luna — 1.000 Input-, 50.000 Cached-, 220 Output-Tokens pro Anfrage - Kimi K3 — 1.050 Input-, 76.500 Cached-, 300 Output-Tokens pro Anfrage - Kimi K2.7/K2.6 — 870 Input-, 55.000 Cached-, 200 Output-Tokens pro Anfrage - DeepSeek V4 Pro — 750 Input-, 82.000 Cached-, 290 Output-Tokens pro Anfrage - DeepSeek V4 Flash — 790 Input-, 68.000 Cached-, 280 Output-Tokens pro Anfrage - MiniMax M3 — 510 Input-, 56.000 Cached-, 190 Output-Tokens pro Anfrage - MiniMax M2.7 — 300 Input-, 55.000 Cached-, 125 Output-Tokens pro Anfrage -- Qwen3.8 Max — 420 Input-, 66.000 Cached-, 200 Output-Tokens pro Anfrage - Qwen3.7 Max — 420 Input-, 66.000 Cached-, 200 Output-Tokens pro Anfrage - Qwen3.7 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage - Qwen3.6 Plus — 500 Input-, 57.000 Cached-, 190 Output-Tokens pro Anfrage -- Hy3 — 830 Input-, 71.500 Cached-, 295 Output-Tokens pro Anfrage - MiMo-V2.5 — 830 Input-, 71.500 Cached-, 295 Output-Tokens pro Anfrage - MiMo-V2.5-Pro — 790 Input-, 86.000 Cached-, 305 Output-Tokens pro Anfrage @@ -131,8 +122,6 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und | Model | Input | Output | Cached Read | Cached Write | Nutzung | | ---------------------------- | ------ | ------ | ----------- | ------------ | ------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -143,7 +132,6 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -151,7 +139,6 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kannst deine aktuelle Nutzung in der **Console** verfolgen. @@ -188,7 +175,6 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen. | Modell | Modell-ID | Endpunkt | AI SDK Package | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -201,11 +187,9 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen. | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Die [Modell-ID](/docs/config/#models) in deiner OpenCode Config verwendet das Format `opencode-go/`. Für Kimi K3 würdest du beispielsweise `opencode-go/kimi-k3` in deiner Config verwenden. @@ -223,30 +207,7 @@ https://opencode.ai/zen/go/v1/models ## Datenschutz -| Modell | Modelltraining | Datenaufbewahrung | -| ----------------- | --------------- | ----------------- | -| Grok 4.5 | Nicht verwendet | 30 Tage | -| GPT 5.6 Luna | Nicht verwendet | 30 Tage | -| GLM-5.2 | Nicht verwendet | 0 Tage | -| GLM-5.1 | Nicht verwendet | 0 Tage | -| Kimi K3 | Nicht verwendet | 0 Tage | -| Kimi K2.7 Code | Nicht verwendet | 0 Tage | -| Kimi K2.6 | Nicht verwendet | 0 Tage | -| MiMo-V2.5-Pro | Nicht verwendet | 0 Tage | -| MiMo-V2.5 | Nicht verwendet | 0 Tage | -| Qwen3.8 Max | Nicht verwendet | 0 Tage | -| Qwen3.7 Max | Nicht verwendet | 0 Tage | -| Qwen3.7 Plus | Nicht verwendet | 0 Tage | -| Qwen3.6 Plus | Nicht verwendet | 0 Tage | -| MiniMax M3 | Nicht verwendet | 0 Tage | -| MiniMax M2.7 | Nicht verwendet | 0 Tage | -| DeepSeek V4 Pro | Nicht verwendet | 0 Tage | -| DeepSeek V4 Flash | Nicht verwendet | 0 Tage | -| Hy3 | Nicht verwendet | 0 Tage | - -- **Grok 4.5:** ZDR deaktiviert wichtige API-Funktionen, die von gespeicherten Daten abhängen, einschließlich der zustandsbehafteten Responses API, Files and Collections und der Batch API. [Mehr erfahren](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Für die Nutzung aller API-Funktionen werden Protokolle zur Missbrauchsüberwachung erstellt und bis zu 30 Tage lang aufbewahrt. [Mehr erfahren](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** Die ZDR-Vereinbarung wird monatlich erneuert. Die aktuelle Vereinbarung gilt bis einschließlich 31. August 2026. +Der Plan ist in erster Linie für internationale Nutzer konzipiert, mit in US, EU und Singapore gehosteten Modellen für einen stabilen weltweiten Zugriff. Unsere Anbieter befolgen eine Zero-Retention-Richtlinie und verwenden Ihre Daten nicht für das Modelltraining. --- diff --git a/packages/web/src/content/docs/de/zen.mdx b/packages/web/src/content/docs/de/zen.mdx index 1fa6cbc7eb..0139a4ec22 100644 --- a/packages/web/src/content/docs/de/zen.mdx +++ b/packages/web/src/content/docs/de/zen.mdx @@ -49,68 +49,62 @@ Dir wird pro Anfrage berechnet, und du kannst deinem Konto Guthaben hinzufügen. Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen. -| Model | Model ID | Endpoint | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | Model ID | Endpoint | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5 würdest du zum Beispiel `opencode/gpt-5.5` in deiner Konfiguration verwenden. @@ -135,8 +129,6 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -146,7 +138,6 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -156,7 +147,6 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -166,9 +156,7 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -177,10 +165,10 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -201,7 +189,7 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Möglicherweise siehst du [kostengünstige Modelle](/docs/config/#models) wie Haiku, Nano oder Flash in deinem Nutzungsverlauf. OpenCode verwendet diese Modelle, um Session-Titel zu generieren. +Möglicherweise siehst du _Claude Haiku 3.5_ in deinem Nutzungsverlauf. Das ist ein [kostengünstiges Modell](/docs/config/#models), das verwendet wird, um die Titel deiner Sessions zu generieren. :::note Kreditkartengebühren werden zum Selbstkostenpreis weitergegeben (4.4% + $0.30 pro Transaktion); wir berechnen darüber hinaus nichts. @@ -211,8 +199,6 @@ Die kostenlosen Modelle: - DeepSeek V4 Flash Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern. - MiMo-V2.5 Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern. -- Laguna S 2.1 Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern. -- Ling-3.0-flash Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern. - North Mini Code Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern. - Nemotron 3 Ultra Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern. - Big Pickle ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern. @@ -269,8 +255,6 @@ Alle unsere Modelle werden in den USA gehostet. Unsere Provider folgen einer Zer - Big Pickle: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden. - DeepSeek V4 Flash Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden. - MiMo-V2.5 Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden. -- Laguna S 2.1 Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden. -- Ling-3.0-flash Free: Während des kostenlosen Zeitraums können gesammelte Daten zur Verbesserung des Modells verwendet werden. - North Mini Code Free: Während des kostenlosen Zeitraums können erhobene Daten gespeichert und zur Verbesserung des Modells verwendet werden. Übermitteln Sie keine personenbezogenen oder vertraulichen Daten. Weitere Informationen finden Sie in unseren [Nutzungsbedingungen](https://cohere.com/terms-of-use) und unserer [Datenschutzerklärung](https://cohere.com/privacy). - Nemotron 3 Ultra Free (kostenlose NVIDIA-Endpunkte): Nur für Testzwecke — übermitteln Sie keine personenbezogenen oder vertraulichen Daten. Ihre Nutzung wird zu Sicherheitszwecken und zur Verbesserung der Produkte und Dienste von NVIDIA protokolliert. Die zu Verbesserungszwecken protokollierten Sitzungsdaten sind nicht mit Ihrer Identität oder einem dauerhaften Identifikator verknüpft. Weitere Informationen zu unseren Datenverarbeitungspraktiken finden Sie in unserer [Datenschutzrichtlinie](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Durch die Interaktion mit diesem Endpunkt stimmen Sie unserer Erhebung, Aufzeichnung und Nutzung solcher Informationen sowie den [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) zu. - OpenAI APIs: Anfragen werden in Übereinstimmung mit [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 30 Tage lang gespeichert. diff --git a/packages/web/src/content/docs/ecosystem.mdx b/packages/web/src/content/docs/ecosystem.mdx index ce4f3100af..2fc3b65486 100644 --- a/packages/web/src/content/docs/ecosystem.mdx +++ b/packages/web/src/content/docs/ecosystem.mdx @@ -54,7 +54,6 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw | [opencode-firecrawl](https://github.com/firecrawl/opencode-firecrawl) | Web scraping, crawling, and search via the Firecrawl CLI | | [opencode-jfrog-plugin](https://github.com/jfrog/opencode-jfrog-plugin) | JFrog Plugin for seamless integration of Opencode users to JFrog platform | | [opencode-goal-plugin](https://github.com/willytop8/OpenCode-goal-plugin) | Session-scoped `/goal` workflow that keeps objectives in context and auto-continues until complete | -| [opencode-tavily](https://github.com/tavily-ai/opencode-tavily) | Web search, extraction, crawling, and deep research via the Tavily CLI | --- diff --git a/packages/web/src/content/docs/es/go.mdx b/packages/web/src/content/docs/es/go.mdx index 7182d716fc..4aa1ca46e3 100644 --- a/packages/web/src/content/docs/es/go.mdx +++ b/packages/web/src/content/docs/es/go.mdx @@ -13,7 +13,7 @@ Go funciona como cualquier otro proveedor en OpenCode. Te suscribes a OpenCode G obtienes tu API key. Es **completamente opcional** y no necesitas usarlo para usar OpenCode. -Está diseñado principalmente para usuarios internacionales y ofrece un acceso global estable. +Está diseñado principalmente para usuarios internacionales, con modelos alojados en EE. UU., la UE y Singapur para un acceso global estable. --- @@ -62,7 +62,6 @@ La lista actual de modelos incluye: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -70,13 +69,11 @@ La lista actual de modelos incluye: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** La lista de modelos puede cambiar a medida que probamos y agregamos otros nuevos. @@ -97,40 +94,34 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los | Model | peticiones por 5 horas | peticiones por semana | peticiones por mes | | ----------------- | ---------------------- | --------------------- | ------------------ | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Las estimaciones se basan en los patrones de peticiones observados: - Grok 4.5 — 1,100 tokens de entrada, 71,500 en caché, 220 tokens de salida por petición - GLM-5.2/5.1 — 700 tokens de entrada, 52,000 en caché, 150 tokens de salida por petición -- GPT 5.6 Luna — 1,000 tokens de entrada, 50,000 en caché, 220 tokens de salida por petición - Kimi K3 — 1,050 tokens de entrada, 76,500 en caché, 300 tokens de salida por petición - Kimi K2.7/K2.6 — 870 tokens de entrada, 55,000 en caché, 200 tokens de salida por petición - DeepSeek V4 Pro — 750 tokens de entrada, 82,000 en caché, 290 tokens de salida por petición - DeepSeek V4 Flash — 790 tokens de entrada, 68,000 en caché, 280 tokens de salida por petición - MiniMax M3 — 510 tokens de entrada, 56,000 en caché, 190 tokens de salida por petición - MiniMax M2.7 — 300 tokens de entrada, 55,000 en caché, 125 tokens de salida por petición -- Qwen3.8 Max — 420 tokens de entrada, 66,000 en caché, 200 tokens de salida por petición - Qwen3.7 Max — 420 tokens de entrada, 66,000 en caché, 200 tokens de salida por petición - Qwen3.7 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición - Qwen3.6 Plus — 500 tokens de entrada, 57,000 en caché, 190 tokens de salida por petición -- Hy3 — 830 tokens de entrada, 71,500 en caché, 295 tokens de salida por petición - MiMo-V2.5 — 830 tokens de entrada, 71,500 en caché, 295 tokens de salida por petición - MiMo-V2.5-Pro — 790 tokens de entrada, 86,000 en caché, 305 tokens de salida por petición @@ -139,8 +130,6 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en | Modelo | Entrada | Salida | Lectura en caché | Escritura en caché | Uso | | ---------------------------- | ------- | ------ | ---------------- | ------------------ | --- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -151,7 +140,6 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -159,7 +147,6 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Puedes realizar un seguimiento de tu uso actual en la **consola**. @@ -198,7 +185,6 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint | Modelo | ID del modelo | Endpoint | Paquete de AI SDK | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -211,11 +197,9 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | El [ID del modelo](/docs/config/#models) en tu configuración de OpenCode usa el formato `opencode-go/`. Por ejemplo, para Kimi K3, usarías @@ -235,30 +219,7 @@ https://opencode.ai/zen/go/v1/models ## Privacidad -| Modelo | Entrenamiento del modelo | Retención de datos | -| ----------------- | ------------------------ | ------------------ | -| Grok 4.5 | No utilizado | 30 días | -| GPT 5.6 Luna | No utilizado | 30 días | -| GLM-5.2 | No utilizado | 0 días | -| GLM-5.1 | No utilizado | 0 días | -| Kimi K3 | No utilizado | 0 días | -| Kimi K2.7 Code | No utilizado | 0 días | -| Kimi K2.6 | No utilizado | 0 días | -| MiMo-V2.5-Pro | No utilizado | 0 días | -| MiMo-V2.5 | No utilizado | 0 días | -| Qwen3.8 Max | No utilizado | 0 días | -| Qwen3.7 Max | No utilizado | 0 días | -| Qwen3.7 Plus | No utilizado | 0 días | -| Qwen3.6 Plus | No utilizado | 0 días | -| MiniMax M3 | No utilizado | 0 días | -| MiniMax M2.7 | No utilizado | 0 días | -| DeepSeek V4 Pro | No utilizado | 0 días | -| DeepSeek V4 Flash | No utilizado | 0 días | -| Hy3 | No utilizado | 0 días | - -- **Grok 4.5:** ZDR deshabilita funciones importantes de la API que dependen de datos almacenados, incluidas la Responses API con estado, Files and Collections y la Batch API. [Más información](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Se generan registros de supervisión de abusos para todo el uso de funciones de la API y se conservan durante un máximo de 30 días. [Más información](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** El acuerdo de ZDR se renueva mensualmente. El acuerdo actual es válido hasta el 31 de agosto de 2026. +El plan está diseñado principalmente para usuarios internacionales, con modelos alojados en US, EU y Singapore para ofrecer un acceso global estable. Nuestros proveedores siguen una política de retención cero y no utilizan tus datos para el entrenamiento de modelos. --- diff --git a/packages/web/src/content/docs/es/zen.mdx b/packages/web/src/content/docs/es/zen.mdx index 539d5999f6..1414a9bd78 100644 --- a/packages/web/src/content/docs/es/zen.mdx +++ b/packages/web/src/content/docs/es/zen.mdx @@ -58,68 +58,62 @@ Se te cobra por solicitud y puedes agregar créditos a tu cuenta. También puedes acceder a nuestros modelos a través de los siguientes endpoints de API. -| Modelo | Model ID | Endpoint | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Modelo | Model ID | Endpoint | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5, usarías @@ -146,8 +140,6 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,7 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Puede que notes [modelos de bajo costo](/docs/config/#models), como Haiku, Nano o Flash, en tu historial de uso. OpenCode usa estos modelos para generar títulos de sesiones. +Puede que notes _Claude Haiku 3.5_ en tu historial de uso. Este es un [modelo de bajo costo](/docs/config/#models) que se usa para generar los títulos de tus sesiones. :::note Las comisiones de tarjeta de crédito se trasladan al costo (4.4% + $0.30 por transacción); no cobramos nada más allá de eso. @@ -222,8 +210,6 @@ Los modelos gratuitos: - DeepSeek V4 Flash Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo. - MiMo-V2.5 Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo. -- Laguna S 2.1 Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo. -- Ling-3.0-flash Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo. - North Mini Code Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo. - Nemotron 3 Ultra Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo. - Big Pickle es un modelo stealth que es gratuito en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo. @@ -283,8 +269,6 @@ Todos nuestros modelos están alojados en US. Nuestros proveedores siguen una po - Big Pickle: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo. - DeepSeek V4 Flash Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo. - MiMo-V2.5 Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo. -- Laguna S 2.1 Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo. -- Ling-3.0-flash Free: Durante su período gratuito, los datos recopilados pueden usarse para mejorar el modelo. - North Mini Code Free: Durante el período gratuito, los datos recopilados podrán conservarse y utilizarse para mejorar el modelo. No envíes datos personales ni confidenciales. Consulta nuestros [Términos de uso](https://cohere.com/terms-of-use) y nuestra [Política de privacidad](https://cohere.com/privacy). - Nemotron 3 Ultra Free (endpoints gratuitos de NVIDIA): Solo para uso de prueba — no envíes datos personales ni confidenciales. Tu uso se registra con fines de seguridad y para mejorar los productos y servicios de NVIDIA. Los datos de sesión registrados con fines de mejora no están vinculados a tu identidad ni a ningún identificador persistente. Para obtener más información sobre nuestras prácticas de procesamiento de datos, consulta nuestra [Política de privacidad](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Al interactuar con este endpoint, aceptas que recopilemos, registremos y usemos dicha información, así como los [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: Las solicitudes se conservan durante 30 días de acuerdo con [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/fr/go.mdx b/packages/web/src/content/docs/fr/go.mdx index c858645e44..95849616b0 100644 --- a/packages/web/src/content/docs/fr/go.mdx +++ b/packages/web/src/content/docs/fr/go.mdx @@ -11,7 +11,7 @@ OpenCode Go est un abonnement à bas coût — **5 $ pour votre premier mois**, Go fonctionne comme n'importe quel autre fournisseur dans OpenCode. Vous vous abonnez à OpenCode Go et obtenez votre clé d'API. C'est **totalement facultatif** et vous n'avez pas besoin de l'utiliser pour utiliser OpenCode. -Il est conçu principalement pour les utilisateurs internationaux et offre un accès mondial stable. +Il est conçu principalement pour les utilisateurs internationaux, avec des modèles hébergés aux États-Unis, dans l'UE et à Singapour pour un accès mondial stable. --- @@ -52,7 +52,6 @@ La liste actuelle des modèles comprend : - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ La liste actuelle des modèles comprend : - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** La liste des modèles peut changer au fur et à mesure que nous en testons et en ajoutons de nouveaux. @@ -87,40 +84,34 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d | Model | requêtes par 5 heures | requêtes par semaine | requêtes par mois | | ----------------- | --------------------- | -------------------- | ----------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Les estimations sont basées sur les schémas de requêtes observés : - Grok 4.5 — 1,100 tokens en entrée, 71,500 en cache, 220 tokens en sortie par requête - GLM-5.2/5.1 — 700 tokens en entrée, 52,000 en cache, 150 tokens en sortie par requête -- GPT 5.6 Luna — 1,000 tokens en entrée, 50,000 en cache, 220 tokens en sortie par requête - Kimi K3 — 1,050 tokens en entrée, 76,500 en cache, 300 tokens en sortie par requête - Kimi K2.7/K2.6 — 870 tokens en entrée, 55,000 en cache, 200 tokens en sortie par requête - DeepSeek V4 Pro — 750 tokens en entrée, 82,000 en cache, 290 tokens en sortie par requête - DeepSeek V4 Flash — 790 tokens en entrée, 68,000 en cache, 280 tokens en sortie par requête - MiniMax M3 — 510 tokens en entrée, 56,000 en cache, 190 tokens en sortie par requête - MiniMax M2.7 — 300 tokens en entrée, 55,000 en cache, 125 tokens en sortie par requête -- Qwen3.8 Max — 420 tokens en entrée, 66,000 en cache, 200 tokens en sortie par requête - Qwen3.7 Max — 420 tokens en entrée, 66,000 en cache, 200 tokens en sortie par requête - Qwen3.7 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête - Qwen3.6 Plus — 500 tokens en entrée, 57,000 en cache, 190 tokens en sortie par requête -- Hy3 — 830 tokens en entrée, 71,500 en cache, 295 tokens en sortie par requête - MiMo-V2.5 — 830 tokens en entrée, 71,500 en cache, 295 tokens en sortie par requête - MiMo-V2.5-Pro — 790 tokens en entrée, 86,000 en cache, 305 tokens en sortie par requête @@ -129,8 +120,6 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s | Modèle | Input | Output | Cached Read | Cached Write | Utilisation | | ---------------------------- | ------ | ------ | ----------- | ------------ | ----------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Vous pouvez suivre votre utilisation actuelle dans la **console**. @@ -186,7 +173,6 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d' | Modèle | ID de modèle | Point de terminaison | Package AI SDK | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d' | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | L'[ID de modèle](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode-go/`. Par exemple, pour Kimi K3, vous utiliseriez `opencode-go/kimi-k3` dans votre configuration. @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## Confidentialité -| Modèle | Entraînement des modèles | Conservation des données | -| ----------------- | ------------------------ | ------------------------ | -| Grok 4.5 | Non utilisé | 30 jours | -| GPT 5.6 Luna | Non utilisé | 30 jours | -| GLM-5.2 | Non utilisé | 0 jour | -| GLM-5.1 | Non utilisé | 0 jour | -| Kimi K3 | Non utilisé | 0 jour | -| Kimi K2.7 Code | Non utilisé | 0 jour | -| Kimi K2.6 | Non utilisé | 0 jour | -| MiMo-V2.5-Pro | Non utilisé | 0 jour | -| MiMo-V2.5 | Non utilisé | 0 jour | -| Qwen3.8 Max | Non utilisé | 0 jour | -| Qwen3.7 Max | Non utilisé | 0 jour | -| Qwen3.7 Plus | Non utilisé | 0 jour | -| Qwen3.6 Plus | Non utilisé | 0 jour | -| MiniMax M3 | Non utilisé | 0 jour | -| MiniMax M2.7 | Non utilisé | 0 jour | -| DeepSeek V4 Pro | Non utilisé | 0 jour | -| DeepSeek V4 Flash | Non utilisé | 0 jour | -| Hy3 | Non utilisé | 0 jour | - -- **Grok 4.5:** Le ZDR désactive d’importantes fonctionnalités API qui dépendent des données stockées, notamment Responses API avec état, Files and Collections et Batch API. [En savoir plus](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Des journaux de surveillance des abus sont générés pour toute utilisation des fonctionnalités API et conservés pendant un maximum de 30 jours. [En savoir plus](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** L’accord ZDR est renouvelé chaque mois. L’accord actuel est valable jusqu’au 31 août 2026. +Cette offre est conçue avant tout pour les utilisateurs internationaux, avec des modèles hébergés aux US, dans l’EU et à Singapore afin d’assurer un accès mondial stable. Nos fournisseurs appliquent une politique de rétention zéro et n’utilisent pas vos données pour l’entraînement des modèles. --- diff --git a/packages/web/src/content/docs/fr/zen.mdx b/packages/web/src/content/docs/fr/zen.mdx index 7a5d5bcf7b..251e022670 100644 --- a/packages/web/src/content/docs/fr/zen.mdx +++ b/packages/web/src/content/docs/fr/zen.mdx @@ -49,68 +49,62 @@ La facturation se fait à la requête et vous pouvez ajouter des crédits à vot Vous pouvez également accéder à nos modèles via les points de terminaison API suivants. -| Modèle | ID du modèle | Point de terminaison | Package AI SDK | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Modèle | ID du modèle | Point de terminaison | Package AI SDK | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5, vous utiliseriez `opencode/gpt-5.5` dans votre configuration. @@ -135,8 +129,6 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -146,7 +138,6 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -156,7 +147,6 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -166,9 +156,7 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -177,10 +165,10 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -201,7 +189,7 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Vous remarquerez peut-être des [modèles à faible coût](/docs/config/#models), tels que Haiku, Nano ou Flash, dans votre historique d'utilisation. OpenCode utilise ces modèles pour générer les titres des sessions. +Vous remarquerez peut-être _Claude Haiku 3.5_ dans votre historique d'utilisation. Il s'agit d'un [modèle à faible coût](/docs/config/#models) utilisé pour générer les titres de vos sessions. :::note Les frais de carte de crédit sont répercutés au prix coûtant (4.4% + $0.30 par transaction) ; nous ne facturons rien au-delà. @@ -211,8 +199,6 @@ Les modèles gratuits : - DeepSeek V4 Flash Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle. - MiMo-V2.5 Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle. -- Laguna S 2.1 Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle. -- Ling-3.0-flash Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle. - North Mini Code Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle. - Nemotron 3 Ultra Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle. - Big Pickle est un modèle stealth gratuit sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle. @@ -269,8 +255,6 @@ Tous nos modèles sont hébergés aux US. Nos fournisseurs suivent une politique - Big Pickle : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle. - DeepSeek V4 Flash Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle. - MiMo-V2.5 Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle. -- Laguna S 2.1 Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle. -- Ling-3.0-flash Free : Pendant sa période gratuite, les données collectées peuvent être utilisées pour améliorer le modèle. - North Mini Code Free : Pendant la période de gratuité, les données collectées peuvent être conservées et utilisées pour améliorer le modèle. Ne transmettez aucune donnée personnelle ou confidentielle. Consultez nos [Conditions d’utilisation](https://cohere.com/terms-of-use) et notre [Politique de confidentialité](https://cohere.com/privacy). - Nemotron 3 Ultra Free (endpoints NVIDIA gratuits) : Réservé à un usage d'essai — n'envoyez pas de données personnelles ou confidentielles. Votre utilisation est journalisée à des fins de sécurité et pour améliorer les produits et services de NVIDIA. Les données de session journalisées à des fins d'amélioration ne sont pas liées à votre identité ni à un quelconque identifiant persistant. Pour plus d'informations sur nos pratiques de traitement des données, consultez notre [Politique de confidentialité](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). En interagissant avec cet endpoint, vous consentez à notre collecte, à notre enregistrement et à notre utilisation de ces informations ainsi qu'aux [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs : Les requêtes sont conservées pendant 30 jours conformément à [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/go.mdx b/packages/web/src/content/docs/go.mdx index 507c901a76..8c46464086 100644 --- a/packages/web/src/content/docs/go.mdx +++ b/packages/web/src/content/docs/go.mdx @@ -13,7 +13,7 @@ Go works like any other provider in OpenCode. You subscribe to OpenCode Go and get your API key. It's **completely optional** and you don't need to use it to use OpenCode. -It is designed primarily for international users and provides stable global access. +It is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. --- @@ -62,7 +62,6 @@ The current list of models includes: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -70,13 +69,11 @@ The current list of models includes: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** The list of models may change as we test and add new ones. @@ -97,29 +94,25 @@ The table below provides an estimated request count based on typical Go usage pa | Model | requests per 5 hour | requests per week | requests per month | | ----------------- | ------------------- | ----------------- | ------------------ | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | The estimates are based on observed request patterns: - Grok 4.5 — 1,100 input, 71,500 cached, 220 output tokens per request - GLM-5.2/5.1 — 700 input, 52,000 cached, 150 output tokens per request -- GPT 5.6 Luna — 1,000 input, 50,000 cached, 220 output tokens per request - Kimi K3 — 1,050 input, 76,500 cached, 300 output tokens per request - Kimi K2.7/K2.6 — 870 input, 55,000 cached, 200 output tokens per request - DeepSeek V4 Pro — 750 input, 82,000 cached, 290 output tokens per request @@ -128,19 +121,15 @@ The estimates are based on observed request patterns: - MiniMax M2.7 — 300 input, 55,000 cached, 125 output tokens per request - MiMo-V2.5 — 830 input, 71,500 cached, 295 output tokens per request - MiMo-V2.5-Pro — 790 input, 86,000 cached, 305 output tokens per request -- Qwen3.8 Max — 420 input, 66,000 cached, 200 output tokens per request - Qwen3.7 Max — 420 input, 66,000 cached, 200 output tokens per request - Qwen3.7 Plus — 500 input, 57,000 cached, 190 output tokens per request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens per request -- Hy3 — 830 input, 71,500 cached, 295 output tokens per request The estimates are also based on the following prices per 1M tokens and the monthly usage included with each model: | Model | Input | Output | Cached Read | Cached Write | Usage | | ---------------------------- | ------ | ------ | ----------- | ------------ | ----- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -151,7 +140,6 @@ The estimates are also based on the following prices per 1M tokens and the month | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -159,7 +147,6 @@ The estimates are also based on the following prices per 1M tokens and the month | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | You can track your current usage in the **console**. @@ -198,7 +185,6 @@ You can also access Go models through the following API endpoints. | Model | Model ID | Endpoint | AI SDK Package | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -211,11 +197,9 @@ You can also access Go models through the following API endpoints. | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | The [model id](/docs/config/#models) in your OpenCode config uses the format `opencode-go/`. For example, for Kimi K3, you would @@ -235,30 +219,7 @@ https://opencode.ai/zen/go/v1/models ## Privacy -| Model | Model training | Data retention | -| ----------------- | -------------- | -------------- | -| Grok 4.5 | Not used | 30 days | -| GPT 5.6 Luna | Not used | 30 days | -| GLM-5.2 | Not used | 0 days | -| GLM-5.1 | Not used | 0 days | -| Kimi K3 | Not used | 0 days | -| Kimi K2.7 Code | Not used | 0 days | -| Kimi K2.6 | Not used | 0 days | -| MiMo-V2.5-Pro | Not used | 0 days | -| MiMo-V2.5 | Not used | 0 days | -| Qwen3.8 Max | Not used | 0 days | -| Qwen3.7 Max | Not used | 0 days | -| Qwen3.7 Plus | Not used | 0 days | -| Qwen3.6 Plus | Not used | 0 days | -| MiniMax M3 | Not used | 0 days | -| MiniMax M2.7 | Not used | 0 days | -| DeepSeek V4 Pro | Not used | 0 days | -| DeepSeek V4 Flash | Not used | 0 days | -| Hy3 | Not used | 0 days | - -- **Grok 4.5:** ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API. [Learn more](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days. [Learn more](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026. +The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access. Our providers follow a zero-retention policy and do not use your data for model training. --- diff --git a/packages/web/src/content/docs/it/go.mdx b/packages/web/src/content/docs/it/go.mdx index a724041640..65369e1e86 100644 --- a/packages/web/src/content/docs/it/go.mdx +++ b/packages/web/src/content/docs/it/go.mdx @@ -13,7 +13,7 @@ Go funziona come qualsiasi altro provider in OpenCode. Ti abboni a OpenCode Go e ottieni la tua chiave API. È **completamente facoltativo** e non hai bisogno di usarlo per utilizzare OpenCode. -È progettato principalmente per gli utenti internazionali e garantisce un accesso globale stabile. +È progettato principalmente per gli utenti internazionali, con modelli ospitati negli Stati Uniti, nell'Unione Europea e a Singapore per un accesso globale stabile. --- @@ -60,7 +60,6 @@ L'elenco attuale dei modelli include: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -68,13 +67,11 @@ L'elenco attuale dei modelli include: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** L'elenco dei modelli potrebbe cambiare man mano che ne testiamo e aggiungiamo di nuovi. @@ -95,40 +92,34 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p | Model | richieste ogni 5 ore | richieste a settimana | richieste al mese | | ----------------- | -------------------- | --------------------- | ----------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Le stime si basano sui pattern di richieste osservati: - Grok 4.5 — 1.100 di input, 71.500 in cache, 220 token di output per richiesta - GLM-5.2/5.1 — 700 di input, 52.000 in cache, 150 token di output per richiesta -- GPT 5.6 Luna — 1.000 token di input, 50.000 in cache, 220 token di output per richiesta - Kimi K3 — 1.050 di input, 76.500 in cache, 300 token di output per richiesta - Kimi K2.7/K2.6 — 870 di input, 55.000 in cache, 200 token di output per richiesta - DeepSeek V4 Pro — 750 di input, 82.000 in cache, 290 token di output per richiesta - DeepSeek V4 Flash — 790 di input, 68.000 in cache, 280 token di output per richiesta - MiniMax M3 — 510 di input, 56.000 in cache, 190 token di output per richiesta - MiniMax M2.7 — 300 di input, 55.000 in cache, 125 token di output per richiesta -- Qwen3.8 Max — 420 di input, 66.000 in cache, 200 token di output per richiesta - Qwen3.7 Max — 420 di input, 66.000 in cache, 200 token di output per richiesta - Qwen3.7 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta - Qwen3.6 Plus — 500 di input, 57.000 in cache, 190 token di output per richiesta -- Hy3 — 830 di input, 71.500 in cache, 295 token di output per richiesta - MiMo-V2.5 — 830 di input, 71.500 in cache, 295 token di output per richiesta - MiMo-V2.5-Pro — 790 di input, 86.000 in cache, 305 token di output per richiesta @@ -137,8 +128,6 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil | Modello | Input | Output | Cached Read | Cached Write | Utilizzo | | ---------------------------- | ------ | ------ | ----------- | ------------ | -------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -149,7 +138,6 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -157,7 +145,6 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Puoi monitorare il tuo utilizzo attuale nella **console**. @@ -196,7 +183,6 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API. | Modello | ID Modello | Endpoint | Pacchetto AI SDK | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -209,11 +195,9 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API. | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | Il [model id](/docs/config/#models) nella tua OpenCode config utilizza il formato `opencode-go/`. Ad esempio, per Kimi K3, useresti @@ -233,30 +217,7 @@ https://opencode.ai/zen/go/v1/models ## Privacy -| Modello | Addestramento del modello | Conservazione dei dati | -| ----------------- | ------------------------- | ---------------------- | -| Grok 4.5 | Non utilizzato | 30 giorni | -| GPT 5.6 Luna | Non utilizzato | 30 giorni | -| GLM-5.2 | Non utilizzato | 0 giorni | -| GLM-5.1 | Non utilizzato | 0 giorni | -| Kimi K3 | Non utilizzato | 0 giorni | -| Kimi K2.7 Code | Non utilizzato | 0 giorni | -| Kimi K2.6 | Non utilizzato | 0 giorni | -| MiMo-V2.5-Pro | Non utilizzato | 0 giorni | -| MiMo-V2.5 | Non utilizzato | 0 giorni | -| Qwen3.8 Max | Non utilizzato | 0 giorni | -| Qwen3.7 Max | Non utilizzato | 0 giorni | -| Qwen3.7 Plus | Non utilizzato | 0 giorni | -| Qwen3.6 Plus | Non utilizzato | 0 giorni | -| MiniMax M3 | Non utilizzato | 0 giorni | -| MiniMax M2.7 | Non utilizzato | 0 giorni | -| DeepSeek V4 Pro | Non utilizzato | 0 giorni | -| DeepSeek V4 Flash | Non utilizzato | 0 giorni | -| Hy3 | Non utilizzato | 0 giorni | - -- **Grok 4.5:** ZDR disabilita importanti funzionalità API che dipendono dai dati archiviati, tra cui la Responses API con stato, Files and Collections e Batch API. [Scopri di più](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** I log di monitoraggio degli abusi vengono generati per l'utilizzo di tutte le funzionalità API e conservati per un massimo di 30 giorni. [Scopri di più](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** L'accordo ZDR viene rinnovato mensilmente. L'accordo attuale è valido fino al 31 agosto 2026. +Il piano è pensato principalmente per gli utenti internazionali, con modelli ospitati negli US, nell’EU e a Singapore per un accesso globale stabile. I nostri provider seguono una politica di zero-retention e non utilizzano i tuoi dati per l’addestramento dei modelli. --- diff --git a/packages/web/src/content/docs/it/zen.mdx b/packages/web/src/content/docs/it/zen.mdx index 941db5be94..b0efca238a 100644 --- a/packages/web/src/content/docs/it/zen.mdx +++ b/packages/web/src/content/docs/it/zen.mdx @@ -58,68 +58,62 @@ Ti viene addebitato ogni richiesta e puoi aggiungere credito al tuo account. Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API. -| Modello | Model ID | Endpoint | Pacchetto AI SDK | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Modello | Model ID | Endpoint | Pacchetto AI SDK | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5, useresti @@ -146,8 +140,6 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,7 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**. | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Potresti notare [modelli a basso costo](/docs/config/#models), come Haiku, Nano o Flash, nella cronologia di utilizzo. OpenCode usa questi modelli per generare i titoli delle sessioni. +Potresti notare _Claude Haiku 3.5_ nella cronologia di utilizzo. È un [modello a basso costo](/docs/config/#models) usato per generare i titoli delle tue sessioni. :::note Le commissioni della carta di credito vengono trasferite al costo (4.4% + $0.30 per transazione); non addebitiamo nulla oltre a questo. @@ -222,8 +210,6 @@ I modelli gratuiti: - DeepSeek V4 Flash Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello. - MiMo-V2.5 Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello. -- Laguna S 2.1 Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello. -- Ling-3.0-flash Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello. - North Mini Code Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello. - Nemotron 3 Ultra Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello. - Big Pickle è un modello stealth che è gratuito su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello. @@ -283,8 +269,6 @@ Tutti i nostri modelli sono ospitati negli US. I nostri provider seguono una pol - Big Pickle: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello. - DeepSeek V4 Flash Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello. - MiMo-V2.5 Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello. -- Laguna S 2.1 Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello. -- Ling-3.0-flash Free: durante il periodo gratuito, i dati raccolti possono essere usati per migliorare il modello. - North Mini Code Free: Durante il periodo gratuito, i dati raccolti possono essere conservati e utilizzati per migliorare il modello. Non inviare dati personali o riservati. Consulta i nostri [Termini di utilizzo](https://cohere.com/terms-of-use) e la nostra [Informativa sulla privacy](https://cohere.com/privacy). - Nemotron 3 Ultra Free (endpoint NVIDIA gratuiti): solo per uso di prova — non inviare dati personali o riservati. Il tuo utilizzo viene registrato per finalità di sicurezza e per migliorare i prodotti e i servizi di NVIDIA. I dati di sessione registrati a fini di miglioramento non sono collegati alla tua identità né ad alcun identificatore persistente. Per maggiori informazioni sulle nostre pratiche di trattamento dei dati, consulta la nostra [Informativa sulla privacy](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Interagendo con questo endpoint, acconsenti alla nostra raccolta, registrazione e utilizzo di tali informazioni e ai [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: le richieste vengono conservate per 30 giorni in conformità con [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/ja/go.mdx b/packages/web/src/content/docs/ja/go.mdx index 963a36b1cc..8bb9139e83 100644 --- a/packages/web/src/content/docs/ja/go.mdx +++ b/packages/web/src/content/docs/ja/go.mdx @@ -11,7 +11,7 @@ OpenCode Goは低価格のサブスクリプションで、**初月は5ドル** GoはOpenCodeの他のプロバイダーと同様に機能します。OpenCode GoをサブスクライブしてAPIキーを取得します。これは**完全に任意**であり、OpenCodeを使用するために必須ではありません。 -主に海外ユーザー向けに設計されており、世界中から安定してアクセスできます。 +主に海外ユーザー向けに設計されており、世界中で安定してアクセスできるよう、モデルは米国、EU、シンガポールでホストされています。 --- @@ -52,7 +52,6 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** 新しいモデルをテストして追加するにつれて、モデルのリストは変更される場合があります。 @@ -87,40 +84,34 @@ OpenCode Goには以下の制限が含まれています: | Model | 5時間あたりのリクエスト数 | 週間リクエスト数 | 月間リクエスト数 | | ----------------- | ------------------------- | ---------------- | ---------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | 推定値は、観測されたリクエストパターンに基づいています: - Grok 4.5 — リクエストあたり 入力 1,100トークン、キャッシュ 71,500トークン、出力 220トークン - GLM-5.2/5.1 — リクエストあたり 入力 700トークン、キャッシュ 52,000トークン、出力 150トークン -- GPT 5.6 Luna — リクエストあたり 入力 1,000トークン、キャッシュ 50,000トークン、出力 220トークン - Kimi K3 — リクエストあたり 入力 1,050トークン、キャッシュ 76,500トークン、出力 300トークン - Kimi K2.7/K2.6 — リクエストあたり 入力 870トークン、キャッシュ 55,000トークン、出力 200トークン - DeepSeek V4 Pro — リクエストあたり 入力 750トークン、キャッシュ 82,000トークン、出力 290トークン - DeepSeek V4 Flash — リクエストあたり 入力 790トークン、キャッシュ 68,000トークン、出力 280トークン - MiniMax M3 — リクエストあたり 入力 510トークン、キャッシュ 56,000トークン、出力 190トークン - MiniMax M2.7 — リクエストあたり 入力 300トークン、キャッシュ 55,000トークン、出力 125トークン -- Qwen3.8 Max — リクエストあたり 入力 420トークン、キャッシュ 66,000トークン、出力 200トークン - Qwen3.7 Max — リクエストあたり 入力 420トークン、キャッシュ 66,000トークン、出力 200トークン - Qwen3.7 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン - Qwen3.6 Plus — リクエストあたり 入力 500トークン、キャッシュ 57,000トークン、出力 190トークン -- Hy3 — リクエストあたり 入力 830トークン、キャッシュ 71,500トークン、出力 295トークン - MiMo-V2.5 — リクエストあたり 入力 830トークン、キャッシュ 71,500トークン、出力 295トークン - MiMo-V2.5-Pro — リクエストあたり 入力 790トークン、キャッシュ 86,000トークン、出力 305トークン @@ -129,8 +120,6 @@ OpenCode Goには以下の制限が含まれています: | Model | Input | Output | Cached Read | Cached Write | Usage | | ---------------------------- | ------ | ------ | ----------- | ------------ | ----- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ OpenCode Goには以下の制限が含まれています: | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ OpenCode Goには以下の制限が含まれています: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 現在の利用状況は**コンソール**で追跡できます。 @@ -186,7 +173,6 @@ Goでは月額$10を支払い、その6倍の利用枠を提供することを | Model | Model ID | Endpoint | AI SDK Package | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ Goでは月額$10を支払い、その6倍の利用枠を提供することを | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode設定の[model id](/docs/config/#models)は、`opencode-go/`という形式を使用します。たとえば、Kimi K3の場合は、設定で`opencode-go/kimi-k3`を使用します。 @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## プライバシー -| モデル | モデルのトレーニング | データ保持 | -| ----------------- | -------------------- | ---------- | -| Grok 4.5 | 使用なし | 30日 | -| GPT 5.6 Luna | 使用なし | 30日 | -| GLM-5.2 | 使用なし | 0日 | -| GLM-5.1 | 使用なし | 0日 | -| Kimi K3 | 使用なし | 0日 | -| Kimi K2.7 Code | 使用なし | 0日 | -| Kimi K2.6 | 使用なし | 0日 | -| MiMo-V2.5-Pro | 使用なし | 0日 | -| MiMo-V2.5 | 使用なし | 0日 | -| Qwen3.8 Max | 使用なし | 0日 | -| Qwen3.7 Max | 使用なし | 0日 | -| Qwen3.7 Plus | 使用なし | 0日 | -| Qwen3.6 Plus | 使用なし | 0日 | -| MiniMax M3 | 使用なし | 0日 | -| MiniMax M2.7 | 使用なし | 0日 | -| DeepSeek V4 Pro | 使用なし | 0日 | -| DeepSeek V4 Flash | 使用なし | 0日 | -| Hy3 | 使用なし | 0日 | - -- **Grok 4.5:** ZDRでは、保存データに依存する重要なAPI機能(ステートフルなResponses API、Files and Collections、Batch APIなど)が無効になります。[詳しく見る](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr)。 -- **GPT 5.6 Luna:** 不正使用監視ログはすべてのAPI機能の使用時に生成され、最大30日間保持されます。[詳しく見る](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring)。 -- **DeepSeek V4 Flash:** ZDR契約は毎月更新されます。現在の契約は2026年8月31日まで有効です。 +このプランは主に海外ユーザー向けに設計されており、安定したグローバルアクセスのため、モデルは US、EU、Singapore でホストされています。各プロバイダーはデータを保持しないポリシーに従っており、お客様のデータをモデルのトレーニングに使用することはありません。 --- diff --git a/packages/web/src/content/docs/ja/zen.mdx b/packages/web/src/content/docs/ja/zen.mdx index 73b8d5a187..636d835ec5 100644 --- a/packages/web/src/content/docs/ja/zen.mdx +++ b/packages/web/src/content/docs/ja/zen.mdx @@ -49,68 +49,62 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動 以下の API エンドポイントを通じて、私たちのモデルにアクセスすることもできます。 -| Model | Model ID | Endpoint | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | Model ID | Endpoint | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode 設定で使う [model id](/docs/config/#models) は `opencode/` 形式です。たとえば、GPT 5.5 では設定に `opencode/gpt-5.5` を使用します。 @@ -135,8 +129,6 @@ https://opencode.ai/zen/v1/models | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -146,7 +138,6 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -156,7 +147,6 @@ https://opencode.ai/zen/v1/models | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -166,9 +156,7 @@ https://opencode.ai/zen/v1/models | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -177,10 +165,10 @@ https://opencode.ai/zen/v1/models | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -201,7 +189,7 @@ https://opencode.ai/zen/v1/models | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -使用履歴に Haiku、Nano、Flash などの[低コストモデル](/docs/config/#models)が表示されることがあります。OpenCode はこれらのモデルをセッションタイトルの生成に使用します。 +使用履歴に _Claude Haiku 3.5_ が表示されることがあります。これはセッションのタイトル生成に使われる [low cost model](/docs/config/#models) です。 :::note クレジットカード手数料は実費で転嫁されます(各取引につき 4.4% + $0.30)。それ以外の上乗せはありません。 @@ -211,8 +199,6 @@ https://opencode.ai/zen/v1/models - DeepSeek V4 Flash Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。 - MiMo-V2.5 Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。 -- Laguna S 2.1 Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。 -- Ling-3.0-flash Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。 - North Mini Code Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。 - Nemotron 3 Ultra Free は期間限定で OpenCode で利用できます。チームはこの期間中にフィードバックを集め、モデルを改善しています。 - Big Pickle はステルスモデルで、期間限定で OpenCode で無料提供されています。チームはこの期間中にフィードバックを集め、モデルを改善しています。 @@ -269,8 +255,6 @@ https://opencode.ai/zen/v1/models - Big Pickle: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。 - DeepSeek V4 Flash Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。 - MiMo-V2.5 Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。 -- Laguna S 2.1 Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。 -- Ling-3.0-flash Free: 無料提供期間中、収集されたデータがモデル改善に使われる場合があります。 - North Mini Code Free: 無料提供期間中、収集されたデータは保持され、モデルの改善に使用される場合があります。個人情報や機密情報を送信しないでください。詳しくは、[利用規約](https://cohere.com/terms-of-use)および[プライバシーポリシー](https://cohere.com/privacy)をご覧ください。 - Nemotron 3 Ultra Free(NVIDIA の無料エンドポイント): 試用専用です — 個人情報や機密データは送信しないでください。お客様の利用は、セキュリティ目的および NVIDIA の製品とサービスの改善のために記録されます。改善目的で記録されたセッションデータは、お客様の身元や永続的な識別子とは関連付けられません。当社のデータ処理慣行の詳細については、[プライバシーポリシー](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)をご覧ください。このエンドポイントを利用することで、お客様はそのような情報の当社による収集、記録、利用、および [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) に同意したものとみなされます。 - OpenAI APIs: リクエストは [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) に従って 30 日間保持されます。 diff --git a/packages/web/src/content/docs/ko/go.mdx b/packages/web/src/content/docs/ko/go.mdx index 29693cdb6a..02f88d2828 100644 --- a/packages/web/src/content/docs/ko/go.mdx +++ b/packages/web/src/content/docs/ko/go.mdx @@ -11,7 +11,7 @@ OpenCode Go는 인기 있는 오픈 코딩 모델에 안정적으로 액세스 Go는 OpenCode의 다른 제공자와 똑같이 작동합니다. OpenCode Go를 구독하고 API 키를 발급받으면 됩니다. 이는 **완전히 선택 사항**이며, OpenCode를 사용하기 위해 꼭 필요하지는 않습니다. -주로 해외 사용자를 위해 설계되었으며, 전 세계에서 안정적으로 이용할 수 있습니다. +주로 해외 사용자를 위해 설계되었으며, 안정적인 전 세계 액세스를 위해 모델은 미국, EU, 싱가포르에 호스팅됩니다. --- @@ -52,7 +52,6 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다. - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다. - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** 새로운 모델을 테스트하고 추가함에 따라 이 목록은 변경될 수 있습니다. @@ -87,40 +84,34 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | Model | 5시간당 요청 횟수 | 주간 요청 횟수 | 월간 요청 횟수 | | ----------------- | ----------------- | -------------- | -------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | 이 예상치는 관찰된 요청 패턴을 기준으로 합니다. - Grok 4.5 — 요청당 입력 1,100, 캐시 71,500, 출력 토큰 220 - GLM-5.2/5.1 — 요청당 입력 700, 캐시 52,000, 출력 토큰 150 -- GPT 5.6 Luna — 요청당 입력 토큰 1,000개, 캐시 토큰 50,000개, 출력 토큰 220개 - Kimi K3 — 요청당 입력 1,050, 캐시 76,500, 출력 토큰 300 - Kimi K2.7/K2.6 — 요청당 입력 870, 캐시 55,000, 출력 토큰 200 - DeepSeek V4 Pro — 요청당 입력 750, 캐시 82,000, 출력 토큰 290 - DeepSeek V4 Flash — 요청당 입력 790, 캐시 68,000, 출력 토큰 280 - MiniMax M3 — 요청당 입력 510, 캐시 56,000, 출력 토큰 190 - MiniMax M2.7 — 요청당 입력 300, 캐시 55,000, 출력 토큰 125 -- Qwen3.8 Max — 요청당 입력 420, 캐시 66,000, 출력 토큰 200 - Qwen3.7 Max — 요청당 입력 420, 캐시 66,000, 출력 토큰 200 - Qwen3.7 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190 - Qwen3.6 Plus — 요청당 입력 500, 캐시 57,000, 출력 토큰 190 -- Hy3 — 요청당 입력 830, 캐시 71,500, 출력 토큰 295 - MiMo-V2.5 — 요청당 입력 830, 캐시 71,500, 출력 토큰 295 - MiMo-V2.5-Pro — 요청당 입력 790, 캐시 86,000, 출력 토큰 305 @@ -129,8 +120,6 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | Model | Input | Output | Cached Read | Cached Write | Usage | | ---------------------------- | ------ | ------ | ----------- | ------------ | ----- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다. | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 현재 사용량은 **console**에서 확인할 수 있습니다. @@ -186,7 +173,6 @@ Go에서는 월 $10를 지불하며, 저희는 그 6배의 사용량을 제공 | 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ Go에서는 월 $10를 지불하며, 저희는 그 6배의 사용량을 제공 | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode config의 [model id](/docs/config/#models)는 `opencode-go/` 형식을 사용합니다. 예를 들어 Kimi K3의 경우 config에서 `opencode-go/kimi-k3`를 사용하면 됩니다. @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## 개인정보 보호 -| 모델 | 모델 학습 | 데이터 보존 | -| ----------------- | ------------- | ----------- | -| Grok 4.5 | 사용되지 않음 | 30일 | -| GPT 5.6 Luna | 사용되지 않음 | 30일 | -| GLM-5.2 | 사용되지 않음 | 0일 | -| GLM-5.1 | 사용되지 않음 | 0일 | -| Kimi K3 | 사용되지 않음 | 0일 | -| Kimi K2.7 Code | 사용되지 않음 | 0일 | -| Kimi K2.6 | 사용되지 않음 | 0일 | -| MiMo-V2.5-Pro | 사용되지 않음 | 0일 | -| MiMo-V2.5 | 사용되지 않음 | 0일 | -| Qwen3.8 Max | 사용되지 않음 | 0일 | -| Qwen3.7 Max | 사용되지 않음 | 0일 | -| Qwen3.7 Plus | 사용되지 않음 | 0일 | -| Qwen3.6 Plus | 사용되지 않음 | 0일 | -| MiniMax M3 | 사용되지 않음 | 0일 | -| MiniMax M2.7 | 사용되지 않음 | 0일 | -| DeepSeek V4 Pro | 사용되지 않음 | 0일 | -| DeepSeek V4 Flash | 사용되지 않음 | 0일 | -| Hy3 | 사용되지 않음 | 0일 | - -- **Grok 4.5:** ZDR은 저장된 데이터에 의존하는 중요한 API 기능(상태 저장형 Responses API, Files and Collections, Batch API 포함)을 비활성화합니다. [자세히 알아보기](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** 모든 API 기능 사용에 대해 악용 모니터링 로그가 생성되며 최대 30일 동안 보존됩니다. [자세히 알아보기](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** ZDR 계약은 매월 갱신됩니다. 현재 계약은 2026년 8월 31일까지 유효합니다. +이 플랜은 안정적인 전 세계 액세스를 위해 모델을 미국, EU, 싱가포르에 호스팅하며, 주로 해외 사용자를 위해 설계되었습니다. 저희 제공자는 zero-retention 정책을 따르며, 고객 데이터를 모델 학습에 사용하지 않습니다. --- diff --git a/packages/web/src/content/docs/ko/zen.mdx b/packages/web/src/content/docs/ko/zen.mdx index 474bd38ff4..ce3d279477 100644 --- a/packages/web/src/content/docs/ko/zen.mdx +++ b/packages/web/src/content/docs/ko/zen.mdx @@ -49,68 +49,62 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다. 다음 API 엔드포인트를 통해서도 모델에 접근할 수 있습니다. -| 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode config에서 사용하는 [모델 ID](/docs/config/#models)는 `opencode/` 형식입니다. 예를 들어 GPT 5.5를 사용하려면 config에서 `opencode/gpt-5.5`를 사용하면 됩니다. @@ -135,8 +129,6 @@ https://opencode.ai/zen/v1/models | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -146,7 +138,6 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -156,7 +147,6 @@ https://opencode.ai/zen/v1/models | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -166,9 +156,7 @@ https://opencode.ai/zen/v1/models | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -177,10 +165,10 @@ https://opencode.ai/zen/v1/models | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -201,7 +189,7 @@ https://opencode.ai/zen/v1/models | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -사용 기록에서 Haiku, Nano 또는 Flash와 같은 [저비용 모델](/docs/config/#models)을 볼 수 있습니다. OpenCode는 이러한 모델을 사용해 세션 제목을 생성합니다. +사용 기록에서 *Claude Haiku 3.5*를 볼 수 있습니다. 이는 세션 제목을 생성할 때 사용되는 [저비용 모델](/docs/config/#models)입니다. :::note 신용카드 수수료는 원가 그대로 전달됩니다(거래당 4.4% + $0.30). 그 외 추가 요금은 부과하지 않습니다. @@ -211,8 +199,6 @@ https://opencode.ai/zen/v1/models - DeepSeek V4 Flash Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다. - MiMo-V2.5 Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다. -- Laguna S 2.1 Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다. -- Ling-3.0-flash Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다. - North Mini Code Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다. - Nemotron 3 Ultra Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다. - Big Pickle은 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다. @@ -269,8 +255,6 @@ https://opencode.ai/zen/v1/models - Big Pickle: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다. - DeepSeek V4 Flash Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다. - MiMo-V2.5 Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다. -- Laguna S 2.1 Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다. -- Ling-3.0-flash Free: 무료 제공 기간에는 수집된 데이터가 모델 개선에 사용될 수 있습니다. - North Mini Code Free: 무료 제공 기간 동안 수집된 데이터는 보관되며 모델 개선에 사용될 수 있습니다. 개인 정보나 기밀 정보를 제출하지 마세요. 자세한 내용은 [이용 약관](https://cohere.com/terms-of-use) 및 [개인정보 처리방침](https://cohere.com/privacy)을 참조하세요. - Nemotron 3 Ultra Free(NVIDIA 무료 엔드포인트): 평가판 전용이며 — 개인 정보나 기밀 데이터는 제출하지 마세요. 사용 내역은 보안 목적과 NVIDIA 제품 및 서비스 개선을 위해 기록됩니다. 개선 목적으로 기록된 세션 데이터는 사용자의 신원이나 영구 식별자와 연결되지 않습니다. 당사의 데이터 처리 관행에 대한 자세한 내용은 [개인정보처리방침](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)을 참조하세요. 이 엔드포인트와 상호 작용함으로써 사용자는 당사가 이러한 정보를 수집, 기록, 사용하는 것과 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)에 동의하게 됩니다. - OpenAI APIs: 요청은 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data)에 따라 30일 동안 보관됩니다. diff --git a/packages/web/src/content/docs/nb/go.mdx b/packages/web/src/content/docs/nb/go.mdx index afcc39e19b..60be1cc7bb 100644 --- a/packages/web/src/content/docs/nb/go.mdx +++ b/packages/web/src/content/docs/nb/go.mdx @@ -13,7 +13,7 @@ Go fungerer som enhver annen leverandør i OpenCode. Du abonnerer på OpenCode G får din API-nøkkel. Det er **helt valgfritt**, og du trenger ikke å bruke det for å bruke OpenCode. -Det er først og fremst utviklet for internasjonale brukere og gir stabil global tilgang. +Det er primært utformet for internasjonale brukere, med modeller driftet i USA, EU og Singapore for stabil global tilgang. --- @@ -62,7 +62,6 @@ Den nåværende listen over modeller inkluderer: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -70,13 +69,11 @@ Den nåværende listen over modeller inkluderer: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** Listen over modeller kan endres etter hvert som vi tester og legger til nye. @@ -97,40 +94,34 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm | Model | forespørsler per 5 timer | forespørsler per uke | forespørsler per måned | | ----------------- | ------------------------ | -------------------- | ---------------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Estimatene er basert på observerte forespørselsmønstre: - Grok 4.5 — 1 100 input, 71 500 bufret, 220 output-tokens per forespørsel - GLM-5.2/5.1 — 700 input, 52 000 bufret, 150 output-tokens per forespørsel -- GPT 5.6 Luna — 1 000 input, 50 000 bufret, 220 output-tokens per forespørsel - Kimi K3 — 1 050 input, 76 500 bufret, 300 output-tokens per forespørsel - Kimi K2.7/K2.6 — 870 input, 55 000 bufret, 200 output-tokens per forespørsel - DeepSeek V4 Pro — 750 input, 82 000 bufret, 290 output-tokens per forespørsel - DeepSeek V4 Flash — 790 input, 68 000 bufret, 280 output-tokens per forespørsel - MiniMax M3 — 510 input, 56 000 bufret, 190 output-tokens per forespørsel - MiniMax M2.7 — 300 input, 55 000 bufret, 125 output-tokens per forespørsel -- Qwen3.8 Max — 420 input, 66 000 bufret, 200 output-tokens per forespørsel - Qwen3.7 Max — 420 input, 66 000 bufret, 200 output-tokens per forespørsel - Qwen3.7 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel - Qwen3.6 Plus — 500 input, 57 000 bufret, 190 output-tokens per forespørsel -- Hy3 — 830 input, 71 500 bufret, 295 output-tokens per forespørsel - MiMo-V2.5 — 830 input, 71 500 bufret, 295 output-tokens per forespørsel - MiMo-V2.5-Pro — 790 input, 86 000 bufret, 305 output-tokens per forespørsel @@ -139,8 +130,6 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige | Model | Input | Output | Cached Read | Cached Write | Bruk | | ---------------------------- | ------ | ------ | ----------- | ------------ | ---- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -151,7 +140,6 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -159,7 +147,6 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Du kan spore din nåværende bruk i **konsollen**. @@ -198,7 +185,6 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter. | Modell | Modell-ID | Endepunkt | AI SDK Package | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -211,11 +197,9 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter. | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [Modell-ID-en](/docs/config/#models) i din OpenCode-konfigurasjon bruker formatet `opencode-go/`. For eksempel, for Kimi K3, vil du @@ -235,30 +219,7 @@ https://opencode.ai/zen/go/v1/models ## Personvern -| Modell | Modelltrening | Dataoppbevaring | -| ----------------- | ------------- | --------------- | -| Grok 4.5 | Brukes ikke | 30 dager | -| GPT 5.6 Luna | Brukes ikke | 30 dager | -| GLM-5.2 | Brukes ikke | 0 dager | -| GLM-5.1 | Brukes ikke | 0 dager | -| Kimi K3 | Brukes ikke | 0 dager | -| Kimi K2.7 Code | Brukes ikke | 0 dager | -| Kimi K2.6 | Brukes ikke | 0 dager | -| MiMo-V2.5-Pro | Brukes ikke | 0 dager | -| MiMo-V2.5 | Brukes ikke | 0 dager | -| Qwen3.8 Max | Brukes ikke | 0 dager | -| Qwen3.7 Max | Brukes ikke | 0 dager | -| Qwen3.7 Plus | Brukes ikke | 0 dager | -| Qwen3.6 Plus | Brukes ikke | 0 dager | -| MiniMax M3 | Brukes ikke | 0 dager | -| MiniMax M2.7 | Brukes ikke | 0 dager | -| DeepSeek V4 Pro | Brukes ikke | 0 dager | -| DeepSeek V4 Flash | Brukes ikke | 0 dager | -| Hy3 | Brukes ikke | 0 dager | - -- **Grok 4.5:** ZDR deaktiverer viktige API-funksjoner som er avhengige av lagrede data, inkludert den tilstandsbaserte Responses API, Files and Collections og Batch API. [Les mer](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Logger for overvåking av misbruk genereres for all bruk av API-funksjoner og oppbevares i opptil 30 dager. [Les mer](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** ZDR-avtalen fornyes månedlig. Den gjeldende avtalen er gyldig til og med 31. august 2026. +Planen er primært utformet for internasjonale brukere, med modeller hostet i US, EU og Singapore for stabil global tilgang. Våre leverandører følger en zero-retention-policy og bruker ikke dataene dine til modelltrening. --- diff --git a/packages/web/src/content/docs/nb/zen.mdx b/packages/web/src/content/docs/nb/zen.mdx index 74cc45b3a9..2e3f05d8ff 100644 --- a/packages/web/src/content/docs/nb/zen.mdx +++ b/packages/web/src/content/docs/nb/zen.mdx @@ -58,68 +58,62 @@ Du blir belastet per forespørsel, og du kan legge til kreditt på kontoen din. Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter. -| Modell | Modell-ID | Endepunkt | AI SDK-pakke | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Modell | Modell-ID | Endepunkt | AI SDK-pakke | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5, ville du @@ -146,8 +140,6 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**. | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Du vil kanskje legge merke til [lavprismodeller](/docs/config/#models), som Haiku, Nano eller Flash, i brukshistorikken din. OpenCode bruker disse modellene til å generere økttitler. +Du vil kanskje legge merke til _Claude Haiku 3.5_ i brukshistorikken din. Dette er en [lavprismodell](/docs/config/#models) som brukes til å generere titlene på øktene dine. :::note Kredittkortgebyrer videreføres til kostpris (4.4% + $0.30 per transaction); vi tar ikke betalt noe utover det. @@ -222,8 +210,6 @@ Gratis-modellene: - DeepSeek V4 Flash Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen. - MiMo-V2.5 Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen. -- Laguna S 2.1 Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen. -- Ling-3.0-flash Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen. - North Mini Code Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen. - Nemotron 3 Ultra Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen. - Big Pickle er en stealth-modell som er gratis på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen. @@ -283,8 +269,6 @@ Alle modellene våre hostes i US. Leverandørene våre følger en policy for zer - Big Pickle: I gratisperioden kan innsamlede data brukes til å forbedre modellen. - DeepSeek V4 Flash Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen. - MiMo-V2.5 Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen. -- Laguna S 2.1 Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen. -- Ling-3.0-flash Free: I gratisperioden kan innsamlede data brukes til å forbedre modellen. - North Mini Code Free: I gratisperioden kan innsamlede data bli oppbevart og brukt til å forbedre modellen. Ikke send inn personopplysninger eller konfidensielle opplysninger. Se våre [Vilkår for bruk](https://cohere.com/terms-of-use) og vår [Personvernerklæring](https://cohere.com/privacy). - Nemotron 3 Ultra Free (gratis NVIDIA-endepunkter): Kun for prøvebruk — ikke send inn personopplysninger eller konfidensielle data. Bruken din logges av sikkerhetshensyn og for å forbedre NVIDIAs produkter og tjenester. Sesjonsdataene som logges for forbedringsformål, er ikke knyttet til identiteten din eller noen vedvarende identifikator. For mer informasjon om vår databehandlingspraksis, se vår [personvernerklæring](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Ved å samhandle med dette endepunktet samtykker du til at vi samler inn, registrerer og bruker slik informasjon, samt til [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: Forespørsler lagres i 30 dager i samsvar med [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/pl/go.mdx b/packages/web/src/content/docs/pl/go.mdx index f215736964..4a867bf0e7 100644 --- a/packages/web/src/content/docs/pl/go.mdx +++ b/packages/web/src/content/docs/pl/go.mdx @@ -13,7 +13,7 @@ Go działa jak każdy inny dostawca w OpenCode. Subskrybujesz OpenCode Go i otrzymujesz swój klucz API. Jest to **całkowicie opcjonalne** i nie musisz z tego korzystać, aby używać OpenCode. -Jest przeznaczony przede wszystkim dla użytkowników z całego świata i zapewnia stabilny dostęp globalny. +Jest przeznaczony przede wszystkim dla użytkowników międzynarodowych, a modele są hostowane w USA, UE i Singapurze, co zapewnia stabilny globalny dostęp. --- @@ -56,7 +56,6 @@ Obecna lista modeli obejmuje: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -64,13 +63,11 @@ Obecna lista modeli obejmuje: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** Lista modeli może ulec zmianie w miarę testowania i dodawania nowych. @@ -91,40 +88,34 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych | Model | żądania na 5 godzin | żądania na tydzień | żądania na miesiąc | | ----------------- | ------------------- | ------------------ | ------------------ | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Szacunki te opierają się na zaobserwowanych wzorcach żądań: - Grok 4.5 — 1 100 tokenów wejściowych, 71 500 w pamięci podręcznej, 220 tokenów wyjściowych na żądanie - GLM-5.2/5.1 — 700 tokenów wejściowych, 52 000 w pamięci podręcznej, 150 tokenów wyjściowych na żądanie -- GPT 5.6 Luna — 1 000 tokenów wejściowych, 50 000 w pamięci podręcznej, 220 tokenów wyjściowych na żądanie - Kimi K3 — 1 050 tokenów wejściowych, 76 500 w pamięci podręcznej, 300 tokenów wyjściowych na żądanie - Kimi K2.7/K2.6 — 870 tokenów wejściowych, 55 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie - DeepSeek V4 Pro — 750 tokenów wejściowych, 82 000 w pamięci podręcznej, 290 tokenów wyjściowych na żądanie - DeepSeek V4 Flash — 790 tokenów wejściowych, 68 000 w pamięci podręcznej, 280 tokenów wyjściowych na żądanie - MiniMax M3 — 510 tokenów wejściowych, 56 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie - MiniMax M2.7 — 300 tokenów wejściowych, 55 000 w pamięci podręcznej, 125 tokenów wyjściowych na żądanie -- Qwen3.8 Max — 420 tokenów wejściowych, 66 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie - Qwen3.7 Max — 420 tokenów wejściowych, 66 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie - Qwen3.7 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie - Qwen3.6 Plus — 500 tokenów wejściowych, 57 000 w pamięci podręcznej, 190 tokenów wyjściowych na żądanie -- Hy3 — 830 tokenów wejściowych, 71 500 w pamięci podręcznej, 295 tokenów wyjściowych na żądanie - MiMo-V2.5 — 830 tokenów wejściowych, 71 500 w pamięci podręcznej, 295 tokenów wyjściowych na żądanie - MiMo-V2.5-Pro — 790 tokenów wejściowych, 86 000 w pamięci podręcznej, 305 tokenów wyjściowych na żądanie @@ -133,8 +124,6 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz | Model | Wejście | Wyjście | Odczyt z cache | Zapis do cache | Użycie | | ---------------------------- | ------- | ------- | -------------- | -------------- | ------ | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -145,7 +134,6 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -153,7 +141,6 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Możesz śledzić swoje bieżące zużycie w **konsoli**. @@ -190,7 +177,6 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc | Model | ID modelu | Punkt końcowy | Pakiet AI SDK | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -203,11 +189,9 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [ID modelu](/docs/config/#models) w Twojej konfiguracji OpenCode używa formatu `opencode-go/`. Na przykład dla Kimi K3 należy użyć @@ -227,30 +211,7 @@ https://opencode.ai/zen/go/v1/models ## Prywatność -| Model | Trenowanie modelu | Retencja danych | -| ----------------- | ----------------- | --------------- | -| Grok 4.5 | Niewykorzystywane | 30 dni | -| GPT 5.6 Luna | Niewykorzystywane | 30 dni | -| GLM-5.2 | Niewykorzystywane | 0 dni | -| GLM-5.1 | Niewykorzystywane | 0 dni | -| Kimi K3 | Niewykorzystywane | 0 dni | -| Kimi K2.7 Code | Niewykorzystywane | 0 dni | -| Kimi K2.6 | Niewykorzystywane | 0 dni | -| MiMo-V2.5-Pro | Niewykorzystywane | 0 dni | -| MiMo-V2.5 | Niewykorzystywane | 0 dni | -| Qwen3.8 Max | Niewykorzystywane | 0 dni | -| Qwen3.7 Max | Niewykorzystywane | 0 dni | -| Qwen3.7 Plus | Niewykorzystywane | 0 dni | -| Qwen3.6 Plus | Niewykorzystywane | 0 dni | -| MiniMax M3 | Niewykorzystywane | 0 dni | -| MiniMax M2.7 | Niewykorzystywane | 0 dni | -| DeepSeek V4 Pro | Niewykorzystywane | 0 dni | -| DeepSeek V4 Flash | Niewykorzystywane | 0 dni | -| Hy3 | Niewykorzystywane | 0 dni | - -- **Grok 4.5:** ZDR wyłącza ważne funkcje API zależne od przechowywanych danych, w tym stanowy Responses API, Files and Collections oraz Batch API. [Dowiedz się więcej](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Dzienniki monitorowania nadużyć są generowane dla każdego użycia funkcji API i przechowywane przez maksymalnie 30 dni. [Dowiedz się więcej](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** Umowa ZDR jest odnawiana co miesiąc. Obecna umowa obowiązuje do 31 sierpnia 2026 r. +Plan został zaprojektowany przede wszystkim z myślą o użytkownikach międzynarodowych, a modele są hostowane w US, EU i Singapore, aby zapewnić stabilny dostęp na całym świecie. Nasi dostawcy stosują politykę zerowej retencji i nie wykorzystują Twoich danych do trenowania modeli. --- diff --git a/packages/web/src/content/docs/pl/zen.mdx b/packages/web/src/content/docs/pl/zen.mdx index c6c8ba7fb4..a41602d17d 100644 --- a/packages/web/src/content/docs/pl/zen.mdx +++ b/packages/web/src/content/docs/pl/zen.mdx @@ -58,68 +58,62 @@ Płatność jest naliczana za każde żądanie i możesz doładować swoje konto Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API. -| Model | ID modelu | Endpoint | Pakiet AI SDK | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | ID modelu | Endpoint | Pakiet AI SDK | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5 użyjesz w konfiguracji @@ -146,8 +140,6 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,8 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów* | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -W historii użycia możesz zauważyć [niedrogie modele](/docs/config/#models), takie jak Haiku, Nano lub Flash. OpenCode używa tych modeli do generowania tytułów sesji. +Możesz zauważyć _Claude Haiku 3.5_ w historii użycia. To [niedrogi model](/docs/config/#models), +który służy do generowania tytułów Twoich sesji. :::note Opłaty za karty kredytowe są przenoszone po kosztach (4.4% + $0.30 per transaction); nie naliczamy nic ponad to. @@ -222,8 +211,6 @@ Darmowe modele: - DeepSeek V4 Flash Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu. - MiMo-V2.5 Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu. -- Laguna S 2.1 Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu. -- Ling-3.0-flash Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu. - North Mini Code Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu. - Nemotron 3 Ultra Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu. - Big Pickle to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu. @@ -283,8 +270,6 @@ Wszystkie nasze modele są hostowane w US. Nasi dostawcy stosują politykę zero - Big Pickle: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu. - DeepSeek V4 Flash Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu. - MiMo-V2.5 Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu. -- Laguna S 2.1 Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu. -- Ling-3.0-flash Free: W czasie darmowego okresu zebrane dane mogą być wykorzystywane do ulepszania modelu. - North Mini Code Free: W okresie bezpłatnego dostępu zebrane dane mogą być przechowywane i wykorzystywane do ulepszania modelu. Nie przesyłaj danych osobowych ani poufnych. Zapoznaj się z naszym [Regulaminem korzystania](https://cohere.com/terms-of-use) i [Polityką prywatności](https://cohere.com/privacy). - Nemotron 3 Ultra Free (darmowe endpointy NVIDIA): Tylko do użytku próbnego — nie przesyłaj danych osobowych ani poufnych. Twoje korzystanie jest rejestrowane w celach bezpieczeństwa oraz w celu ulepszania produktów i usług NVIDIA. Rejestrowane dane sesji wykorzystywane do celów ulepszania nie są powiązane z Twoją tożsamością ani żadnym trwałym identyfikatorem. Aby uzyskać więcej informacji o naszych praktykach przetwarzania danych, zapoznaj się z naszą [Polityką prywatności](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Korzystając z tego endpointu, wyrażasz zgodę na gromadzenie, rejestrowanie i wykorzystywanie przez nas takich informacji oraz na [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: Żądania są przechowywane przez 30 dni zgodnie z [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/providers.mdx b/packages/web/src/content/docs/providers.mdx index 7931d4e03f..d3c541311f 100644 --- a/packages/web/src/content/docs/providers.mdx +++ b/packages/web/src/content/docs/providers.mdx @@ -1505,30 +1505,6 @@ To use Kimi K2 from Moonshot AI: --- -### Modal - -1. Create a [shared Endpoint](https://modal.com/endpoints) for the model you want to use. - -2. Create a [proxy token](https://modal.com/docs/guide/endpoints#proxy-tokens), then join its ID and secret with a period: - - ```txt - wk-.ws- - ``` - -3. Run the `/connect` command, search for **Modal**, and enter the combined proxy token. - - ```txt - /connect - ``` - -4. Run the `/models` command to select one of the endpoints in your Modal workspace. - - ```txt - /models - ``` - ---- - ### NVIDIA NVIDIA provides access to Nemotron models and many other open models through [build.nvidia.com](https://build.nvidia.com) for free. diff --git a/packages/web/src/content/docs/pt-br/go.mdx b/packages/web/src/content/docs/pt-br/go.mdx index fcfc8ed608..96c1addfcc 100644 --- a/packages/web/src/content/docs/pt-br/go.mdx +++ b/packages/web/src/content/docs/pt-br/go.mdx @@ -13,7 +13,7 @@ O Go funciona como qualquer outro provedor no OpenCode. Você assina o OpenCode obtém a sua chave de API. Ele é **totalmente opcional** e você não precisa usá-lo para usar o OpenCode. -Ele foi projetado principalmente para usuários internacionais e oferece acesso global estável. +Ele foi projetado principalmente para usuários internacionais, com modelos hospedados nos EUA, na UE e em Singapura para um acesso global estável. --- @@ -62,7 +62,6 @@ A lista atual de modelos inclui: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -70,13 +69,11 @@ A lista atual de modelos inclui: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** A lista de modelos pode mudar conforme testamos e adicionamos novos. @@ -97,40 +94,34 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr | Model | requisições por 5 horas | requisições por semana | requisições por mês | | ----------------- | ----------------------- | ---------------------- | ------------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | As estimativas se baseiam nos padrões de requisições observados: - Grok 4.5 — 1.100 tokens de entrada, 71.500 em cache, 220 tokens de saída por requisição - GLM-5.2/5.1 — 700 tokens de entrada, 52.000 em cache, 150 tokens de saída por requisição -- GPT 5.6 Luna — 1.000 tokens de entrada, 50.000 em cache, 220 tokens de saída por requisição - Kimi K3 — 1.050 tokens de entrada, 76.500 em cache, 300 tokens de saída por requisição - Kimi K2.7/K2.6 — 870 tokens de entrada, 55.000 em cache, 200 tokens de saída por requisição - DeepSeek V4 Pro — 750 tokens de entrada, 82.000 em cache, 290 tokens de saída por requisição - DeepSeek V4 Flash — 790 tokens de entrada, 68.000 em cache, 280 tokens de saída por requisição - MiniMax M3 — 510 tokens de entrada, 56.000 em cache, 190 tokens de saída por requisição - MiniMax M2.7 — 300 tokens de entrada, 55.000 em cache, 125 tokens de saída por requisição -- Qwen3.8 Max — 420 tokens de entrada, 66.000 em cache, 200 tokens de saída por requisição - Qwen3.7 Max — 420 tokens de entrada, 66.000 em cache, 200 tokens de saída por requisição - Qwen3.7 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição - Qwen3.6 Plus — 500 tokens de entrada, 57.000 em cache, 190 tokens de saída por requisição -- Hy3 — 830 tokens de entrada, 71.500 em cache, 295 tokens de saída por requisição - MiMo-V2.5 — 830 tokens de entrada, 71.500 em cache, 295 tokens de saída por requisição - MiMo-V2.5-Pro — 790 tokens de entrada, 86.000 em cache, 305 tokens de saída por requisição @@ -139,8 +130,6 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m | Modelo | Entrada | Saída | Leitura em cache | Escrita em cache | Uso | | ---------------------------- | ------- | ------ | ---------------- | ---------------- | --- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -151,7 +140,6 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -159,7 +147,6 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Você pode acompanhar o seu uso atual no **console**. @@ -198,7 +185,6 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de | Modelo | ID do Modelo | Endpoint | Pacote do AI SDK | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -211,11 +197,9 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | O [ID do modelo](/docs/config/#models) na sua configuração do OpenCode usa o formato `opencode-go/`. Por exemplo, para o Kimi K3, você usaria @@ -235,30 +219,7 @@ https://opencode.ai/zen/go/v1/models ## Privacidade -| Modelo | Treinamento de modelos | Retenção de dados | -| ----------------- | ---------------------- | ----------------- | -| Grok 4.5 | Não usado | 30 dias | -| GPT 5.6 Luna | Não usado | 30 dias | -| GLM-5.2 | Não usado | 0 dias | -| GLM-5.1 | Não usado | 0 dias | -| Kimi K3 | Não usado | 0 dias | -| Kimi K2.7 Code | Não usado | 0 dias | -| Kimi K2.6 | Não usado | 0 dias | -| MiMo-V2.5-Pro | Não usado | 0 dias | -| MiMo-V2.5 | Não usado | 0 dias | -| Qwen3.8 Max | Não usado | 0 dias | -| Qwen3.7 Max | Não usado | 0 dias | -| Qwen3.7 Plus | Não usado | 0 dias | -| Qwen3.6 Plus | Não usado | 0 dias | -| MiniMax M3 | Não usado | 0 dias | -| MiniMax M2.7 | Não usado | 0 dias | -| DeepSeek V4 Pro | Não usado | 0 dias | -| DeepSeek V4 Flash | Não usado | 0 dias | -| Hy3 | Não usado | 0 dias | - -- **Grok 4.5:** O ZDR desativa recursos importantes da API que dependem de dados armazenados, incluindo a Responses API com estado, Files and Collections e a Batch API. [Saiba mais](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Logs de monitoramento de abuso são gerados para todo uso de recursos da API e retidos por até 30 dias. [Saiba mais](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** O acordo de ZDR é renovado mensalmente. O acordo atual é válido até 31 de agosto de 2026. +O plano foi projetado principalmente para usuários internacionais, com modelos hospedados em US, EU e Singapore para garantir acesso global estável. Nossos provedores seguem uma política de retenção zero e não usam seus dados para treinamento de modelos. --- diff --git a/packages/web/src/content/docs/pt-br/zen.mdx b/packages/web/src/content/docs/pt-br/zen.mdx index fa19e0e7d2..c2aa5cf945 100644 --- a/packages/web/src/content/docs/pt-br/zen.mdx +++ b/packages/web/src/content/docs/pt-br/zen.mdx @@ -49,68 +49,62 @@ Você é cobrado por solicitação e pode adicionar créditos à sua conta. Você também pode acessar nossos modelos pelos seguintes endpoints de API. -| Modelo | ID do modelo | Endpoint | Pacote AI SDK | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Modelo | ID do modelo | Endpoint | Pacote AI SDK | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5, você usaria `opencode/gpt-5.5` na sua configuração. @@ -135,8 +129,6 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -146,7 +138,6 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -156,7 +147,6 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -166,9 +156,7 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -177,10 +165,10 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -201,7 +189,7 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**. | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Você pode notar [modelos de baixo custo](/docs/config/#models), como Haiku, Nano ou Flash, no seu histórico de uso. O OpenCode usa esses modelos para gerar títulos de sessões. +Você pode notar _Claude Haiku 3.5_ no seu histórico de uso. Este é um [low cost model](/docs/config/#models) usado para gerar os títulos das suas sessões. :::note As taxas de cartão de crédito são repassadas a preço de custo (4.4% + $0.30 por transação); não cobramos nada além disso. @@ -211,8 +199,6 @@ Os modelos gratuitos: - DeepSeek V4 Flash Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo. - MiMo-V2.5 Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo. -- Laguna S 2.1 Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo. -- Ling-3.0-flash Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo. - North Mini Code Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo. - Nemotron 3 Ultra Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo. - Big Pickle é um modelo stealth que está gratuito no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo. @@ -269,8 +255,6 @@ Todos os nossos modelos são hospedados nos US. Nossos provedores seguem uma pol - Big Pickle: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo. - DeepSeek V4 Flash Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo. - MiMo-V2.5 Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo. -- Laguna S 2.1 Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo. -- Ling-3.0-flash Free: Durante seu período gratuito, os dados coletados podem ser usados para melhorar o modelo. - North Mini Code Free: Durante o período gratuito, os dados coletados poderão ser retidos e usados para aprimorar o modelo. Não envie dados pessoais ou confidenciais. Consulte nossos [Termos de Uso](https://cohere.com/terms-of-use) e nossa [Política de Privacidade](https://cohere.com/privacy). - Nemotron 3 Ultra Free (endpoints gratuitos da NVIDIA): Apenas para uso de avaliação — não envie dados pessoais ou confidenciais. Seu uso é registrado para fins de segurança e para melhorar os produtos e serviços da NVIDIA. Os dados de sessão registrados para fins de melhoria não estão vinculados à sua identidade nem a qualquer identificador persistente. Para mais informações sobre nossas práticas de processamento de dados, consulte nossa [Política de Privacidade](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Ao interagir com este endpoint, você consente com a nossa coleta, registro e uso dessas informações e com os [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: As solicitações são retidas por 30 dias de acordo com [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/ru/go.mdx b/packages/web/src/content/docs/ru/go.mdx index 4df18953ef..62305fbdb6 100644 --- a/packages/web/src/content/docs/ru/go.mdx +++ b/packages/web/src/content/docs/ru/go.mdx @@ -13,7 +13,7 @@ Go работает так же, как и любой другой провай получаете свой API-ключ. Использование Go **абсолютно необязательно**, и вам не нужно использовать его, чтобы пользоваться OpenCode. -Она предназначена прежде всего для международных пользователей и обеспечивает стабильный доступ по всему миру. +Она предназначена в первую очередь для пользователей со всего мира, а модели размещены в США, ЕС и Сингапуре для стабильного глобального доступа. --- @@ -62,7 +62,6 @@ OpenCode Go работает так же, как и любой другой пр - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -70,13 +69,11 @@ OpenCode Go работает так же, как и любой другой пр - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** Список моделей может меняться по мере того, как мы тестируем и добавляем новые. @@ -97,40 +94,34 @@ OpenCode Go включает следующие лимиты: | Model | запросов за 5 часов | запросов в неделю | запросов в месяц | | ----------------- | ------------------- | ----------------- | ---------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Эти оценки основаны на наблюдаемых показателях запросов: - Grok 4.5 — 1,100 входных, 71,500 кешированных, 220 выходных токенов на запрос - GLM-5.2/5.1 — 700 входных, 52,000 кешированных, 150 выходных токенов на запрос -- GPT 5.6 Luna — 1,000 входных, 50,000 кешированных, 220 выходных токенов на запрос - Kimi K3 — 1,050 входных, 76,500 кешированных, 300 выходных токенов на запрос - Kimi K2.7/K2.6 — 870 входных, 55,000 кешированных, 200 выходных токенов на запрос - DeepSeek V4 Pro — 750 входных, 82,000 кешированных, 290 выходных токенов на запрос - DeepSeek V4 Flash — 790 входных, 68,000 кешированных, 280 выходных токенов на запрос - MiniMax M3 — 510 входных, 56,000 кешированных, 190 выходных токенов на запрос - MiniMax M2.7 — 300 входных, 55,000 кешированных, 125 выходных токенов на запрос -- Qwen3.8 Max — 420 входных, 66,000 кешированных, 200 выходных токенов на запрос - Qwen3.7 Max — 420 входных, 66,000 кешированных, 200 выходных токенов на запрос - Qwen3.7 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос - Qwen3.6 Plus — 500 входных, 57,000 кешированных, 190 выходных токенов на запрос -- Hy3 — 830 входных, 71,500 кешированных, 295 выходных токенов на запрос - MiMo-V2.5 — 830 входных, 71,500 кешированных, 295 выходных токенов на запрос - MiMo-V2.5-Pro — 790 входных, 86,000 кешированных, 305 выходных токенов на запрос @@ -139,8 +130,6 @@ OpenCode Go включает следующие лимиты: | Model | Input | Output | Cached Read | Cached Write | Использование | | ---------------------------- | ------ | ------ | ----------- | ------------ | ------------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -151,7 +140,6 @@ OpenCode Go включает следующие лимиты: | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -159,7 +147,6 @@ OpenCode Go включает следующие лимиты: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Вы можете отслеживать текущее использование в **консоли**. @@ -198,7 +185,6 @@ OpenCode Go включает следующие лимиты: | Модель | ID модели | Эндпоинт | Пакет AI SDK | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -211,11 +197,9 @@ OpenCode Go включает следующие лимиты: | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [ID модели](/docs/config/#models) в вашем конфиге OpenCode использует формат `opencode-go/`. Например, для Kimi K3 вам нужно @@ -235,30 +219,7 @@ https://opencode.ai/zen/go/v1/models ## Конфиденциальность -| Модель | Обучение моделей | Хранение данных | -| ----------------- | ---------------- | --------------- | -| Grok 4.5 | Не используется | 30 дней | -| GPT 5.6 Luna | Не используется | 30 дней | -| GLM-5.2 | Не используется | 0 дней | -| GLM-5.1 | Не используется | 0 дней | -| Kimi K3 | Не используется | 0 дней | -| Kimi K2.7 Code | Не используется | 0 дней | -| Kimi K2.6 | Не используется | 0 дней | -| MiMo-V2.5-Pro | Не используется | 0 дней | -| MiMo-V2.5 | Не используется | 0 дней | -| Qwen3.8 Max | Не используется | 0 дней | -| Qwen3.7 Max | Не используется | 0 дней | -| Qwen3.7 Plus | Не используется | 0 дней | -| Qwen3.6 Plus | Не используется | 0 дней | -| MiniMax M3 | Не используется | 0 дней | -| MiniMax M2.7 | Не используется | 0 дней | -| DeepSeek V4 Pro | Не используется | 0 дней | -| DeepSeek V4 Flash | Не используется | 0 дней | -| Hy3 | Не используется | 0 дней | - -- **Grok 4.5:** ZDR отключает важные функции API, зависящие от сохраненных данных, включая Responses API с сохранением состояния, Files and Collections и Batch API. [Подробнее](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Журналы мониторинга злоупотреблений создаются при любом использовании функций API и хранятся до 30 дней. [Подробнее](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** Соглашение ZDR продлевается ежемесячно. Текущее соглашение действует до 31 августа 2026 года. +Этот план разработан в первую очередь для международных пользователей: модели размещены в US, EU и Singapore, чтобы обеспечить стабильный доступ по всему миру. Наши провайдеры придерживаются политики zero-retention и не используют ваши данные для обучения моделей. --- diff --git a/packages/web/src/content/docs/ru/zen.mdx b/packages/web/src/content/docs/ru/zen.mdx index f9633ac323..242b350390 100644 --- a/packages/web/src/content/docs/ru/zen.mdx +++ b/packages/web/src/content/docs/ru/zen.mdx @@ -58,68 +58,62 @@ OpenCode Zen работает как любой другой провайдер Вы также можете получить доступ к нашим моделям через следующие конечные точки API. -| Модель | Идентификатор модели | Конечная точка | Пакет AI SDK | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Модель | Идентификатор модели | Конечная точка | Пакет AI SDK | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | [идентификатор модели](/docs/config/#models) в вашей конфигурации OpenCode использует формат `opencode/`. Например, для GPT 5.5 вам нужно @@ -146,8 +140,6 @@ https://opencode.ai/zen/v1/models | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ https://opencode.ai/zen/v1/models | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ https://opencode.ai/zen/v1/models | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ https://opencode.ai/zen/v1/models | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,7 @@ https://opencode.ai/zen/v1/models | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -В истории использования могут появляться [недорогие модели](/docs/config/#models), такие как Haiku, Nano или Flash. OpenCode использует эти модели для создания заголовков сессий. +Вы можете заметить _Claude Haiku 3.5_ в истории использования. Это [недорогая модель](/docs/config/#models), которая используется для генерации заголовков ваших сессий. :::note Комиссии по кредитным картам передаются по себестоимости (4.4% + $0.30 за транзакцию); мы ничего не начисляем сверх этого. @@ -222,8 +210,6 @@ https://opencode.ai/zen/v1/models - DeepSeek V4 Flash Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель. - MiMo-V2.5 Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель. -- Laguna S 2.1 Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель. -- Ling-3.0-flash Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель. - North Mini Code Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель. - Nemotron 3 Ultra Free доступна в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель. - Big Pickle — это скрытая модель, которая доступна бесплатно в OpenCode ограниченное время. Команда использует это время, чтобы собирать отзывы и улучшать модель. @@ -283,8 +269,6 @@ https://opencode.ai/zen/v1/models - Big Pickle: во время бесплатного периода собранные данные могут использоваться для улучшения модели. - DeepSeek V4 Flash Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели. - MiMo-V2.5 Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели. -- Laguna S 2.1 Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели. -- Ling-3.0-flash Free: во время бесплатного периода собранные данные могут использоваться для улучшения модели. - North Mini Code Free: В течение бесплатного периода собранные данные могут храниться и использоваться для улучшения модели. Не отправляйте персональные или конфиденциальные данные. Ознакомьтесь с нашими [Условиями использования](https://cohere.com/terms-of-use) и [Политикой конфиденциальности](https://cohere.com/privacy). - Nemotron 3 Ultra Free (бесплатные эндпоинты NVIDIA): только для пробного использования — не отправляйте персональные или конфиденциальные данные. Использование логируется в целях безопасности и для улучшения продуктов и сервисов NVIDIA. Логируемые данные сессии, используемые в целях улучшения, не связаны с вашей личностью или каким-либо постоянным идентификатором. Подробнее о наших практиках обработки данных см. в нашей [Политике конфиденциальности](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). Взаимодействуя с этим эндпоинтом, вы соглашаетесь на сбор, запись и использование нами такой информации, а также с [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: запросы хранятся 30 дней в соответствии с [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/th/go.mdx b/packages/web/src/content/docs/th/go.mdx index f241b77ee0..36036426da 100644 --- a/packages/web/src/content/docs/th/go.mdx +++ b/packages/web/src/content/docs/th/go.mdx @@ -11,7 +11,7 @@ OpenCode Go คือการสมัครสมาชิกในราค Go ทำงานเหมือนกับผู้ให้บริการ (provider) รายอื่นๆ ใน OpenCode คุณสามารถสมัครสมาชิก OpenCode Go และรับ API key ของคุณ บริการนี้เป็น**ทางเลือกเพิ่มเติม** และคุณไม่จำเป็นต้องใช้มันเพื่อใช้งาน OpenCode -บริการนี้ออกแบบมาโดยเน้นผู้ใช้ในต่างประเทศเป็นหลัก และให้บริการได้อย่างเสถียรทั่วโลก +บริการนี้ออกแบบมาเพื่อผู้ใช้ในระดับสากลเป็นหลัก โดยมีโมเดลโฮสต์อยู่ในสหรัฐอเมริกา สหภาพยุโรป และสิงคโปร์ เพื่อการเข้าถึงทั่วโลกที่เสถียร --- @@ -52,7 +52,6 @@ OpenCode Go ทำงานเหมือนกับผู้ให้บร - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ OpenCode Go ทำงานเหมือนกับผู้ให้บร - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** รายชื่อโมเดลอาจมีการเปลี่ยนแปลงเมื่อเราทำการทดสอบและเพิ่มโมเดลใหม่ๆ @@ -87,40 +84,34 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Model | requests ต่อ 5 ชั่วโมง | requests ต่อสัปดาห์ | requests ต่อเดือน | | ----------------- | ---------------------- | ------------------- | ----------------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | การประมาณการนี้อ้างอิงจากรูปแบบการใช้งาน request ที่สังเกตพบ: - Grok 4.5 — 1,100 input, 71,500 cached, 220 output tokens ต่อ request - GLM-5.2/5.1 — 700 input, 52,000 cached, 150 output tokens ต่อ request -- GPT 5.6 Luna — 1,000 input, 50,000 cached, 220 output tokens ต่อ request - Kimi K3 — 1,050 input, 76,500 cached, 300 output tokens ต่อ request - Kimi K2.7/K2.6 — 870 input, 55,000 cached, 200 output tokens ต่อ request - DeepSeek V4 Pro — 750 input, 82,000 cached, 290 output tokens ต่อ request - DeepSeek V4 Flash — 790 input, 68,000 cached, 280 output tokens ต่อ request - MiniMax M3 — 510 input, 56,000 cached, 190 output tokens ต่อ request - MiniMax M2.7 — 300 input, 55,000 cached, 125 output tokens ต่อ request -- Qwen3.8 Max — 420 input, 66,000 cached, 200 output tokens ต่อ request - Qwen3.7 Max — 420 input, 66,000 cached, 200 output tokens ต่อ request - Qwen3.7 Plus — 500 input, 57,000 cached, 190 output tokens ต่อ request - Qwen3.6 Plus — 500 input, 57,000 cached, 190 output tokens ต่อ request -- Hy3 — 830 input, 71,500 cached, 295 output tokens ต่อ request - MiMo-V2.5 — 830 input, 71,500 cached, 295 output tokens ต่อ request - MiMo-V2.5-Pro — 790 input, 86,000 cached, 305 output tokens ต่อ request @@ -129,8 +120,6 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Model | Input | Output | Cached Read | Cached Write | Usage | | ---------------------------- | ------ | ------ | ----------- | ------------ | ----- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | คุณสามารถติดตามการใช้งานปัจจุบันของคุณได้ใน **console** @@ -186,7 +173,6 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | Model | Model ID | Endpoint | AI SDK Package | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ OpenCode Go มีขีดจำกัดดังต่อไปนี้: | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | [model id](/docs/config/#models) ใน OpenCode config ของคุณจะใช้รูปแบบ `opencode-go/` ตัวอย่างเช่น สำหรับ Kimi K3 คุณจะใช้ `opencode-go/kimi-k3` ใน config ของคุณ @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## Privacy -| โมเดล | การฝึกโมเดล | การเก็บรักษาข้อมูล | -| ----------------- | ----------- | ------------------ | -| Grok 4.5 | ไม่นำไปใช้ | 30 วัน | -| GPT 5.6 Luna | ไม่นำไปใช้ | 30 วัน | -| GLM-5.2 | ไม่นำไปใช้ | 0 วัน | -| GLM-5.1 | ไม่นำไปใช้ | 0 วัน | -| Kimi K3 | ไม่นำไปใช้ | 0 วัน | -| Kimi K2.7 Code | ไม่นำไปใช้ | 0 วัน | -| Kimi K2.6 | ไม่นำไปใช้ | 0 วัน | -| MiMo-V2.5-Pro | ไม่นำไปใช้ | 0 วัน | -| MiMo-V2.5 | ไม่นำไปใช้ | 0 วัน | -| Qwen3.8 Max | ไม่นำไปใช้ | 0 วัน | -| Qwen3.7 Max | ไม่นำไปใช้ | 0 วัน | -| Qwen3.7 Plus | ไม่นำไปใช้ | 0 วัน | -| Qwen3.6 Plus | ไม่นำไปใช้ | 0 วัน | -| MiniMax M3 | ไม่นำไปใช้ | 0 วัน | -| MiniMax M2.7 | ไม่นำไปใช้ | 0 วัน | -| DeepSeek V4 Pro | ไม่นำไปใช้ | 0 วัน | -| DeepSeek V4 Flash | ไม่นำไปใช้ | 0 วัน | -| Hy3 | ไม่นำไปใช้ | 0 วัน | - -- **Grok 4.5:** ZDR ปิดใช้งานฟีเจอร์ API สำคัญที่ต้องอาศัยข้อมูลที่จัดเก็บไว้ ซึ่งรวมถึง Responses API แบบมีสถานะ, Files and Collections และ Batch API [ดูข้อมูลเพิ่มเติม](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr) -- **GPT 5.6 Luna:** ระบบจะสร้างบันทึกการตรวจสอบการใช้งานในทางที่ผิดสำหรับการใช้งานฟีเจอร์ API ทั้งหมด และเก็บรักษาไว้นานสูงสุด 30 วัน [ดูข้อมูลเพิ่มเติม](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring) -- **DeepSeek V4 Flash:** ข้อตกลง ZDR จะต่ออายุทุกเดือน ข้อตกลงปัจจุบันมีผลใช้ถึงวันที่ 31 สิงหาคม 2026 +แพลนนี้ออกแบบมาสำหรับผู้ใช้ทั่วโลกเป็นหลัก โดยโฮสต์โมเดลไว้ใน US, EU และ Singapore เพื่อให้เข้าถึงได้อย่างเสถียรจากทั่วโลก ผู้ให้บริการของเราปฏิบัติตามนโยบาย zero-retention และไม่นำข้อมูลของคุณไปใช้ในการฝึกโมเดล --- diff --git a/packages/web/src/content/docs/th/zen.mdx b/packages/web/src/content/docs/th/zen.mdx index 2c256704ae..8fbdda3a67 100644 --- a/packages/web/src/content/docs/th/zen.mdx +++ b/packages/web/src/content/docs/th/zen.mdx @@ -51,68 +51,62 @@ OpenCode Zen ทำงานเหมือน provider อื่น ๆ ใน คุณยังสามารถเข้าถึงโมเดลของเราผ่าน API endpoints ต่อไปนี้ได้ -| Model | Model ID | Endpoint | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | Model ID | Endpoint | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | [model id](/docs/config/#models) ใน OpenCode config ของคุณใช้รูปแบบ `opencode/` ตัวอย่างเช่น สำหรับ GPT 5.5 คุณจะใช้ `opencode/gpt-5.5` ใน config ของคุณ @@ -137,8 +131,6 @@ https://opencode.ai/zen/v1/models | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -148,7 +140,6 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -158,7 +149,6 @@ https://opencode.ai/zen/v1/models | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -168,9 +158,7 @@ https://opencode.ai/zen/v1/models | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -179,10 +167,10 @@ https://opencode.ai/zen/v1/models | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -203,7 +191,7 @@ https://opencode.ai/zen/v1/models | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -คุณอาจสังเกตเห็น[โมเดลต้นทุนต่ำ](/docs/config/#models) เช่น Haiku, Nano หรือ Flash ในประวัติการใช้งานของคุณ OpenCode ใช้โมเดลเหล่านี้เพื่อสร้างชื่อเซสชัน +คุณอาจสังเกตเห็น _Claude Haiku 3.5_ ในประวัติการใช้งานของคุณ นี่คือ [low cost model](/docs/config/#models) ที่ใช้สร้างชื่อ session ของคุณ :::note ค่าธรรมเนียมบัตรเครดิตจะถูกส่งต่อในราคาทุน (4.4% + $0.30 ต่อธุรกรรม); เราไม่ได้คิดค่าใช้จ่ายเพิ่มจากนั้น @@ -213,8 +201,6 @@ https://opencode.ai/zen/v1/models - DeepSeek V4 Flash Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล - MiMo-V2.5 Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล -- Laguna S 2.1 Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล -- Ling-3.0-flash Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล - North Mini Code Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล - Nemotron 3 Ultra Free เปิดให้ใช้บน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล - Big Pickle เป็น stealth model ที่ใช้งานฟรีบน OpenCode ในช่วงเวลาจำกัด ทีมกำลังใช้ช่วงเวลานี้เพื่อเก็บ feedback และปรับปรุงโมเดล @@ -271,8 +257,6 @@ https://opencode.ai/zen/v1/models - Big Pickle: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล - DeepSeek V4 Flash Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล - MiMo-V2.5 Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล -- Laguna S 2.1 Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล -- Ling-3.0-flash Free: ระหว่างช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกนำไปใช้เพื่อปรับปรุงโมเดล - North Mini Code Free: ในช่วงที่เปิดให้ใช้ฟรี ข้อมูลที่เก็บรวบรวมอาจถูกเก็บรักษาและนำไปใช้เพื่อปรับปรุงโมเดล โปรดอย่าส่งข้อมูลส่วนบุคคลหรือข้อมูลที่เป็นความลับ ดู[ข้อกำหนดการใช้งาน](https://cohere.com/terms-of-use)และ[นโยบายความเป็นส่วนตัว](https://cohere.com/privacy)ของเรา - Nemotron 3 Ultra Free (endpoint ฟรีของ NVIDIA): ใช้สำหรับการทดลองเท่านั้น — โปรดอย่าส่งข้อมูลส่วนบุคคลหรือข้อมูลลับ การใช้งานของคุณจะถูกบันทึกเพื่อวัตถุประสงค์ด้านความปลอดภัยและเพื่อปรับปรุงผลิตภัณฑ์และบริการของ NVIDIA ข้อมูลเซสชันที่บันทึกไว้เพื่อวัตถุประสงค์ในการปรับปรุงจะไม่เชื่อมโยงกับตัวตนของคุณหรือตัวระบุถาวรใด ๆ สำหรับข้อมูลเพิ่มเติมเกี่ยวกับแนวปฏิบัติในการประมวลผลข้อมูลของเรา โปรดดู [นโยบายความเป็นส่วนตัว](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) ของเรา การโต้ตอบกับ endpoint นี้ถือว่าคุณยินยอมให้เราเก็บรวบรวม บันทึก และใช้ข้อมูลดังกล่าว รวมถึง [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) - OpenAI APIs: คำขอจะถูกเก็บไว้เป็นเวลา 30 วันตาม [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/tr/go.mdx b/packages/web/src/content/docs/tr/go.mdx index b480e0b2e5..e24ead2959 100644 --- a/packages/web/src/content/docs/tr/go.mdx +++ b/packages/web/src/content/docs/tr/go.mdx @@ -11,7 +11,7 @@ OpenCode Go, popüler açık kodlama modellerine güvenilir erişim sağlayan d Go, OpenCode'daki diğer sağlayıcılar gibi çalışır. OpenCode Go'ya abone olur ve API anahtarınızı alırsınız. Bu **tamamen isteğe bağlıdır** ve OpenCode'u kullanmak için bunu kullanmanıza gerek yoktur. -Öncelikle uluslararası kullanıcılar için tasarlanmıştır ve dünya genelinde istikrarlı erişim sağlar. +Dünya çapında istikrarlı erişim için ABD, AB ve Singapur'da barındırılan modellerle temel olarak uluslararası kullanıcılar için tasarlanmıştır. --- @@ -52,7 +52,6 @@ Mevcut model listesi şunları içerir: - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ Mevcut model listesi şunları içerir: - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** Test edip yenilerini ekledikçe model listesi değişebilir. @@ -87,40 +84,34 @@ Aşağıdaki tablo, tipik Go kullanım modellerine dayalı tahmini bir istek say | Model | 5 saatte bir istek | haftalık istek | aylık istek | | ----------------- | ------------------ | -------------- | ----------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | Tahminler, gözlemlenen istek modellerine dayanır: - Grok 4.5 — İstek başına 1.100 girdi, 71.500 önbelleğe alınmış, 220 çıktı token'ı - GLM-5.2/5.1 — İstek başına 700 girdi, 52.000 önbelleğe alınmış, 150 çıktı token'ı -- GPT 5.6 Luna — İstek başına 1.000 girdi, 50.000 önbelleğe alınmış, 220 çıktı token'ı - Kimi K3 — İstek başına 1.050 girdi, 76.500 önbelleğe alınmış, 300 çıktı token'ı - Kimi K2.7/K2.6 — İstek başına 870 girdi, 55.000 önbelleğe alınmış, 200 çıktı token'ı - DeepSeek V4 Pro — İstek başına 750 girdi, 82.000 önbelleğe alınmış, 290 çıktı token'ı - DeepSeek V4 Flash — İstek başına 790 girdi, 68.000 önbelleğe alınmış, 280 çıktı token'ı - MiniMax M3 — İstek başına 510 girdi, 56.000 önbelleğe alınmış, 190 çıktı token'ı - MiniMax M2.7 — İstek başına 300 girdi, 55.000 önbelleğe alınmış, 125 çıktı token'ı -- Qwen3.8 Max — İstek başına 420 girdi, 66.000 önbelleğe alınmış, 200 çıktı token'ı - Qwen3.7 Max — İstek başına 420 girdi, 66.000 önbelleğe alınmış, 200 çıktı token'ı - Qwen3.7 Plus — İstek başına 500 girdi, 57.000 önbelleğe alınmış, 190 çıktı token'ı - Qwen3.6 Plus — İstek başına 500 girdi, 57.000 önbelleğe alınmış, 190 çıktı token'ı -- Hy3 — İstek başına 830 girdi, 71.500 önbelleğe alınmış, 295 çıktı token'ı - MiMo-V2.5 — İstek başına 830 girdi, 71.500 önbelleğe alınmış, 295 çıktı token'ı - MiMo-V2.5-Pro — İstek başına 790 girdi, 86.000 önbelleğe alınmış, 305 çıktı token'ı @@ -129,8 +120,6 @@ Tahminler ayrıca 1M token başına aşağıdaki fiyatlara ve her modelle birlik | Model | Input | Output | Cached Read | Cached Write | Kullanım | | ---------------------------- | ------ | ------ | ----------- | ------------ | -------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ Tahminler ayrıca 1M token başına aşağıdaki fiyatlara ve her modelle birlik | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ Tahminler ayrıca 1M token başına aşağıdaki fiyatlara ve her modelle birlik | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | Mevcut kullanımınızı **konsoldan** takip edebilirsiniz. @@ -186,7 +173,6 @@ Go modellerine aşağıdaki API uç noktaları aracılığıyla da erişebilirsi | Model | Model ID | Uç Nokta | AI SDK Paketi | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ Go modellerine aşağıdaki API uç noktaları aracılığıyla da erişebilirsi | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode yapılandırmanızdaki [model id](/docs/config/#models) formatı `opencode-go/` şeklindedir. Örneğin, Kimi K3 için yapılandırmanızda `opencode-go/kimi-k3` kullanmalısınız. @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## Gizlilik -| Model | Model eğitimi | Veri saklama | -| ----------------- | ------------- | ------------ | -| Grok 4.5 | Kullanılmaz | 30 gün | -| GPT 5.6 Luna | Kullanılmaz | 30 gün | -| GLM-5.2 | Kullanılmaz | 0 gün | -| GLM-5.1 | Kullanılmaz | 0 gün | -| Kimi K3 | Kullanılmaz | 0 gün | -| Kimi K2.7 Code | Kullanılmaz | 0 gün | -| Kimi K2.6 | Kullanılmaz | 0 gün | -| MiMo-V2.5-Pro | Kullanılmaz | 0 gün | -| MiMo-V2.5 | Kullanılmaz | 0 gün | -| Qwen3.8 Max | Kullanılmaz | 0 gün | -| Qwen3.7 Max | Kullanılmaz | 0 gün | -| Qwen3.7 Plus | Kullanılmaz | 0 gün | -| Qwen3.6 Plus | Kullanılmaz | 0 gün | -| MiniMax M3 | Kullanılmaz | 0 gün | -| MiniMax M2.7 | Kullanılmaz | 0 gün | -| DeepSeek V4 Pro | Kullanılmaz | 0 gün | -| DeepSeek V4 Flash | Kullanılmaz | 0 gün | -| Hy3 | Kullanılmaz | 0 gün | - -- **Grok 4.5:** ZDR, durum bilgisi tutan Responses API, Files and Collections ve Batch API dahil olmak üzere saklanan verilere bağlı önemli API özelliklerini devre dışı bırakır. [Daha fazla bilgi](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr). -- **GPT 5.6 Luna:** Tüm API özelliklerinin kullanımı için kötüye kullanım izleme günlükleri oluşturulur ve 30 güne kadar saklanır. [Daha fazla bilgi](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring). -- **DeepSeek V4 Flash:** ZDR anlaşması aylık olarak yenilenir. Mevcut anlaşma 31 Ağustos 2026 tarihine kadar geçerlidir. +Plan, öncelikle uluslararası kullanıcılar için tasarlanmıştır; dünya genelinde istikrarlı erişim sağlamak için modeller US, EU ve Singapore'da barındırılır. Sağlayıcılarımız sıfır veri saklama politikası uygular ve verilerinizi model eğitimi için kullanmaz. --- diff --git a/packages/web/src/content/docs/tr/zen.mdx b/packages/web/src/content/docs/tr/zen.mdx index 83f5cfea78..f36dba702d 100644 --- a/packages/web/src/content/docs/tr/zen.mdx +++ b/packages/web/src/content/docs/tr/zen.mdx @@ -49,68 +49,62 @@ OpenCode Zen, OpenCode'daki diğer sağlayıcılar gibi çalışır. Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsiniz. -| Model | Model ID | Endpoint | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | Model ID | Endpoint | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5 için yapılandırmanızda `opencode/gpt-5.5` kullanırsınız. @@ -135,8 +129,6 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -146,7 +138,6 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -156,7 +147,6 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -166,9 +156,7 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -177,10 +165,10 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -201,7 +189,7 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1M token başına** fiya | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -Kullanım geçmişinizde Haiku, Nano veya Flash gibi [düşük maliyetli modeller](/docs/config/#models) görebilirsiniz. OpenCode, oturum başlıklarını oluşturmak için bu modelleri kullanır. +Kullanım geçmişinizde _Claude Haiku 3.5_ görebilirsiniz. Bu, oturum başlıklarınızı oluşturmak için kullanılan [düşük maliyetli bir modeldir](/docs/config/#models). :::note Kredi kartı ücretleri maliyet üzerinden yansıtılır (%4.4 + işlem başına $0.30); bunun dışında ekstra bir ücret almıyoruz. @@ -211,8 +199,6 @@ Kredi kartı ücretleri maliyet üzerinden yansıtılır (%4.4 + işlem başına - DeepSeek V4 Flash Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor. - MiMo-V2.5 Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor. -- Laguna S 2.1 Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor. -- Ling-3.0-flash Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor. - North Mini Code Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor. - Nemotron 3 Ultra Free, sınırlı bir süre için OpenCode'da ücretsizdir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor. - Big Pickle, sınırlı bir süre için OpenCode'da ücretsiz olan gizli bir modeldir. Ekip bu süreyi geri bildirim toplamak ve modeli iyileştirmek için kullanıyor. @@ -269,8 +255,6 @@ Tüm modellerimiz US'de barındırılıyor. Sağlayıcılarımız zero-retention - Big Pickle: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir. - DeepSeek V4 Flash Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir. - MiMo-V2.5 Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir. -- Laguna S 2.1 Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir. -- Ling-3.0-flash Free: Ücretsiz döneminde toplanan veriler modeli iyileştirmek için kullanılabilir. - North Mini Code Free: Ücretsiz kullanım süresi boyunca toplanan veriler saklanabilir ve modeli geliştirmek için kullanılabilir. Kişisel veya gizli veriler göndermeyin. [Kullanım Koşullarımıza](https://cohere.com/terms-of-use) ve [Gizlilik Politikamıza](https://cohere.com/privacy) bakın. - Nemotron 3 Ultra Free (ücretsiz NVIDIA uç noktaları): Yalnızca deneme amaçlıdır — kişisel veya gizli veri göndermeyin. Kullanımınız güvenlik amacıyla ve NVIDIA ürünlerini ve hizmetlerini geliştirmek için kaydedilir. Geliştirme amacıyla kaydedilen oturum verileri kimliğinizle veya herhangi bir kalıcı tanımlayıcıyla ilişkilendirilmez. Veri işleme uygulamalarımız hakkında daha fazla bilgi için [Gizlilik Politikamıza](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) bakın. Bu uç noktayla etkileşime geçerek, bu tür bilgileri toplamamıza, kaydetmemize ve kullanmamıza ve [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf) koşullarına onay vermiş olursunuz. - OpenAI APIs: İstekler [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) uyarınca 30 gün boyunca saklanır. diff --git a/packages/web/src/content/docs/zen.mdx b/packages/web/src/content/docs/zen.mdx index 6be3aa7d99..28eb1ee8b3 100644 --- a/packages/web/src/content/docs/zen.mdx +++ b/packages/web/src/content/docs/zen.mdx @@ -58,68 +58,62 @@ You are charged per request and you can add credits to your account. You can also access our models through the following API endpoints. -| Model | Model ID | Endpoint | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Model | Model ID | Endpoint | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-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.5, you would @@ -146,8 +140,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -157,7 +149,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -167,7 +158,6 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -177,9 +167,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -188,10 +176,10 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -212,7 +200,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**. | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -You may notice [low-cost models](/docs/config/#models), such as Haiku, Nano, or Flash, in your usage history. OpenCode uses these models to generate session titles. +You might notice _Claude Haiku 3.5_ in your usage history. This is a [low cost model](/docs/config/#models) that's used to generate the titles of your sessions. :::note Credit card fees are passed along at cost (4.4% + $0.30 per transaction); we don't charge anything beyond that. @@ -222,8 +210,6 @@ The free models: - DeepSeek V4 Flash Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - MiMo-V2.5 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. -- Laguna S 2.1 Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. -- Ling-3.0-flash Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - North Mini Code Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - Nemotron 3 Ultra Free is available on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. - Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model. @@ -283,8 +269,6 @@ All our models are hosted in the US. Our providers follow a zero-retention polic - Big Pickle: During its free period, collected data may be used to improve the model. - DeepSeek V4 Flash Free: During its free period, collected data may be used to improve the model. - MiMo-V2.5 Free: During its free period, collected data may be used to improve the model. -- Laguna S 2.1 Free: During its free period, collected data may be used to improve the model. -- Ling-3.0-flash Free: During its free period, collected data may be used to improve the model. - North Mini Code Free: During its free period, collected data may be retained and used to improve the model. Do not submit personal or confidential data. See our [Terms of Use](https://cohere.com/terms-of-use) and [Privacy Policy](https://cohere.com/privacy). - Nemotron 3 Ultra Free (NVIDIA free endpoints): Trial use only — do not submit personal or confidential data. Your use is logged for security purposes and to improve NVIDIA products and services. The logged session data for improvement purposes is not linked to your identity or any persistent identifier. For more information about our data processing practices, see our [Privacy Policy](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). By interacting with this endpoint, you consent to our collection, recording, and use of such information and the [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf). - OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data). diff --git a/packages/web/src/content/docs/zh-cn/go.mdx b/packages/web/src/content/docs/zh-cn/go.mdx index ecf553f33d..604b910159 100644 --- a/packages/web/src/content/docs/zh-cn/go.mdx +++ b/packages/web/src/content/docs/zh-cn/go.mdx @@ -11,7 +11,7 @@ OpenCode Go 是一项低成本的订阅服务 —— **首月 5 美元**,之 Go 的工作方式与 OpenCode 中的任何其他提供商(provider)一样。订阅 OpenCode Go 后你将获得 API 密钥。它是 **完全可选** 的,并非使用 OpenCode 所必需的条件。 -它主要面向国际用户,并提供稳定的全球访问。 +它主要为国际用户设计,模型托管在美国、欧盟和新加坡,以确保稳定的全球访问。 --- @@ -52,7 +52,6 @@ OpenCode Go 的工作方式与 OpenCode 中的其他提供商一样。 - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ OpenCode Go 的工作方式与 OpenCode 中的其他提供商一样。 - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** 随着我们进行测试和添加新模型,该列表可能会发生变化。 @@ -87,29 +84,25 @@ OpenCode Go 包含以下限制: | Model | 每 5 小时请求数 | 每周请求数 | 每月请求数 | | ----------------- | --------------- | ---------- | ---------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | 预估值基于观察到的请求模式: - Grok 4.5 — 每次请求 1,100 个输入 token,71,500 个缓存 token,220 个输出 token - GLM-5.2/5.1 — 每次请求 700 个输入 token,52,000 个缓存 token,150 个输出 token -- GPT 5.6 Luna — 每次请求 1,000 个输入 token,50,000 个缓存 token,220 个输出 token - Kimi K3 — 每次请求 1,050 个输入 token,76,500 个缓存 token,300 个输出 token - Kimi K2.7/K2.6 — 每次请求 870 个输入 token,55,000 个缓存 token,200 个输出 token - DeepSeek V4 Pro — 每次请求 750 个输入 token,82,000 个缓存 token,290 个输出 token @@ -118,19 +111,15 @@ OpenCode Go 包含以下限制: - MiMo-V2.5-Pro — 每次请求 790 个输入 token,86,000 个缓存 token,305 个输出 token - MiniMax M3 — 每次请求 510 个输入 token,56,000 个缓存 token,190 个输出 token - MiniMax M2.7 — 每次请求 300 个输入 token,55,000 个缓存 token,125 个输出 token -- Qwen3.8 Max — 每次请求 420 个输入 token,66,000 个缓存 token,200 个输出 token - Qwen3.7 Max — 每次请求 420 个输入 token,66,000 个缓存 token,200 个输出 token - Qwen3.7 Plus — 每次请求 500 个输入 token,57,000 个缓存 token,190 个输出 token - Qwen3.6 Plus — 每次请求 500 个输入 token,57,000 个缓存 token,190 个输出 token -- Hy3 — 每次请求 830 个输入 token,71,500 个缓存 token,295 个输出 token 预估值还基于以下每 1M tokens 的价格以及每个模型包含的每月使用额度: | 模型 | 输入 | 输出 | 缓存读取 | 缓存写入 | 使用额度 | | ---------------------------- | ------ | ------ | --------- | -------- | -------- | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ OpenCode Go 包含以下限制: | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 你可以在 **控制台** 中跟踪你当前的使用情况。 @@ -186,7 +173,6 @@ OpenCode Go 包含以下限制: | 模型 | 模型 ID | 端点 | AI SDK 包 | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ OpenCode Go 包含以下限制: | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | 你的 OpenCode 配置中的 [模型 ID](/docs/config/#models) 使用 `opencode-go/` 格式。例如,对于 Kimi K3,你将在配置中使用 `opencode-go/kimi-k3`。 @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## 隐私保护 -| 模型 | 模型训练 | 数据留存 | -| ----------------- | -------- | -------- | -| Grok 4.5 | 不使用 | 30 天 | -| GPT 5.6 Luna | 不使用 | 30 天 | -| GLM-5.2 | 不使用 | 0 天 | -| GLM-5.1 | 不使用 | 0 天 | -| Kimi K3 | 不使用 | 0 天 | -| Kimi K2.7 Code | 不使用 | 0 天 | -| Kimi K2.6 | 不使用 | 0 天 | -| MiMo-V2.5-Pro | 不使用 | 0 天 | -| MiMo-V2.5 | 不使用 | 0 天 | -| Qwen3.8 Max | 不使用 | 0 天 | -| Qwen3.7 Max | 不使用 | 0 天 | -| Qwen3.7 Plus | 不使用 | 0 天 | -| Qwen3.6 Plus | 不使用 | 0 天 | -| MiniMax M3 | 不使用 | 0 天 | -| MiniMax M2.7 | 不使用 | 0 天 | -| DeepSeek V4 Pro | 不使用 | 0 天 | -| DeepSeek V4 Flash | 不使用 | 0 天 | -| Hy3 | 不使用 | 0 天 | - -- **Grok 4.5:** ZDR 会禁用依赖所存储数据的重要 API 功能,包括有状态的 Responses API、Files and Collections 和 Batch API。[了解更多](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr)。 -- **GPT 5.6 Luna:** 所有 API 功能的使用都会生成滥用监控日志,并最多保留 30 天。[了解更多](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring)。 -- **DeepSeek V4 Flash:** ZDR 协议每月续签。当前协议有效期至 2026 年 8 月 31 日。 +该方案主要面向国际用户,模型托管在 US、EU 和 Singapore,以提供稳定的全球访问。我们的提供商遵循零保留政策,不会将您的数据用于模型训练。 --- diff --git a/packages/web/src/content/docs/zh-cn/zen.mdx b/packages/web/src/content/docs/zh-cn/zen.mdx index f4bb9cc502..012d6a5a45 100644 --- a/packages/web/src/content/docs/zh-cn/zen.mdx +++ b/packages/web/src/content/docs/zh-cn/zen.mdx @@ -49,68 +49,62 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。 你也可以通过以下 API 端点访问我们的模型。 -| 模型 | 模型 ID | 端点 | AI SDK 包 | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| 模型 | 模型 ID | 端点 | AI SDK 包 | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | 在你的 OpenCode 配置中,[模型 ID](/docs/config/#models) 使用 `opencode/` 格式。例如,对于 GPT 5.5,你需要在配置中使用 `opencode/gpt-5.5`。 @@ -135,8 +129,6 @@ https://opencode.ai/zen/v1/models | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -146,7 +138,6 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -156,7 +147,6 @@ https://opencode.ai/zen/v1/models | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -166,9 +156,7 @@ https://opencode.ai/zen/v1/models | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -177,10 +165,10 @@ https://opencode.ai/zen/v1/models | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -201,7 +189,7 @@ https://opencode.ai/zen/v1/models | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -你可能会在使用记录中看到 Haiku、Nano 或 Flash 等[低成本模型](/docs/config/#models)。OpenCode 使用这些模型生成会话标题。 +你可能会在使用记录中看到 _Claude Haiku 3.5_。这是一个[低成本模型](/docs/config/#models),用于生成会话标题。 :::note 信用卡手续费按成本转嫁(每笔交易 4.4% + $0.30);除此之外我们不会额外收费。 @@ -211,8 +199,6 @@ https://opencode.ai/zen/v1/models - DeepSeek V4 Flash Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - MiMo-V2.5 Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 -- Laguna S 2.1 Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 -- Ling-3.0-flash Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - North Mini Code Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - Nemotron 3 Ultra Free 目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 - Big Pickle 是一个隐身模型,目前在 OpenCode 上限时免费提供。团队正在利用这段时间收集反馈并改进模型。 @@ -269,8 +255,6 @@ https://opencode.ai/zen/v1/models - Big Pickle:在免费期间,收集的数据可能会被用于改进模型。 - DeepSeek V4 Flash Free:在免费期间,收集的数据可能会被用于改进模型。 - MiMo-V2.5 Free:在免费期间,收集的数据可能会被用于改进模型。 -- Laguna S 2.1 Free:在免费期间,收集的数据可能会被用于改进模型。 -- Ling-3.0-flash Free:在免费期间,收集的数据可能会被用于改进模型。 - North Mini Code Free:免费期间,所收集的数据可能会被保留并用于改进模型。请勿提交个人或机密数据。请参阅我们的[使用条款](https://cohere.com/terms-of-use)和[隐私政策](https://cohere.com/privacy)。 - Nemotron 3 Ultra Free(NVIDIA 免费端点):仅供试用 — 请勿提交个人或机密数据。出于安全目的以及为改进 NVIDIA 产品和服务,系统会记录你的使用情况。出于改进目的而记录的会话数据不会与你的身份或任何持久标识符相关联。有关我们数据处理实践的更多信息,请参阅我们的[隐私政策](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。与此端点进行交互,即表示你同意我们收集、记录和使用此类信息,并同意 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。 - OpenAI APIs:请求会根据 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 保留 30 天。 diff --git a/packages/web/src/content/docs/zh-tw/go.mdx b/packages/web/src/content/docs/zh-tw/go.mdx index 53da06c772..6434504a3e 100644 --- a/packages/web/src/content/docs/zh-tw/go.mdx +++ b/packages/web/src/content/docs/zh-tw/go.mdx @@ -11,7 +11,7 @@ OpenCode Go 是一項低成本的訂閱服務——**首月 $5 美元**,之後 Go 的運作方式與 OpenCode 中的任何其他供應商相同。您訂閱 OpenCode Go 並取得您的 API key。這是**完全可選的**,您不需要使用它也能使用 OpenCode。 -它主要為國際使用者設計,提供穩定的全球存取體驗。 +它主要為國際使用者設計,模型託管於美國、歐盟和新加坡,以提供全球穩定的存取。 --- @@ -52,7 +52,6 @@ OpenCode Go 的運作方式與 OpenCode 中的任何其他供應商相同。 - **Grok 4.5** - **GLM-5.2** - **GLM-5.1** -- **GPT 5.6 Luna** - **Kimi K3** - **Kimi K2.7 Code** - **Kimi K2.6** @@ -60,13 +59,11 @@ OpenCode Go 的運作方式與 OpenCode 中的任何其他供應商相同。 - **MiMo-V2.5-Pro** - **MiniMax M3** - **MiniMax M2.7** -- **Qwen3.8 Max** - **Qwen3.7 Max** - **Qwen3.7 Plus** - **Qwen3.6 Plus** - **DeepSeek V4 Pro** - **DeepSeek V4 Flash** -- **Hy3** 隨著我們測試並加入新模型,模型清單可能會有所變動。 @@ -87,40 +84,34 @@ OpenCode Go 包含以下限制: | Model | 每 5 小時請求數 | 每週請求數 | 每月請求數 | | ----------------- | --------------- | ---------- | ---------- | | Grok 4.5 | 120 | 300 | 600 | -| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 | | GLM-5.2 | 880 | 2,150 | 4,300 | | GLM-5.1 | 880 | 2,150 | 4,300 | | Kimi K3 | 110 | 250 | 490 | -| Kimi K2.7 Code | 1,350 | 3,380 | 6,750 | +| Kimi K2.7 Code | 1,350 | 4,630 | 9,250 | | Kimi K2.6 | 1,150 | 2,880 | 5,750 | | MiMo-V2.5 | 30,100 | 75,200 | 150,400 | | MiMo-V2.5-Pro | 3,250 | 8,150 | 16,300 | | MiniMax M3 | 3,200 | 8,000 | 16,000 | | MiniMax M2.7 | 3,400 | 8,500 | 17,000 | -| Qwen3.8 Max | 160 | 400 | 810 | -| Qwen3.7 Max | 340 | 840 | 1,690 | +| Qwen3.7 Max | 950 | 2,390 | 4,770 | | Qwen3.7 Plus | 4,300 | 10,800 | 21,600 | | Qwen3.6 Plus | 3,300 | 8,200 | 16,300 | | DeepSeek V4 Pro | 3,450 | 8,550 | 17,150 | | DeepSeek V4 Flash | 31,650 | 79,050 | 158,150 | -| Hy3 | 4,300 | 10,750 | 21,500 | 這些預估值是基於觀察到的請求模式: - Grok 4.5 — 每次請求 1,100 個輸入 token、71,500 個快取 token、220 個輸出 token - GLM-5.2/5.1 — 每次請求 700 個輸入 token、52,000 個快取 token、150 個輸出 token -- GPT 5.6 Luna — 每次請求 1,000 個輸入 token、50,000 個快取 token、220 個輸出 token - Kimi K3 — 每次請求 1,050 個輸入 token、76,500 個快取 token、300 個輸出 token - Kimi K2.7/K2.6 — 每次請求 870 個輸入 token、55,000 個快取 token、200 個輸出 token - DeepSeek V4 Pro — 每次請求 750 個輸入 token、82,000 個快取 token、290 個輸出 token - DeepSeek V4 Flash — 每次請求 790 個輸入 token、68,000 個快取 token、280 個輸出 token - MiniMax M3 — 每次請求 510 個輸入 token、56,000 個快取 token、190 個輸出 token - MiniMax M2.7 — 每次請求 300 個輸入 token、55,000 個快取 token、125 個輸出 token -- Qwen3.8 Max — 每次請求 420 個輸入 token、66,000 個快取 token、200 個輸出 token - Qwen3.7 Max — 每次請求 420 個輸入 token、66,000 個快取 token、200 個輸出 token - Qwen3.7 Plus — 每次請求 500 個輸入 token、57,000 個快取 token、190 個輸出 token - Qwen3.6 Plus — 每次請求 500 個輸入 token、57,000 個快取 token、190 個輸出 token -- Hy3 — 每次請求 830 個輸入 token、71,500 個快取 token、295 個輸出 token - MiMo-V2.5 — 每次請求 830 個輸入 token、71,500 個快取 token、295 個輸出 token - MiMo-V2.5-Pro — 每次請求 790 個輸入 token、86,000 個快取 token、305 個輸出 token @@ -129,8 +120,6 @@ OpenCode Go 包含以下限制: | 模型 | 輸入 | 輸出 | 快取讀取 | 快取寫入 | 使用量 | | ---------------------------- | ------ | ------ | --------- | -------- | ------ | | Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 | | GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 | | GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 | | Kimi K3 | $3.00 | $15.00 | $0.30 | - | $15 | @@ -141,7 +130,6 @@ OpenCode Go 包含以下限制: | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | $60 | | MiniMax M2.7 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | | MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 | $60 | -| Qwen3.8 Max | $2.00 | $6.00 | $0.25 | $2.50 | $15 | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | $60 | | Qwen3.7 Plus (≤ 256K tokens) | $0.40 | $1.60 | $0.04 | $0.50 | $60 | | Qwen3.7 Plus (> 256K tokens) | $1.20 | $4.80 | $0.12 | $1.50 | $60 | @@ -149,7 +137,6 @@ OpenCode Go 包含以下限制: | Qwen3.6 Plus (> 256K tokens) | $2.00 | $6.00 | $0.20 | $2.50 | $60 | | DeepSeek V4 Pro | $0.435 | $0.87 | $0.003625 | - | $15 | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.0028 | - | $60 | -| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 | 您可以在 **console** 中追蹤您目前的使用量。 @@ -186,7 +173,6 @@ OpenCode Go 包含以下限制: | 模型 | 模型 ID | 端點 | AI SDK 套件 | | ----------------- | ----------------- | ------------------------------------------------ | --------------------------- | | Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` | | GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | | Kimi K3 | kimi-k3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | @@ -199,11 +185,9 @@ OpenCode Go 包含以下限制: | MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | 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.8 Max | qwen3.8-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | | Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` | -| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` | 您的 OpenCode 設定中的 [model id](/docs/config/#models) 使用 `opencode-go/` 格式。例如,Kimi K3 在設定中應使用 `opencode-go/kimi-k3`。 @@ -221,30 +205,7 @@ https://opencode.ai/zen/go/v1/models ## 隱私權 -| 模型 | 模型訓練 | 資料保留 | -| ----------------- | -------- | -------- | -| Grok 4.5 | 不使用 | 30 天 | -| GPT 5.6 Luna | 不使用 | 30 天 | -| GLM-5.2 | 不使用 | 0 天 | -| GLM-5.1 | 不使用 | 0 天 | -| Kimi K3 | 不使用 | 0 天 | -| Kimi K2.7 Code | 不使用 | 0 天 | -| Kimi K2.6 | 不使用 | 0 天 | -| MiMo-V2.5-Pro | 不使用 | 0 天 | -| MiMo-V2.5 | 不使用 | 0 天 | -| Qwen3.8 Max | 不使用 | 0 天 | -| Qwen3.7 Max | 不使用 | 0 天 | -| Qwen3.7 Plus | 不使用 | 0 天 | -| Qwen3.6 Plus | 不使用 | 0 天 | -| MiniMax M3 | 不使用 | 0 天 | -| MiniMax M2.7 | 不使用 | 0 天 | -| DeepSeek V4 Pro | 不使用 | 0 天 | -| DeepSeek V4 Flash | 不使用 | 0 天 | -| Hy3 | 不使用 | 0 天 | - -- **Grok 4.5:** ZDR 會停用依賴儲存資料的重要 API 功能,包括具狀態的 Responses API、Files and Collections 與 Batch API。[了解更多](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr)。 -- **GPT 5.6 Luna:** 所有 API 功能的使用都會產生濫用監控日誌,並保留最多 30 天。[了解更多](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring)。 -- **DeepSeek V4 Flash:** ZDR 協議每月續簽。目前的協議有效至 2026 年 8 月 31 日。 +此方案主要為國際使用者設計,模型部署於 US、EU 與 Singapore,以提供穩定的全球存取體驗。我們的供應商遵循零保留政策,不會將你的資料用於模型訓練。 --- diff --git a/packages/web/src/content/docs/zh-tw/zen.mdx b/packages/web/src/content/docs/zh-tw/zen.mdx index 60b87c88df..7a394d2149 100644 --- a/packages/web/src/content/docs/zh-tw/zen.mdx +++ b/packages/web/src/content/docs/zh-tw/zen.mdx @@ -53,68 +53,62 @@ OpenCode Zen 的運作方式和 OpenCode 中的其他供應商一樣。 你也可以透過以下 API 端點存取我們的模型。 -| 模型 | Model ID | 端點 | AI SDK Package | -| ---------------------- | ---------------------- | --------------------------------------------------------- | --------------------------- | -| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| 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` | -| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 5 | claude-opus-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Gemini 3.6 Flash | gemini-3.6-flash | `https://opencode.ai/zen/v1/models/gemini-3.6-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | -| Gemini 3.5 Flash Lite | gemini-3.5-flash-lite | `https://opencode.ai/zen/v1/models/gemini-3.5-flash-lite` | `@ai-sdk/google` | -| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | -| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | -| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | -| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | -| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Laguna S 2.1 Free | laguna-s-2.1-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Ling-3.0-flash Free | ling-3.0-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | -| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| 模型 | Model ID | 端點 | AI SDK Package | +| ---------------------- | ---------------------- | ---------------------------------------------------- | --------------------------- | +| GPT 5.6 Sol | gpt-5.6-sol | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Terra | gpt-5.6-terra | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| 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` | +| GPT 5.4 Nano | gpt-5.4-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` | +| Claude Fable 5 | claude-fable-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.8 | claude-opus-4-8 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.7 | claude-opus-4-7 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 5 | claude-sonnet-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Gemini 3.5 Flash | gemini-3.5-flash | `https://opencode.ai/zen/v1/models/gemini-3.5-flash` | `@ai-sdk/google` | +| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` | +| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` | +| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Grok Build 0.1 | grok-build-0.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Qwen3.7 Max | qwen3.7-max | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| Qwen3.5 Plus | qwen3.5-plus | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` | +| DeepSeek V4 Pro | deepseek-v4-pro | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash | deepseek-v4-flash | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M3 | minimax-m3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.7 | minimax-m2.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.2 | glm-5.2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5.1 | glm-5.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.6 | kimi-k2.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| North Mini Code Free | north-mini-code-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | +| DeepSeek V4 Flash Free | deepseek-v4-flash-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` | OpenCode 設定中的 [模型 ID](/docs/config/#models) 會使用 `opencode/` 格式。例如,如果是 GPT 5.5,你會在設定中使用 `opencode/gpt-5.5`。 @@ -140,8 +134,6 @@ https://opencode.ai/zen/v1/models | Big Pickle | Free | Free | Free | - | | DeepSeek V4 Flash Free | Free | Free | Free | - | | MiMo-V2.5 Free | Free | Free | Free | - | -| Laguna S 2.1 Free | Free | Free | Free | - | -| Ling-3.0-flash Free | Free | Free | Free | - | | North Mini Code Free | Free | Free | Free | - | | Nemotron 3 Ultra Free | Free | Free | Free | - | | MiniMax M3 | $0.30 | $1.20 | $0.06 | - | @@ -151,7 +143,6 @@ https://opencode.ai/zen/v1/models | GLM 5.1 | $1.40 | $4.40 | $0.26 | - | | GLM 5 | $1.00 | $3.20 | $0.20 | - | | Kimi K2.7 Code | $0.95 | $4.00 | $0.19 | - | -| Kimi K3 | $3.00 | $15.00 | $0.30 | - | | Kimi K2.6 | $0.95 | $4.00 | $0.16 | - | | Kimi K2.5 | $0.60 | $3.00 | $0.10 | - | | Qwen3.7 Max | $2.50 | $7.50 | $0.50 | $3.125 | @@ -161,7 +152,6 @@ https://opencode.ai/zen/v1/models | DeepSeek V4 Pro | $1.74 | $3.48 | $0.145 | - | | DeepSeek V4 Flash | $0.14 | $0.28 | $0.028 | - | | Claude Fable 5 | $10.00 | $50.00 | $1.00 | $12.50 | -| Claude Opus 5 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.8 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.7 | $5.00 | $25.00 | $0.50 | $6.25 | | Claude Opus 4.6 | $5.00 | $25.00 | $0.50 | $6.25 | @@ -171,9 +161,7 @@ https://opencode.ai/zen/v1/models | Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 | | Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 | | Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 | -| Gemini 3.6 Flash | $1.50 | $7.50 | $0.15 | - | | Gemini 3.5 Flash | $1.50 | $9.00 | $0.15 | - | -| Gemini 3.5 Flash Lite | $0.30 | $2.50 | $0.03 | - | | 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 | - | @@ -182,10 +170,10 @@ https://opencode.ai/zen/v1/models | Grok Build 0.1 | $1.00 | $2.00 | $0.20 | - | | GPT 5.6 Sol (≤ 272K tokens) | $5.00 | $30.00 | $0.50 | $6.25 | | GPT 5.6 Sol (> 272K tokens) | $10.00 | $45.00 | $1.00 | $12.50 | -| GPT 5.6 Terra (≤ 272K tokens) | $2.00 | $12.00 | $0.20 | $2.50 | -| GPT 5.6 Terra (> 272K tokens) | $4.00 | $18.00 | $0.40 | $5.00 | -| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | -| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | +| GPT 5.6 Terra (≤ 272K tokens) | $2.50 | $15.00 | $0.25 | $3.125 | +| GPT 5.6 Terra (> 272K tokens) | $5.00 | $22.50 | $0.50 | $6.25 | +| GPT 5.6 Luna (≤ 272K tokens) | $1.00 | $6.00 | $0.10 | $1.25 | +| GPT 5.6 Luna (> 272K tokens) | $2.00 | $9.00 | $0.20 | $2.50 | | 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 | - | @@ -206,7 +194,8 @@ https://opencode.ai/zen/v1/models | GPT 5 Codex | $1.07 | $8.50 | $0.107 | - | | GPT 5 Nano | $0.05 | $0.40 | $0.005 | - | -你可能會在使用紀錄中看到 Haiku、Nano 或 Flash 等[低成本模型](/docs/config/#models)。OpenCode 使用這些模型產生工作階段標題。 +你可能會在使用紀錄中看到 _Claude Haiku 3.5_。這是一個[低成本模型](/docs/config/#models), +會用來產生工作階段的標題。 :::note 信用卡手續費會以成本轉嫁(每筆交易 4.4% + $0.30);除此之外,我們不會額外收費。 @@ -216,8 +205,6 @@ https://opencode.ai/zen/v1/models - DeepSeek V4 Flash Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。 - MiMo-V2.5 Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。 -- Laguna S 2.1 Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。 -- Ling-3.0-flash Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。 - North Mini Code Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。 - Nemotron 3 Ultra Free 在 OpenCode 上限時提供。團隊正在利用這段時間收集回饋並改進模型。 - Big Pickle 是一個隱身模型,在 OpenCode 上限時免費提供。團隊正在利用這段時間收集回饋並改進模型。 @@ -275,8 +262,6 @@ https://opencode.ai/zen/v1/models - Big Pickle: 在免費期間,收集到的資料可能會用於改進模型。 - DeepSeek V4 Flash Free: 在免費期間,收集到的資料可能會用於改進模型。 - MiMo-V2.5 Free: 在免費期間,收集到的資料可能會用於改進模型。 -- Laguna S 2.1 Free: 在免費期間,收集到的資料可能會用於改進模型。 -- Ling-3.0-flash Free: 在免費期間,收集到的資料可能會用於改進模型。 - North Mini Code Free:免費期間,所收集的資料可能會被保留並用於改進模型。請勿提交個人或機密資料。請參閱我們的[使用條款](https://cohere.com/terms-of-use)和[隱私權政策](https://cohere.com/privacy)。 - Nemotron 3 Ultra Free(NVIDIA 免費端點):僅供試用 — 請勿提交個人或機密資料。基於安全目的以及為了改進 NVIDIA 產品與服務,系統會記錄你的使用情況。基於改進目的而記錄的工作階段資料不會與你的身分或任何持久識別碼相關聯。有關我們資料處理實務的更多資訊,請參閱我們的[隱私政策](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。與此端點進行互動,即表示你同意我們收集、記錄與使用此類資訊,並同意 [NVIDIA API Trial Terms of Service](https://assets.ngc.nvidia.com/products/api-catalog/legal/NVIDIA%20API%20Trial%20Terms%20of%20Service.pdf)。 - OpenAI APIs: 請求會依據 [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data) 保留 30 天。 diff --git a/patches/@ai-sdk%2Fmistral@3.0.51.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch deleted file mode 100644 index 141b14a689..0000000000 --- a/patches/@ai-sdk%2Fmistral@3.0.51.patch +++ /dev/null @@ -1,709 +0,0 @@ -diff --git a/dist/index.d.mts b/dist/index.d.mts -index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 ---- a/dist/index.d.mts -+++ b/dist/index.d.mts -@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ - none: "none"; - high: "high"; - }>>; -+ promptCacheKey: z.ZodOptional; - }, z.core.$strip>; - type MistralLanguageModelOptions = z.infer; - -diff --git a/dist/index.d.ts b/dist/index.d.ts -index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 ---- a/dist/index.d.ts -+++ b/dist/index.d.ts -@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ - none: "none"; - high: "high"; - }>>; -+ promptCacheKey: z.ZodOptional; - }, z.core.$strip>; - type MistralLanguageModelOptions = z.infer; - -diff --git a/dist/index.js b/dist/index.js -index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..267f34e20ea392b7a85ad5259d72d50605a6f971 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -128,11 +128,14 @@ function convertToMistralChatMessages(prompt) { - } - case "assistant": { - let text = ""; -+ const structuredContent = []; -+ let hasNativeReasoning = false; - const toolCalls = []; - for (const part of content) { - switch (part.type) { - case "text": { - text += part.text; -+ structuredContent.push({ type: "text", text: part.text }); - break; - } - case "tool-call": { -@@ -148,6 +151,13 @@ function convertToMistralChatMessages(prompt) { - } - case "reasoning": { - text += part.text; -+ const native = part.providerOptions?.mistral?.thinking; -+ if (native?.type === "thinking") { -+ hasNativeReasoning = true; -+ structuredContent.push(native); -+ break; -+ } -+ structuredContent.push({ type: "text", text: part.text }); - break; - } - default: { -@@ -159,7 +169,7 @@ function convertToMistralChatMessages(prompt) { - } - messages.push({ - role: "assistant", -- content: text, -+ content: hasNativeReasoning ? structuredContent : text, - prefix: isLastMessage ? true : void 0, - tool_calls: toolCalls.length > 0 ? toolCalls : void 0 - }); -@@ -268,7 +278,8 @@ var mistralLanguageModelOptions = import_v4.z.object({ - * - `'high'`: Enable reasoning - * - `'none'`: Disable reasoning - */ -- reasoningEffort: import_v4.z.enum(["high", "none"]).optional() -+ reasoningEffort: import_v4.z.enum(["high", "none"]).optional(), -+ promptCacheKey: import_v4.z.string().optional() - }); - - // src/mistral-error.ts -@@ -407,6 +418,7 @@ var MistralChatLanguageModel = class { - stop: stopSequences, - random_seed: seed, - reasoning_effort: options.reasoningEffort, -+ prompt_cache_key: options.promptCacheKey, - // response format: - response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { - type: "json_schema", -@@ -465,9 +477,11 @@ var MistralChatLanguageModel = class { - for (const part of choice.message.content) { - if (part.type === "thinking") { - const reasoningText = extractReasoningContent(part.thinking); -- if (reasoningText.length > 0) { -- content.push({ type: "reasoning", text: reasoningText }); -- } -+ content.push({ -+ type: "reasoning", -+ text: reasoningText, -+ providerMetadata: { mistral: { thinking: part } } -+ }); - } else if (part.type === "text") { - if (part.text.length > 0) { - content.push({ type: "text", text: part.text }); -@@ -528,6 +542,7 @@ var MistralChatLanguageModel = class { - let isFirstChunk = true; - let activeText = false; - let activeReasoningId = null; -+ let activeThinking = null; - const generateId2 = this.generateId; - return { - stream: response.pipeThrough( -@@ -561,18 +576,19 @@ var MistralChatLanguageModel = class { - for (const part of delta.content) { - if (part.type === "thinking") { - const reasoningDelta = extractReasoningContent(part.thinking); -- if (reasoningDelta.length > 0) { -- if (activeReasoningId == null) { -- if (activeText) { -- controller.enqueue({ type: "text-end", id: "0" }); -- activeText = false; -- } -- activeReasoningId = generateId2(); -- controller.enqueue({ -- type: "reasoning-start", -- id: activeReasoningId -- }); -+ activeThinking = mergeThinking(activeThinking, part); -+ if (activeReasoningId == null) { -+ if (activeText) { -+ controller.enqueue({ type: "text-end", id: "0" }); -+ activeText = false; - } -+ activeReasoningId = generateId2(); -+ controller.enqueue({ -+ type: "reasoning-start", -+ id: activeReasoningId -+ }); -+ } -+ if (reasoningDelta.length > 0) { - controller.enqueue({ - type: "reasoning-delta", - id: activeReasoningId, -@@ -587,9 +603,11 @@ var MistralChatLanguageModel = class { - if (activeReasoningId != null) { - controller.enqueue({ - type: "reasoning-end", -- id: activeReasoningId -+ id: activeReasoningId, -+ providerMetadata: { mistral: { thinking: activeThinking } } - }); - activeReasoningId = null; -+ activeThinking = null; - } - controller.enqueue({ type: "text-start", id: "0" }); - activeText = true; -@@ -638,7 +656,8 @@ var MistralChatLanguageModel = class { - if (activeReasoningId != null) { - controller.enqueue({ - type: "reasoning-end", -- id: activeReasoningId -+ id: activeReasoningId, -+ providerMetadata: { mistral: { thinking: activeThinking } } - }); - } - if (activeText) { -@@ -660,6 +679,13 @@ var MistralChatLanguageModel = class { - function extractReasoningContent(thinking) { - return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); - } -+function mergeThinking(current, next) { -+ if (current === null) return { ...next, thinking: [...next.thinking] }; -+ current.thinking.push(...next.thinking); -+ if (next.closed !== void 0) current.closed = next.closed; -+ if (next.signature !== void 0) current.signature = next.signature; -+ return current; -+} - function extractTextContent(content) { - if (typeof content === "string") { - return content; -@@ -686,6 +712,30 @@ function extractTextContent(content) { - } - return textContent.length ? textContent.join("") : void 0; - } -+var mistralThinkingContentSchema = import_v43.z.discriminatedUnion("type", [ -+ import_v43.z.object({ -+ type: import_v43.z.literal("text"), -+ text: import_v43.z.string() -+ }), -+ import_v43.z.object({ -+ type: import_v43.z.literal("tool_reference"), -+ tool: import_v43.z.string(), -+ title: import_v43.z.string(), -+ url: import_v43.z.string().nullish(), -+ favicon: import_v43.z.string().nullish(), -+ description: import_v43.z.string().nullish() -+ }), -+ import_v43.z.object({ -+ type: import_v43.z.literal("reference"), -+ reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number().int()])) -+ }) -+]); -+var mistralThinkChunkSchema = import_v43.z.object({ -+ type: import_v43.z.literal("thinking"), -+ thinking: import_v43.z.array(mistralThinkingContentSchema), -+ closed: import_v43.z.boolean().optional(), -+ signature: import_v43.z.string().nullish() -+}); - var mistralContentSchema = import_v43.z.union([ - import_v43.z.string(), - import_v43.z.array( -@@ -708,15 +758,7 @@ var mistralContentSchema = import_v43.z.union([ - type: import_v43.z.literal("reference"), - reference_ids: import_v43.z.array(import_v43.z.union([import_v43.z.string(), import_v43.z.number()])) - }), -- import_v43.z.object({ -- type: import_v43.z.literal("thinking"), -- thinking: import_v43.z.array( -- import_v43.z.object({ -- type: import_v43.z.literal("text"), -- text: import_v43.z.string() -- }) -- ) -- }) -+ mistralThinkChunkSchema - ]) - ) - ]).nullish(); -diff --git a/dist/index.mjs b/dist/index.mjs -index d2eff622c1b84a96bdeb4012cb0206a33012a04d..3bff11ddd6136ada45809568828cbc8f2493a42a 100644 ---- a/dist/index.mjs -+++ b/dist/index.mjs -@@ -116,11 +116,14 @@ function convertToMistralChatMessages(prompt) { - } - case "assistant": { - let text = ""; -+ const structuredContent = []; -+ let hasNativeReasoning = false; - const toolCalls = []; - for (const part of content) { - switch (part.type) { - case "text": { - text += part.text; -+ structuredContent.push({ type: "text", text: part.text }); - break; - } - case "tool-call": { -@@ -136,6 +139,13 @@ function convertToMistralChatMessages(prompt) { - } - case "reasoning": { - text += part.text; -+ const native = part.providerOptions?.mistral?.thinking; -+ if (native?.type === "thinking") { -+ hasNativeReasoning = true; -+ structuredContent.push(native); -+ break; -+ } -+ structuredContent.push({ type: "text", text: part.text }); - break; - } - default: { -@@ -147,7 +157,7 @@ function convertToMistralChatMessages(prompt) { - } - messages.push({ - role: "assistant", -- content: text, -+ content: hasNativeReasoning ? structuredContent : text, - prefix: isLastMessage ? true : void 0, - tool_calls: toolCalls.length > 0 ? toolCalls : void 0 - }); -@@ -256,7 +266,8 @@ var mistralLanguageModelOptions = z.object({ - * - `'high'`: Enable reasoning - * - `'none'`: Disable reasoning - */ -- reasoningEffort: z.enum(["high", "none"]).optional() -+ reasoningEffort: z.enum(["high", "none"]).optional(), -+ promptCacheKey: z.string().optional() - }); - - // src/mistral-error.ts -@@ -397,6 +408,7 @@ var MistralChatLanguageModel = class { - stop: stopSequences, - random_seed: seed, - reasoning_effort: options.reasoningEffort, -+ prompt_cache_key: options.promptCacheKey, - // response format: - response_format: (responseFormat == null ? void 0 : responseFormat.type) === "json" ? structuredOutputs && (responseFormat == null ? void 0 : responseFormat.schema) != null ? { - type: "json_schema", -@@ -455,9 +467,11 @@ var MistralChatLanguageModel = class { - for (const part of choice.message.content) { - if (part.type === "thinking") { - const reasoningText = extractReasoningContent(part.thinking); -- if (reasoningText.length > 0) { -- content.push({ type: "reasoning", text: reasoningText }); -- } -+ content.push({ -+ type: "reasoning", -+ text: reasoningText, -+ providerMetadata: { mistral: { thinking: part } } -+ }); - } else if (part.type === "text") { - if (part.text.length > 0) { - content.push({ type: "text", text: part.text }); -@@ -518,6 +532,7 @@ var MistralChatLanguageModel = class { - let isFirstChunk = true; - let activeText = false; - let activeReasoningId = null; -+ let activeThinking = null; - const generateId2 = this.generateId; - return { - stream: response.pipeThrough( -@@ -551,18 +566,19 @@ var MistralChatLanguageModel = class { - for (const part of delta.content) { - if (part.type === "thinking") { - const reasoningDelta = extractReasoningContent(part.thinking); -- if (reasoningDelta.length > 0) { -- if (activeReasoningId == null) { -- if (activeText) { -- controller.enqueue({ type: "text-end", id: "0" }); -- activeText = false; -- } -- activeReasoningId = generateId2(); -- controller.enqueue({ -- type: "reasoning-start", -- id: activeReasoningId -- }); -+ activeThinking = mergeThinking(activeThinking, part); -+ if (activeReasoningId == null) { -+ if (activeText) { -+ controller.enqueue({ type: "text-end", id: "0" }); -+ activeText = false; - } -+ activeReasoningId = generateId2(); -+ controller.enqueue({ -+ type: "reasoning-start", -+ id: activeReasoningId -+ }); -+ } -+ if (reasoningDelta.length > 0) { - controller.enqueue({ - type: "reasoning-delta", - id: activeReasoningId, -@@ -577,9 +593,11 @@ var MistralChatLanguageModel = class { - if (activeReasoningId != null) { - controller.enqueue({ - type: "reasoning-end", -- id: activeReasoningId -+ id: activeReasoningId, -+ providerMetadata: { mistral: { thinking: activeThinking } } - }); - activeReasoningId = null; -+ activeThinking = null; - } - controller.enqueue({ type: "text-start", id: "0" }); - activeText = true; -@@ -628,7 +646,8 @@ var MistralChatLanguageModel = class { - if (activeReasoningId != null) { - controller.enqueue({ - type: "reasoning-end", -- id: activeReasoningId -+ id: activeReasoningId, -+ providerMetadata: { mistral: { thinking: activeThinking } } - }); - } - if (activeText) { -@@ -650,6 +669,13 @@ var MistralChatLanguageModel = class { - function extractReasoningContent(thinking) { - return thinking.filter((chunk) => chunk.type === "text").map((chunk) => chunk.text).join(""); - } -+function mergeThinking(current, next) { -+ if (current === null) return { ...next, thinking: [...next.thinking] }; -+ current.thinking.push(...next.thinking); -+ if (next.closed !== void 0) current.closed = next.closed; -+ if (next.signature !== void 0) current.signature = next.signature; -+ return current; -+} - function extractTextContent(content) { - if (typeof content === "string") { - return content; -@@ -676,6 +702,30 @@ function extractTextContent(content) { - } - return textContent.length ? textContent.join("") : void 0; - } -+var mistralThinkingContentSchema = z3.discriminatedUnion("type", [ -+ z3.object({ -+ type: z3.literal("text"), -+ text: z3.string() -+ }), -+ z3.object({ -+ type: z3.literal("tool_reference"), -+ tool: z3.string(), -+ title: z3.string(), -+ url: z3.string().nullish(), -+ favicon: z3.string().nullish(), -+ description: z3.string().nullish() -+ }), -+ z3.object({ -+ type: z3.literal("reference"), -+ reference_ids: z3.array(z3.union([z3.string(), z3.number().int()])) -+ }) -+]); -+var mistralThinkChunkSchema = z3.object({ -+ type: z3.literal("thinking"), -+ thinking: z3.array(mistralThinkingContentSchema), -+ closed: z3.boolean().optional(), -+ signature: z3.string().nullish() -+}); - var mistralContentSchema = z3.union([ - z3.string(), - z3.array( -@@ -698,15 +748,7 @@ var mistralContentSchema = z3.union([ - type: z3.literal("reference"), - reference_ids: z3.array(z3.union([z3.string(), z3.number()])) - }), -- z3.object({ -- type: z3.literal("thinking"), -- thinking: z3.array( -- z3.object({ -- type: z3.literal("text"), -- text: z3.string() -- }) -- ) -- }) -+ mistralThinkChunkSchema - ]) - ) - ]).nullish(); -diff --git a/src/convert-to-mistral-chat-messages.ts b/src/convert-to-mistral-chat-messages.ts -index 3c6914f8da615d7517bc43dd56198298d0a50247..8cd6f4c7577f746ef41e8a0aee682234c473667a 100644 ---- a/src/convert-to-mistral-chat-messages.ts -+++ b/src/convert-to-mistral-chat-messages.ts -@@ -3,7 +3,11 @@ import { - type LanguageModelV3DataContent, - type LanguageModelV3Prompt, - } from '@ai-sdk/provider'; --import type { MistralPrompt } from './mistral-chat-prompt'; -+import type { -+ MistralAssistantMessageContent, -+ MistralPrompt, -+ MistralThinkChunk, -+} from './mistral-chat-prompt'; - import { convertToBase64 } from '@ai-sdk/provider-utils'; - - function formatFileUrl({ -@@ -76,6 +80,8 @@ export function convertToMistralChatMessages( - - case 'assistant': { - let text = ''; -+ const structuredContent: Array = []; -+ let hasNativeReasoning = false; - const toolCalls: Array<{ - id: string; - type: 'function'; -@@ -86,6 +92,7 @@ export function convertToMistralChatMessages( - switch (part.type) { - case 'text': { - text += part.text; -+ structuredContent.push({ type: 'text', text: part.text }); - break; - } - case 'tool-call': { -@@ -101,6 +108,14 @@ export function convertToMistralChatMessages( - } - case 'reasoning': { - text += part.text; -+ const native = part.providerOptions?.mistral -+ ?.thinking as MistralThinkChunk | undefined; -+ if (native?.type === 'thinking') { -+ hasNativeReasoning = true; -+ structuredContent.push(native); -+ break; -+ } -+ structuredContent.push({ type: 'text', text: part.text }); - break; - } - default: { -@@ -113,7 +128,7 @@ export function convertToMistralChatMessages( - - messages.push({ - role: 'assistant', -- content: text, -+ content: hasNativeReasoning ? structuredContent : text, - prefix: isLastMessage ? true : undefined, - tool_calls: toolCalls.length > 0 ? toolCalls : undefined, - }); -diff --git a/src/mistral-chat-language-model.ts b/src/mistral-chat-language-model.ts -index 7e4a7ab552f1b41b7074e1b3cada8a51d791268d..847d26f9dfe03572a969a122f8c96b8bbfda8066 100644 ---- a/src/mistral-chat-language-model.ts -+++ b/src/mistral-chat-language-model.ts -@@ -122,6 +122,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - stop: stopSequences, - random_seed: seed, - reasoning_effort: options.reasoningEffort, -+ prompt_cache_key: options.promptCacheKey, - - // response format: - response_format: -@@ -201,9 +202,11 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - for (const part of choice.message.content) { - if (part.type === 'thinking') { - const reasoningText = extractReasoningContent(part.thinking); -- if (reasoningText.length > 0) { -- content.push({ type: 'reasoning', text: reasoningText }); -- } -+ content.push({ -+ type: 'reasoning', -+ text: reasoningText, -+ providerMetadata: { mistral: { thinking: part } }, -+ }); - } else if (part.type === 'text') { - if (part.text.length > 0) { - content.push({ type: 'text', text: part.text }); -@@ -278,6 +281,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - let isFirstChunk = true; - let activeText = false; - let activeReasoningId: string | null = null; -+ let activeThinking: z.infer | null = null; - - const generateId = this.generateId; - -@@ -326,20 +330,21 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - for (const part of delta.content) { - if (part.type === 'thinking') { - const reasoningDelta = extractReasoningContent(part.thinking); -- if (reasoningDelta.length > 0) { -- if (activeReasoningId == null) { -- // end any active text before starting reasoning -- if (activeText) { -- controller.enqueue({ type: 'text-end', id: '0' }); -- activeText = false; -- } -- -- activeReasoningId = generateId(); -- controller.enqueue({ -- type: 'reasoning-start', -- id: activeReasoningId, -- }); -+ activeThinking = mergeThinking(activeThinking, part); -+ if (activeReasoningId == null) { -+ // end any active text before starting reasoning -+ if (activeText) { -+ controller.enqueue({ type: 'text-end', id: '0' }); -+ activeText = false; - } -+ -+ activeReasoningId = generateId(); -+ controller.enqueue({ -+ type: 'reasoning-start', -+ id: activeReasoningId, -+ }); -+ } -+ if (reasoningDelta.length > 0) { - controller.enqueue({ - type: 'reasoning-delta', - id: activeReasoningId, -@@ -357,8 +362,12 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - controller.enqueue({ - type: 'reasoning-end', - id: activeReasoningId, -+ providerMetadata: { -+ mistral: { thinking: activeThinking }, -+ }, - }); - activeReasoningId = null; -+ activeThinking = null; - } - controller.enqueue({ type: 'text-start', id: '0' }); - activeText = true; -@@ -416,6 +425,9 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - controller.enqueue({ - type: 'reasoning-end', - id: activeReasoningId, -+ providerMetadata: { -+ mistral: { thinking: activeThinking }, -+ }, - }); - } - if (activeText) { -@@ -437,7 +449,7 @@ export class MistralChatLanguageModel implements LanguageModelV3 { - } - - function extractReasoningContent( -- thinking: Array<{ type: string; text: string }>, -+ thinking: Array>, - ) { - return thinking - .filter(chunk => chunk.type === 'text') -@@ -445,6 +457,17 @@ function extractReasoningContent( - .join(''); - } - -+function mergeThinking( -+ current: z.infer | null, -+ next: z.infer, -+) { -+ if (current === null) return { ...next, thinking: [...next.thinking] }; -+ current.thinking.push(...next.thinking); -+ if (next.closed !== undefined) current.closed = next.closed; -+ if (next.signature !== undefined) current.signature = next.signature; -+ return current; -+} -+ - function extractTextContent(content: z.infer) { - if (typeof content === 'string') { - return content; -@@ -478,6 +501,32 @@ function extractTextContent(content: z.infer) { - return textContent.length ? textContent.join('') : undefined; - } - -+const mistralThinkingContentSchema = z.discriminatedUnion('type', [ -+ z.object({ -+ type: z.literal('text'), -+ text: z.string(), -+ }), -+ z.object({ -+ type: z.literal('tool_reference'), -+ tool: z.string(), -+ title: z.string(), -+ url: z.string().nullish(), -+ favicon: z.string().nullish(), -+ description: z.string().nullish(), -+ }), -+ z.object({ -+ type: z.literal('reference'), -+ reference_ids: z.array(z.union([z.string(), z.number().int()])), -+ }), -+]); -+ -+const mistralThinkChunkSchema = z.object({ -+ type: z.literal('thinking'), -+ thinking: z.array(mistralThinkingContentSchema), -+ closed: z.boolean().optional(), -+ signature: z.string().nullish(), -+}); -+ - const mistralContentSchema = z - .union([ - z.string(), -@@ -501,15 +550,7 @@ const mistralContentSchema = z - type: z.literal('reference'), - reference_ids: z.array(z.union([z.string(), z.number()])), - }), -- z.object({ -- type: z.literal('thinking'), -- thinking: z.array( -- z.object({ -- type: z.literal('text'), -- text: z.string(), -- }), -- ), -- }), -+ mistralThinkChunkSchema, - ]), - ), - ]) -diff --git a/src/mistral-chat-options.ts b/src/mistral-chat-options.ts -index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9ada05d11 100644 ---- a/src/mistral-chat-options.ts -+++ b/src/mistral-chat-options.ts -@@ -64,6 +64,11 @@ export const mistralLanguageModelOptions = z.object({ - * - `'none'`: Disable reasoning - */ - reasoningEffort: z.enum(['high', 'none']).optional(), -+ -+ /** -+ * A stable identifier used to route requests with shared prompt prefixes. -+ */ -+ promptCacheKey: z.string().optional(), - }); - - export type MistralLanguageModelOptions = z.infer< -diff --git a/src/mistral-chat-prompt.ts b/src/mistral-chat-prompt.ts -index 13f1dced55ac4be084128127a57fbdd58115bc28..172b11dde3dd326c2f3befd99237474ed8c79285 100644 ---- a/src/mistral-chat-prompt.ts -+++ b/src/mistral-chat-prompt.ts -@@ -23,7 +23,7 @@ export type MistralUserMessageContent = - - export interface MistralAssistantMessage { - role: 'assistant'; -- content: string; -+ content: string | Array; - prefix?: boolean; - tool_calls?: Array<{ - id: string; -@@ -32,6 +32,29 @@ export interface MistralAssistantMessage { - }>; - } - -+export type MistralAssistantMessageContent = -+ | { type: 'text'; text: string } -+ | MistralThinkChunk; -+ -+export type MistralThinkChunk = { -+ type: 'thinking'; -+ thinking: Array; -+ closed?: boolean; -+ signature?: string | null; -+}; -+ -+export type MistralThinkingContent = -+ | { type: 'text'; text: string } -+ | { -+ type: 'tool_reference'; -+ tool: string; -+ title: string; -+ url?: string | null; -+ favicon?: string | null; -+ description?: string | null; -+ } -+ | { type: 'reference'; reference_ids: Array }; -+ - export interface MistralToolMessage { - role: 'tool'; - name: string; diff --git a/patches/@dnd-kit%2Fdom@0.5.0.patch b/patches/@dnd-kit%2Fdom@0.5.0.patch deleted file mode 100644 index 7c56793f9a..0000000000 --- a/patches/@dnd-kit%2Fdom@0.5.0.patch +++ /dev/null @@ -1,40 +0,0 @@ ---- a/index.js -+++ b/index.js -@@ -2195,6 +2195,17 @@ - sensors, - modifiers - })); -+ } -+ get plugins() { -+ return super.plugins; -+ } -+ set plugins(plugins) { -+ super.plugins = [ -+ ScrollListener, -+ Scroller, -+ StyleInjector, -+ ...plugins -+ ]; - } - }; - var _element_dec, _handle_dec, _c, _init5, _handle, _element; ---- a/index.cjs -+++ b/index.cjs -@@ -2196,6 +2196,17 @@ - sensors, - modifiers - })); -+ } -+ get plugins() { -+ return super.plugins; -+ } -+ set plugins(plugins) { -+ super.plugins = [ -+ ScrollListener, -+ Scroller, -+ StyleInjector, -+ ...plugins -+ ]; - } - }; - var _element_dec, _handle_dec, _c, _init5, _handle, _element; diff --git a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch index 3b70bea558..13b8000a01 100644 --- a/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch +++ b/patches/@modelcontextprotocol%2Fsdk@1.29.0.patch @@ -141,15 +141,6 @@ diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp. index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644 --- a/dist/cjs/client/streamableHttp.js +++ b/dist/cjs/client/streamableHttp.js -@@ -204,7 +204,7 @@ class StreamableHTTPClientTransport { - if (!event.event || event.event === 'message') { - try { - const message = types_js_1.JSONRPCMessageSchema.parse(JSON.parse(event.data)); -- if ((0, types_js_1.isJSONRPCResultResponse)(message)) { -+ if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) { - // Mark that we received a response - no need to reconnect for this request - receivedResponse = true; - if (replayMessageId !== undefined) { @@ -290,7 +290,38 @@ class StreamableHTTPClientTransport { this.onclose?.(); } @@ -527,19 +518,10 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88 @@ -1,5 +1,5 @@ import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js'; -import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; -+import { isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; ++import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js'; import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js'; import { EventSourceParserStream } from 'eventsource-parser/stream'; // Default reconnection options for StreamableHTTP connections -@@ -200,7 +200,7 @@ export class StreamableHTTPClientTransport { - if (!event.event || event.event === 'message') { - try { - const message = JSONRPCMessageSchema.parse(JSON.parse(event.data)); -- if (isJSONRPCResultResponse(message)) { -+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) { - // Mark that we received a response - no need to reconnect for this request - receivedResponse = true; - if (replayMessageId !== undefined) { @@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport { this.onclose?.(); } diff --git a/patches/solid-js@1.9.10.patch b/patches/solid-js@1.9.10.patch index 0571d5c8c6..e4e38c2e68 100644 --- a/patches/solid-js@1.9.10.patch +++ b/patches/solid-js@1.9.10.patch @@ -1,5 +1,11 @@ +diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-6fcb6b48d6947d2c b/.bun-tag-6fcb6b48d6947d2c +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +diff --git a/Users/brendonovich/github.com/anomalyco/opencode/node_modules/solid-js/.bun-tag-b272f631c12927b0 b/.bun-tag-b272f631c12927b0 +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/dist/dev.cjs b/dist/dev.cjs -index 7104749..dc3eac9 100644 +index 7104749486e4361e8c4ee7836a8046582cec7aa1..0501eb1ec5d13b81ecb13a5ac1a82db42502b976 100644 --- a/dist/dev.cjs +++ b/dist/dev.cjs @@ -764,6 +764,8 @@ function runComputation(node, value, time) { @@ -11,33 +17,8 @@ index 7104749..dc3eac9 100644 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; -@@ -987,18 +989,21 @@ function cleanNode(node) { - } - } - if (node.tOwned) { -- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); -+ const tOwned = node.tOwned; - delete node.tOwned; -+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); - } - if (Transition && Transition.running && node.pure) { - reset(node, true); - } else if (node.owned) { -- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); -+ const owned = node.owned; - node.owned = null; -+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); - } - if (node.cleanups) { -- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); -+ const cleanups = node.cleanups; - node.cleanups = null; -+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); - } - if (Transition && Transition.running) node.tState = 0;else node.state = 0; - delete node.sourceMap; diff --git a/dist/dev.js b/dist/dev.js -index ea5e4bc..a2e2d59 100644 +index ea5e4bc2fd4f0b3922a73d9134439529dc81339f..4b3ec07e624d20fdd23d6941a4fdde6d3a78cca3 100644 --- a/dist/dev.js +++ b/dist/dev.js @@ -762,6 +762,8 @@ function runComputation(node, value, time) { @@ -49,75 +30,8 @@ index ea5e4bc..a2e2d59 100644 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; -@@ -985,18 +987,21 @@ function cleanNode(node) { - } - } - if (node.tOwned) { -- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); -+ const tOwned = node.tOwned; - delete node.tOwned; -+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); - } - if (Transition && Transition.running && node.pure) { - reset(node, true); - } else if (node.owned) { -- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); -+ const owned = node.owned; - node.owned = null; -+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); - } - if (node.cleanups) { -- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); -+ const cleanups = node.cleanups; - node.cleanups = null; -+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); - } - if (Transition && Transition.running) node.tState = 0;else node.state = 0; - delete node.sourceMap; -diff --git a/dist/server.cjs b/dist/server.cjs -index e715309..188ba81 100644 ---- a/dist/server.cjs -+++ b/dist/server.cjs -@@ -127,12 +127,14 @@ function onCleanup(fn) { - } - function cleanNode(node) { - if (node.owned) { -- for (let i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]); -+ const owned = node.owned; - node.owned = null; -+ for (let i = 0; i < owned.length; i++) cleanNode(owned[i]); - } - if (node.cleanups) { -- for (let i = 0; i < node.cleanups.length; i++) node.cleanups[i](); -+ const cleanups = node.cleanups; - node.cleanups = null; -+ for (let i = 0; i < cleanups.length; i++) cleanups[i](); - } - } - function catchError(fn, handler) { -diff --git a/dist/server.js b/dist/server.js -index d5f8803..320d9af 100644 ---- a/dist/server.js -+++ b/dist/server.js -@@ -125,12 +125,14 @@ function onCleanup(fn) { - } - function cleanNode(node) { - if (node.owned) { -- for (let i = 0; i < node.owned.length; i++) cleanNode(node.owned[i]); -+ const owned = node.owned; - node.owned = null; -+ for (let i = 0; i < owned.length; i++) cleanNode(owned[i]); - } - if (node.cleanups) { -- for (let i = 0; i < node.cleanups.length; i++) node.cleanups[i](); -+ const cleanups = node.cleanups; - node.cleanups = null; -+ for (let i = 0; i < cleanups.length; i++) cleanups[i](); - } - } - function catchError(fn, handler) { diff --git a/dist/solid.cjs b/dist/solid.cjs -index 7c133a2..5ef1501 100644 +index 7c133a2b254678a84fd61d719fbeffad766e1331..2f68c99f2698210cc0bac62f074cc8cd3beb2881 100644 --- a/dist/solid.cjs +++ b/dist/solid.cjs @@ -717,6 +717,8 @@ function runComputation(node, value, time) { @@ -129,33 +43,8 @@ index 7c133a2..5ef1501 100644 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; -@@ -938,18 +940,21 @@ function cleanNode(node) { - } - } - if (node.tOwned) { -- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); -+ const tOwned = node.tOwned; - delete node.tOwned; -+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); - } - if (Transition && Transition.running && node.pure) { - reset(node, true); - } else if (node.owned) { -- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); -+ const owned = node.owned; - node.owned = null; -+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); - } - if (node.cleanups) { -- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); -+ const cleanups = node.cleanups; - node.cleanups = null; -+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); - } - if (Transition && Transition.running) node.tState = 0;else node.state = 0; - } diff --git a/dist/solid.js b/dist/solid.js -index 656fd26..6e0038c 100644 +index 656fd26e7e5c794aa22df19c2377ff5c0591fc29..f08e9f5a7157c3506e5b6922fe2ef991335a80be 100644 --- a/dist/solid.js +++ b/dist/solid.js @@ -715,6 +715,8 @@ function runComputation(node, value, time) { @@ -167,28 +56,3 @@ index 656fd26..6e0038c 100644 Transition.sources.add(node); node.tValue = nextValue; } else node.value = nextValue; -@@ -936,18 +938,21 @@ function cleanNode(node) { - } - } - if (node.tOwned) { -- for (i = node.tOwned.length - 1; i >= 0; i--) cleanNode(node.tOwned[i]); -+ const tOwned = node.tOwned; - delete node.tOwned; -+ for (i = tOwned.length - 1; i >= 0; i--) cleanNode(tOwned[i]); - } - if (Transition && Transition.running && node.pure) { - reset(node, true); - } else if (node.owned) { -- for (i = node.owned.length - 1; i >= 0; i--) cleanNode(node.owned[i]); -+ const owned = node.owned; - node.owned = null; -+ for (i = owned.length - 1; i >= 0; i--) cleanNode(owned[i]); - } - if (node.cleanups) { -- for (i = node.cleanups.length - 1; i >= 0; i--) node.cleanups[i](); -+ const cleanups = node.cleanups; - node.cleanups = null; -+ for (i = cleanups.length - 1; i >= 0; i--) cleanups[i](); - } - if (Transition && Transition.running) node.tState = 0;else node.state = 0; - } diff --git a/sdks/vscode/package.json b/sdks/vscode/package.json index 4c0e903459..117c227a65 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.18.11", + "version": "1.18.3", "publisher": "sst-dev", "repository": { "type": "git",