diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml index 9d29724d86a..92bddf7f531 100644 --- a/.github/actions/setup-bun/action.yml +++ b/.github/actions/setup-bun/action.yml @@ -15,12 +15,15 @@ runs: with: node-version: "24" - - name: Get baseline download URL + - name: Get Bun version and baseline download URL id: bun-url shell: bash run: | + V=$(node -p "require('./package.json').packageManager.split('@')[1]") + # Bun 1.4.2 includes the patched peer-variant fix in oven-sh/bun#33646. + if [ "$RUNNER_OS" = "Windows" ]; then V=1.4.2; fi + echo "version=$V" >> "$GITHUB_OUTPUT" if [ "$RUNNER_ARCH" = "X64" ]; then - V=$(node -p "require('./package.json').packageManager.split('@')[1]") case "$RUNNER_OS" in macOS) OS=darwin ;; Linux) OS=linux ;; @@ -30,9 +33,10 @@ runs: fi - name: Setup Bun + id: setup-bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }} + bun-version: ${{ !steps.bun-url.outputs.url && steps.bun-url.outputs.version || '' }} bun-download-url: ${{ steps.bun-url.outputs.url }} - name: Get cache directory @@ -45,24 +49,16 @@ runs: uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ${{ steps.cache.outputs.dir }} - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }} restore-keys: | - ${{ runner.os }}-bun- + ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}- - name: Install setuptools for distutils compatibility run: python3 -m pip install setuptools || pip install setuptools || true shell: bash - name: Install dependencies - run: | - # Workaround for patched peer variants - # e.g. ./patches/ for standard-openapi - # https://github.com/oven-sh/bun/issues/28147 - if [ "$RUNNER_OS" = "Windows" ]; then - bun install --linker hoisted ${{ inputs.install-flags }} - else - bun install ${{ inputs.install-flags }} - fi + run: bun install ${{ inputs.install-flags }} shell: bash - name: Save Bun dependencies @@ -70,4 +66,4 @@ runs: uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ${{ steps.cache.outputs.dir }} - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }} diff --git a/.github/workflows/duplicate-issues.yml b/.github/workflows/duplicate-issues.yml index 3972247dafc..4254cca6449 100644 --- a/.github/workflows/duplicate-issues.yml +++ b/.github/workflows/duplicate-issues.yml @@ -54,7 +54,7 @@ jobs: "webfetch": "deny" } run: | - opencode run -m opencode/claude-sonnet-4-6 "A new issue has been created: + opencode run -m opencode/gpt-6-luna --variant high "A new issue has been created: Issue number: ${{ github.event.issue.number }} Issue author association: ${{ github.event.issue.author_association }} @@ -188,7 +188,7 @@ jobs: "webfetch": "deny" } run: | - opencode run -m opencode/claude-sonnet-4-6 "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited. + opencode run -m opencode/gpt-6-luna --variant high "Issue #${{ github.event.issue.number }} was previously flagged as non-compliant and has been edited. Issue author association: ${{ github.event.issue.author_association }} Lookup this issue with gh issue view ${{ github.event.issue.number }}. diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml index 0350e43877c..b4654eca5d5 100644 --- a/.github/workflows/triage.yml +++ b/.github/workflows/triage.yml @@ -49,7 +49,7 @@ jobs: ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_BODY: ${{ github.event.issue.body }} run: | - opencode run --agent triage "The following issue was just opened, triage it: + opencode run --agent triage --variant high "The following issue was just opened, triage it: Title: $ISSUE_TITLE diff --git a/.opencode/agent/triage.md b/.opencode/agent/triage.md index 11c4c816cfa..638f4d58183 100644 --- a/.opencode/agent/triage.md +++ b/.opencode/agent/triage.md @@ -1,7 +1,7 @@ --- mode: primary hidden: true -model: opencode/gpt-5.4-mini +model: opencode/gpt-6-luna color: "#44BA81" tools: "*": false diff --git a/.opencode/command/changelog.md b/.opencode/command/changelog.md index b28d963d005..d4c87f4cb32 100644 --- a/.opencode/command/changelog.md +++ b/.opencode/command/changelog.md @@ -1,5 +1,5 @@ --- -model: opencode/gpt-5.4 +model: opencode/gpt-6-luna --- Create `UPCOMING_CHANGELOG.md` from the structured changelog input below. diff --git a/bun.lock b/bun.lock index 79aa7d7e385..b415e68b6e7 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.32", + "version": "1.18.33", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -195,7 +195,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -222,7 +222,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -245,7 +245,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -269,7 +269,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -289,7 +289,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.32", + "version": "1.18.33", "bin": { "opencode": "./bin/opencode", }, @@ -352,6 +352,7 @@ "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", + "open": "11.0.4", "semver": "^7.6.3", "turndown": "7.2.0", "venice-ai-sdk-provider": "2.1.1", @@ -383,7 +384,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@zip.js/zip.js": "2.7.62", "drizzle-orm": "catalog:", @@ -437,7 +438,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -451,7 +452,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "effect": "catalog:", }, @@ -463,7 +464,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -495,7 +496,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -511,7 +512,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -542,7 +543,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -561,7 +562,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.32", + "version": "1.18.33", "bin": { "opencode": "./bin/opencode", }, @@ -645,7 +646,6 @@ "mime-types": "3.0.2", "minimatch": "10.0.3", "npm-package-arg": "13.0.2", - "open": "10.1.2", "opencode-gitlab-auth": "2.1.0", "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", @@ -692,7 +692,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -768,7 +768,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "cross-spawn": "catalog:", }, @@ -783,7 +783,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -798,7 +798,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -838,7 +838,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -851,7 +851,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -878,7 +878,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -897,7 +897,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -939,7 +939,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -952,7 +952,6 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "open": "10.1.2", "opentui-spinner": "catalog:", "remeda": "catalog:", "solid-js": "catalog:", @@ -966,7 +965,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1017,7 +1016,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1086,6 +1085,7 @@ "@opentui/solid": "catalog:", "@types/bun": "catalog:", "@types/node": "catalog:", + "open": "11.0.4", }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", @@ -3456,7 +3456,7 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -4592,7 +4592,7 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], - "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], + "open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="], "openai": ["openai@5.11.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="], @@ -4758,7 +4758,7 @@ "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], "preact": ["preact@11.0.0-beta.0", "", {}, "sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg=="], @@ -5550,7 +5550,7 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="], "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], @@ -5928,8 +5928,6 @@ "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], - "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -6414,8 +6412,6 @@ "opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -6488,8 +6484,6 @@ "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -6560,6 +6554,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], @@ -7172,8 +7168,6 @@ "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "opencode/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="], "opencode/@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], @@ -7200,8 +7194,6 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], diff --git a/nix/hashes.json b/nix/hashes.json index c4c6ed3cc56..3672243020b 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-yKqw4YLXSzPOxPxXVWEK2W11YpwXQtFR1533Z8/s2rI=", - "aarch64-linux": "sha256-xbrn9P8be2XHOVKhH4EM2fiao2kppgJouXcoQSBRfnk=", - "aarch64-darwin": "sha256-bs0ulK/Ny3JWVcGF4CRVr2/RsLAK5bglkrshVmGrBrc=", - "x86_64-darwin": "sha256-s0j5DImmcA/tCP2kNTaYd6QugG+k8WtcDWVt7Yi2cxU=" + "x86_64-linux": "sha256-RIzpvDBR6GOfU2vjrtgP3LJ/j+gLxkMnJ9VE5Y65Pfo=", + "aarch64-linux": "sha256-seE7v8AqxWCiyxNARMiLJNB1MZv15OwEf6r21k68xqg=", + "aarch64-darwin": "sha256-rbyK+3qz9bGOxg3HlGW6yh3qeazCaxFcuPfdGbXHsIY=", + "x86_64-darwin": "sha256-chSE5WtSQ+qdbj5vTvGmr4rn9gk8eVp3pVeow4gVqYI=" } } diff --git a/package.json b/package.json index a3f9544410d..59bf0501f09 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,8 @@ "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@types/bun": "catalog:", - "@types/node": "catalog:" + "@types/node": "catalog:", + "open": "11.0.4" }, "patchedDependencies": { "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", diff --git a/packages/app/package.json b/packages/app/package.json index c651d9aef76..109f928dc87 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "1.18.32", + "version": "1.18.33", "description": "", "type": "module", "exports": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 4c51214028d..ad2d73cc8e3 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.32", + "version": "1.18.33", "type": "module", "license": "MIT", "bin": { diff --git a/packages/codemode/package.json b/packages/codemode/package.json index 0c699730d4e..d857d1ee907 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.32", + "version": "1.18.33", "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 2d66b17a913..2feae199edc 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.32", + "version": "1.18.33", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/console/app/src/component/go-models.ts b/packages/console/app/src/component/go-models.ts index 7c731b1e301..2df5cb69845 100644 --- a/packages/console/app/src/component/go-models.ts +++ b/packages/console/app/src/component/go-models.ts @@ -28,14 +28,12 @@ export const goModels = [ id: "deepseek-flash", name: "DeepSeek V4.1 Flash", requests: 26000, - baseRequests: 6500, allowance: 60, - baseAllowance: 15, - bonus: 4, featured: true, fresh: true, }, { id: "longcat-2.0", name: "LongCat-2.0", requests: 11400, allowance: 60 }, + { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", requests: 13000, allowance: 30 }, { id: "mimo-v2.6-flash", name: "MiMo-V2.6-Flash", @@ -69,4 +67,13 @@ export const goModels = [ fresh: true, limitedTime: true, }, + { + id: "longcat-2.5-preview-free", + name: "LongCat 2.5 Preview Free", + requests: Infinity, + allowance: Infinity, + featured: true, + fresh: true, + limitedTime: true, + }, ].sort((a, b) => a.requests - b.requests) diff --git a/packages/console/app/src/component/limits-graph.tsx b/packages/console/app/src/component/limits-graph.tsx index 3bb95d1f4af..c7cfd5eeba8 100644 --- a/packages/console/app/src/component/limits-graph.tsx +++ b/packages/console/app/src/component/limits-graph.tsx @@ -88,9 +88,6 @@ export function LimitsGraph(props: { href: string }) { {i18n.t("go.graph.new")} - - {i18n.t("go.graph.bonus", { count: model.bonus! })} - {i18n.t("go.graph.limitedTime")} @@ -117,16 +114,10 @@ export function LimitsGraph(props: { href: string }) {
- - {format().format(model.baseRequests!)}{" "} - {format().format(model.requests)}
= 60 ? "" : undefined}> - - {currency().format(model.baseAllowance!)}{" "} - diff --git a/packages/console/app/src/routes/go/index.tsx b/packages/console/app/src/routes/go/index.tsx index 05fea4281cc..b820dfdcdbf 100644 --- a/packages/console/app/src/routes/go/index.tsx +++ b/packages/console/app/src/routes/go/index.tsx @@ -26,6 +26,7 @@ const checkLoggedIn = query(async () => { const models = [ { name: "Space Bunny Free", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, + { name: "LongCat 2.5 Preview Free", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" }, { name: "Grok 4.7", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, { name: "Grok 4.6", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, { name: "GPT 6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" }, diff --git a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx index 979c1d6975a..6aa93af08a3 100644 --- a/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx +++ b/packages/console/app/src/routes/workspace/[id]/go/lite-section.tsx @@ -662,7 +662,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
  • Qwen3.7 Max
  • Qwen3.7 Plus
  • Qwen3.6 Plus
  • -
  • DeepSeek V4.1 Flash — {i18n.t("go.graph.bonus", { count: 4 })}
  • +
  • DeepSeek V4.1 Flash
  • DeepSeek V4 Pro
  • DeepSeek V4 Flash
  • DeepSeek V4 Flash Vision Exp
  • diff --git a/packages/console/core/package.json b/packages/console/core/package.json index 0c6b06295dd..44869874434 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/console-core", - "version": "1.18.32", + "version": "1.18.33", "private": true, "type": "module", "license": "MIT", diff --git a/packages/console/function/package.json b/packages/console/function/package.json index 5af679c8733..ab3528b8cf2 100644 --- a/packages/console/function/package.json +++ b/packages/console/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-function", - "version": "1.18.32", + "version": "1.18.33", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json index 9bac48ba8d5..efed48ac06e 100644 --- a/packages/console/mail/package.json +++ b/packages/console/mail/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-mail", - "version": "1.18.32", + "version": "1.18.33", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", diff --git a/packages/console/support/package.json b/packages/console/support/package.json index 7224fa97a1b..8c63242b526 100644 --- a/packages/console/support/package.json +++ b/packages/console/support/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/console-support", - "version": "1.18.32", + "version": "1.18.33", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 0a7e0ea07a6..5488a1dc38c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.32", + "version": "1.18.33", "name": "@opencode-ai/core", "type": "module", "license": "MIT", @@ -119,6 +119,7 @@ "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", + "open": "11.0.4", "semver": "^7.6.3", "turndown": "7.2.0", "venice-ai-sdk-provider": "2.1.1", diff --git a/packages/core/src/open.ts b/packages/core/src/open.ts new file mode 100644 index 00000000000..296982f3afc --- /dev/null +++ b/packages/core/src/open.ts @@ -0,0 +1,8 @@ +import open from "open" + +export function openUrl(input: string) { + const url = URL.canParse(input) ? new URL(input) : undefined + if (!url || (url.protocol !== "http:" && url.protocol !== "https:")) + return Promise.reject(new Error(`Only http and https links can be opened in the browser: ${input}`)) + return open(url.href) +} diff --git a/packages/core/test/open.test.ts b/packages/core/test/open.test.ts new file mode 100644 index 00000000000..b40222eb6d2 --- /dev/null +++ b/packages/core/test/open.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test" +import { openUrl } from "@opencode-ai/core/open" + +describe("openUrl", () => { + test("rejects values that are not URLs", async () => { + await expect(openUrl("not a url")).rejects.toThrow("Only http and https links") + await expect(openUrl("")).rejects.toThrow("Only http and https links") + }) + + test("rejects non-http schemes", async () => { + await expect(openUrl("file:///etc/hosts")).rejects.toThrow("Only http and https links") + await expect(openUrl("javascript:alert(1)")).rejects.toThrow("Only http and https links") + await expect(openUrl("ms-msdt:/id PCWDiagnostic")).rejects.toThrow("Only http and https links") + await expect(openUrl("\\\\server\\share\\file.html")).rejects.toThrow("Only http and https links") + }) +}) diff --git a/packages/desktop/package.json b/packages/desktop/package.json index dac7921cf48..4ef186353e6 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "1.18.32", + "version": "1.18.33", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index b6858628411..703c40ecfb3 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.32", + "version": "1.18.33", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 04ced2b0aa9..328f048574d 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.32", + "version": "1.18.33", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/enterprise/package.json b/packages/enterprise/package.json index 8ee5257484b..31d405c8051 100644 --- a/packages/enterprise/package.json +++ b/packages/enterprise/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/enterprise", - "version": "1.18.32", + "version": "1.18.33", "private": true, "type": "module", "license": "MIT", diff --git a/packages/function/package.json b/packages/function/package.json index 9b26385a8ee..5a33b79b82f 100644 --- a/packages/function/package.json +++ b/packages/function/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/function", - "version": "1.18.32", + "version": "1.18.33", "$schema": "https://json.schemastore.org/package.json", "private": true, "type": "module", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 5f6ec74b223..c1970380d05 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.32", + "version": "1.18.33", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 4acb171d238..8b0100150ef 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.32", + "version": "1.18.33", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8b6e8ae73d1..6926ddfa3e1 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "1.18.32", + "version": "1.18.33", "name": "opencode", "type": "module", "license": "MIT", @@ -131,7 +131,6 @@ "mime-types": "3.0.2", "minimatch": "10.0.3", "npm-package-arg": "13.0.2", - "open": "10.1.2", "opencode-gitlab-auth": "2.1.0", "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index f439c187314..d15bb1d0176 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -5,9 +5,9 @@ import { Account } from "@/account/account" import { AccountID, OrgID, PollExpired, type PollResult, type AccountError } from "@/account/schema" import { effectCmd } from "../effect-cmd" import * as Prompt from "../effect/prompt" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" -const openBrowser = (url: string) => Effect.promise(() => open(url).catch(() => undefined)) +const openBrowser = (url: string) => Effect.promise(() => openUrl(url).catch(() => undefined)) const println = (msg: string) => Effect.sync(() => UI.println(msg)) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 69a981aada4..8842b7ca37c 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -3,7 +3,7 @@ import { UI } from "../ui" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "@opencode-ai/core/flag/flag" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" import { networkInterfaces } from "os" function getNetworkIPs() { @@ -72,11 +72,11 @@ export const WebCommand = effectCmd({ } // Open localhost in browser - open(localhostUrl).catch(() => {}) + openUrl(localhostUrl).catch(() => {}) } else { const displayUrl = server.url.toString() UI.println(UI.Style.TEXT_INFO_BOLD + " Web interface: ", UI.Style.TEXT_NORMAL, displayUrl) - open(displayUrl).catch(() => {}) + openUrl(displayUrl).catch(() => {}) } yield* Effect.never diff --git a/packages/opencode/src/mcp/browser.ts b/packages/opencode/src/mcp/browser.ts index 5760d8cbf9a..68f4f59b213 100644 --- a/packages/opencode/src/mcp/browser.ts +++ b/packages/opencode/src/mcp/browser.ts @@ -1,6 +1,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { openUrl } from "@opencode-ai/core/open" import { Context, Effect, Layer } from "effect" -import open from "open" export interface Interface { readonly open: (url: string) => Effect.Effect @@ -13,7 +13,7 @@ const layer = Layer.succeed( Service.of({ open: Effect.fn("McpBrowser.open")(function* (url: string) { const subprocess = yield* Effect.tryPromise({ - try: () => open(url), + try: () => openUrl(url), catch: (error) => (error instanceof Error ? error : new Error(String(error))), }) yield* Effect.callback((resume) => { @@ -22,11 +22,14 @@ const layer = Layer.succeed( clearTimeout(timer) resume(Effect.fail(error)) }) - subprocess.on("exit", (code) => { + const onExit = (code: number | null) => { if (code === null || code === 0) return clearTimeout(timer) resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`))) - }) + } + subprocess.on("exit", onExit) + // On Windows and WSL, open() can return only after the launcher has exited. + onExit(subprocess.exitCode) }) }), }), diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 596bfe1d551..64a9912a4ec 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -125,6 +125,10 @@ export class McpOAuthProvider implements OAuthClientProvider { } async redirectToAuthorization(authorizationUrl: URL): Promise { + if (authorizationUrl.protocol !== "http:" && authorizationUrl.protocol !== "https:") + throw new Error( + `MCP server "${this.serverUrl}" returned a ${authorizationUrl.protocol} authorization URL; only http and https are supported`, + ) await this.callbacks.onRedirect(authorizationUrl) } diff --git a/packages/opencode/src/plugin/digitalocean.ts b/packages/opencode/src/plugin/digitalocean.ts index af241781e61..6553fd5f25c 100644 --- a/packages/opencode/src/plugin/digitalocean.ts +++ b/packages/opencode/src/plugin/digitalocean.ts @@ -3,7 +3,7 @@ import type { Model } from "@opencode-ai/sdk/v2" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { OauthCallbackPage } from "@opencode-ai/core/oauth/page" import { createServer } from "http" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" const DO_OAUTH_CLIENT_ID = "b1a6c5158156caac821fd1b30253ca8acb52454a48fa744420e41889cb589f82" const DO_AUTHORIZE_URL = "https://cloud.digitalocean.com/v1/oauth/authorize" @@ -279,7 +279,7 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise const state = generateState() const callbackPromise = waitForOAuthCallback(state) const url = buildAuthorizeUrl(state) - await open(url).catch(() => undefined) + await openUrl(url).catch(() => undefined) return { url, instructions: diff --git a/packages/opencode/src/plugin/snowflake-cortex.ts b/packages/opencode/src/plugin/snowflake-cortex.ts index 09f107ed7b9..c5c4df126ad 100644 --- a/packages/opencode/src/plugin/snowflake-cortex.ts +++ b/packages/opencode/src/plugin/snowflake-cortex.ts @@ -3,7 +3,7 @@ import { OAUTH_DUMMY_KEY } from "../auth" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { OauthCallbackPage } from "@opencode-ai/core/oauth/page" import { createServer } from "http" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" const OAUTH_CLIENT_ID = "LOCAL_APPLICATION" const OAUTH_CALLBACK_HOST = "127.0.0.1" @@ -470,7 +470,7 @@ export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise undefined) + await openUrl(url).catch(() => undefined) return { url, diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 867ef2ca2fa..97f83bb51d1 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -91,6 +91,41 @@ function timeoutController(ms: number) { } } +// Applies the `headerTimeout`, `chunkTimeout` (SSE idle) and `timeout` provider options at the +// fetch layer, on top of `options.fetch` when a custom fetch is configured. +function timeoutFetch(options: Record) { + const customFetch = options["fetch"] + const chunkTimeout = options["chunkTimeout"] ?? 300_000 + const headerTimeout = options["headerTimeout"] ?? 300_000 + const timeout = options["timeout"] + + return async (input: any, init?: BunFetchRequestInit) => { + const fetchFn = customFetch ?? fetch + const opts = init ?? {} + const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined + const headerTimeoutMs = headerTimeout === false ? undefined : headerTimeout + const headerTimeoutCtl = typeof headerTimeoutMs === "number" ? timeoutController(headerTimeoutMs) : undefined + const signals: AbortSignal[] = [] + + if (opts.signal) signals.push(opts.signal) + if (chunkAbortCtl) signals.push(chunkAbortCtl.signal) + if (headerTimeoutCtl) signals.push(headerTimeoutCtl.signal) + if (timeout !== undefined && timeout !== null && timeout !== false) signals.push(AbortSignal.timeout(timeout)) + + const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) + if (combined) opts.signal = combined + + const res = await fetchFn(input, { + ...opts, + // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 + timeout: false, + }).finally(() => headerTimeoutCtl?.clear()) + + if (!chunkAbortCtl) return res + return wrapSSE(res, chunkTimeout, chunkAbortCtl) + } +} + function googleVertexAnthropicBaseURL(project: string | undefined, location: string | undefined) { if (!project) return if (location !== "eu" && location !== "us") return @@ -810,7 +845,7 @@ function custom(dep: CustomDep): Record { ) } - const { createAiGateway } = yield* Effect.promise(() => import("ai-gateway-provider")) + const { createAiGateway, parseAiGatewayOptions } = yield* Effect.promise(() => import("ai-gateway-provider")) const { createUnified } = yield* Effect.promise(() => import("ai-gateway-provider/providers/unified")) const { createOpenAI } = yield* Effect.promise(() => import("ai-gateway-provider/providers/openai")) const { createAnthropic } = yield* Effect.promise(() => import("ai-gateway-provider/providers/anthropic")) @@ -835,15 +870,30 @@ function custom(dep: CustomDep): Record { }, } - const aigateway = createAiGateway({ - accountId, - gateway, - apiKey: apiToken, - ...(Object.values(opts).some((v) => v !== undefined) ? { options: opts } : {}), - }) return { autoload: true, - async getModel(_sdk: any, modelID: string, _options?: Record) { + async getModel(_sdk: any, modelID: string, options?: Record) { + // This loader builds its own clients instead of using the SDK from resolveSDK, so the + // timeout options have to be applied to the requests it makes explicitly. + const gatewayFetch = timeoutFetch(options ?? {}) + // ai-gateway-provider's REST path always calls the global fetch. Its binding path hands + // the request to `run`, so send the same request the REST path would (options as + // request-level cf-aig-* headers) through the timeout-aware fetch instead. + const aigateway = createAiGateway({ + binding: { + run(body, init) { + const headers = parseAiGatewayOptions(opts) + headers.set("Content-Type", "application/json") + headers.set("cf-aig-authorization", `Bearer ${apiToken}`) + return gatewayFetch(`https://gateway.ai.cloudflare.com/v1/${accountId}/${gateway}`, { + body: JSON.stringify(body), + headers, + method: "POST", + signal: init?.signal, + }) + }, + }, + }) // Model IDs use Unified API format: provider/model (e.g., "anthropic/claude-sonnet-4-5"). // OpenAI and Anthropic ride their native passthrough routes so agents get the Responses // and Messages APIs; new OpenAI models reject tools+reasoning_effort on chat completions. @@ -877,6 +927,7 @@ function custom(dep: CustomDep): Record { baseURL: `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`, apiKey: apiToken, headers: { "cf-aig-gateway-id": gateway }, + fetch: gatewayFetch as typeof fetch, })(modelID) }, options: {}, @@ -1795,39 +1846,10 @@ const layer = Layer.effect( const existing = s.sdk.get(key) if (existing) return existing - const customFetch = options["fetch"] - const chunkTimeout = options["chunkTimeout"] ?? 300_000 - const headerTimeout = options["headerTimeout"] ?? 300_000 + options["fetch"] = timeoutFetch(options) delete options["chunkTimeout"] delete options["headerTimeout"] - options["fetch"] = async (input: any, init?: BunFetchRequestInit) => { - const fetchFn = customFetch ?? fetch - const opts = init ?? {} - const chunkAbortCtl = typeof chunkTimeout === "number" && chunkTimeout > 0 ? new AbortController() : undefined - const headerTimeoutMs = headerTimeout === false ? undefined : headerTimeout - const headerTimeoutCtl = typeof headerTimeoutMs === "number" ? timeoutController(headerTimeoutMs) : undefined - const signals: AbortSignal[] = [] - - if (opts.signal) signals.push(opts.signal) - if (chunkAbortCtl) signals.push(chunkAbortCtl.signal) - if (headerTimeoutCtl) signals.push(headerTimeoutCtl.signal) - if (options["timeout"] !== undefined && options["timeout"] !== null && options["timeout"] !== false) - signals.push(AbortSignal.timeout(options["timeout"])) - - const combined = signals.length === 0 ? null : signals.length === 1 ? signals[0] : AbortSignal.any(signals) - if (combined) opts.signal = combined - - const res = await fetchFn(input, { - ...opts, - // @ts-ignore see here: https://github.com/oven-sh/bun/issues/16682 - timeout: false, - }).finally(() => headerTimeoutCtl?.clear()) - - if (!chunkAbortCtl) return res - return wrapSSE(res, chunkTimeout, chunkAbortCtl) - } - const bundledLoader = BUNDLED_PROVIDERS[model.api.npm] if (bundledLoader) { const factory = await bundledLoader() diff --git a/packages/opencode/test/mcp/browser.test.ts b/packages/opencode/test/mcp/browser.test.ts new file mode 100644 index 00000000000..81dea236ac7 --- /dev/null +++ b/packages/opencode/test/mcp/browser.test.ts @@ -0,0 +1,36 @@ +import { expect, mock, test } from "bun:test" +import { spawn } from "node:child_process" +import { once } from "node:events" +import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { Effect } from "effect" + +await mock.module("@opencode-ai/core/open", () => ({ + openUrl: async (url: string) => { + const code = new URL(url).pathname === "/successful" ? 0 : 23 + const subprocess = spawn(process.execPath, ["-e", `process.exit(${code})`], { stdio: "ignore" }) + await once(subprocess, "close") + return subprocess + }, +})) + +const { McpBrowser } = await import("../../src/mcp/browser") + +test("reports a browser launcher that exited before openUrl returned", async () => { + const error = await Effect.runPromise( + Effect.gen(function* () { + const browser = yield* McpBrowser.Service + return yield* Effect.flip(browser.open("https://example.com/authorize")) + }).pipe(Effect.provide(LayerNode.compile(McpBrowser.node))), + ) + + expect(error.message).toBe("Browser open failed with exit code 23") +}) + +test("accepts a browser launcher that exited successfully before openUrl returned", async () => { + await Effect.runPromise( + Effect.gen(function* () { + const browser = yield* McpBrowser.Service + yield* browser.open("https://example.com/successful") + }).pipe(Effect.provide(LayerNode.compile(McpBrowser.node))), + ) +}) diff --git a/packages/opencode/test/mcp/oauth-provider.test.ts b/packages/opencode/test/mcp/oauth-provider.test.ts index 249c49e8f91..fc5aa7bc270 100644 --- a/packages/opencode/test/mcp/oauth-provider.test.ts +++ b/packages/opencode/test/mcp/oauth-provider.test.ts @@ -34,6 +34,37 @@ describe("McpOAuthProvider.redirectUrl", () => { }) }) +describe("McpOAuthProvider.redirectToAuthorization", () => { + test("forwards http and https authorization URLs", async () => { + const seen: string[] = [] + const provider = new McpOAuthProvider( + "test-server", + "https://mcp.example.com/mcp", + {}, + { onRedirect: (url) => void seen.push(url.href) }, + stubAuth, + ) + await provider.redirectToAuthorization(new URL("https://auth.example.com/authorize?state=1")) + await provider.redirectToAuthorization(new URL("http://127.0.0.1:8080/authorize")) + expect(seen).toEqual(["https://auth.example.com/authorize?state=1", "http://127.0.0.1:8080/authorize"]) + }) + + test("rejects authorization URLs that are not http or https", async () => { + const seen: string[] = [] + const provider = new McpOAuthProvider( + "test-server", + "https://mcp.example.com/mcp", + {}, + { onRedirect: (url) => void seen.push(url.href) }, + stubAuth, + ) + await expect(provider.redirectToAuthorization(new URL("file:///tmp/authorize"))).rejects.toThrow( + "returned a file: authorization URL", + ) + expect(seen).toEqual([]) + }) +}) + describe("McpOAuthProvider.clientMetadata", () => { test("includes redirect_uris from redirectUrl", () => { const provider = makeProvider({ callbackPort: 6620 }) diff --git a/packages/opencode/test/provider/header-timeout.test.ts b/packages/opencode/test/provider/header-timeout.test.ts index 38b884dc7fc..3442105aa70 100644 --- a/packages/opencode/test/provider/header-timeout.test.ts +++ b/packages/opencode/test/provider/header-timeout.test.ts @@ -244,6 +244,112 @@ it.live("OpenAI API auth gets default headerTimeout", () => }), ) +const gatewayModels = { + "native passthrough": "anthropic/claude-sonnet-4-6", + "REST catalog": "google/gemini-2.5-flash", +} + +for (const [route, modelID] of Object.entries(gatewayModels)) { + it.live(`cloudflare-ai-gateway ${route} applies chunkTimeout when the SSE body stalls`, () => + Effect.gen(function* () { + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const urls = yield* setupGateway(() => + Promise.resolve(new Response(new ReadableStream(), { headers: { "content-type": "text/event-stream" } })), + ) + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderV2.ID.make("cloudflare-ai-gateway"), + ModelV2.ID.make(modelID), + ) + const result = streamText({ + model: yield* provider.getLanguage(model), + onError() {}, + messages: [{ role: "user", content: "hello" }], + }) + + const error = yield* Effect.promise(() => firstStreamError(result.fullStream)) + expect(urls).toHaveLength(1) + expect(error).toBeInstanceOf(ProviderError.ResponseStreamError) + }), + { config: gatewayConfig({ chunkTimeout: 50 }) }, + ) + }), + ) +} + +it.live("cloudflare-ai-gateway applies headerTimeout when response headers do not arrive", () => + Effect.gen(function* () { + yield* provideTmpdirInstance( + () => + Effect.gen(function* () { + const urls = yield* setupGateway( + (init) => + new Promise((_, reject) => init?.signal?.addEventListener("abort", () => reject(init.signal?.reason))), + ) + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderV2.ID.make("cloudflare-ai-gateway"), + ModelV2.ID.make(gatewayModels["native passthrough"]), + ) + const result = streamText({ + model: yield* provider.getLanguage(model), + onError() {}, + messages: [{ role: "user", content: "hello" }], + }) + + const error = yield* Effect.promise(() => firstStreamError(result.fullStream)) + expect(urls).toEqual(["https://gateway.ai.cloudflare.com/v1/test-account/test-gateway"]) + expect(String(error)).toContain("response headers timed out") + }), + { config: gatewayConfig({ headerTimeout: 50 }) }, + ) + }), +) + +// Routes the gateway provider's requests to `respond` through the configured custom fetch, which +// the timeout wrapper calls instead of the global fetch. Returns the requested URLs. +function setupGateway(respond: (init?: RequestInit) => Promise) { + return Effect.gen(function* () { + yield* Env.use.set("CLOUDFLARE_ACCOUNT_ID", "test-account") + yield* Env.use.set("CLOUDFLARE_GATEWAY_ID", "test-gateway") + yield* Env.use.set("CLOUDFLARE_API_TOKEN", "test-token") + const provider = yield* Provider.Service + const configured = yield* provider.getProvider(ProviderV2.ID.make("cloudflare-ai-gateway")) + const urls: string[] = [] + configured.options.fetch = (input: string, init?: RequestInit) => { + urls.push(input) + return respond(init) + } + return urls + }) +} + +function gatewayConfig(options: Record) { + return { + provider: { + "cloudflare-ai-gateway": { + options, + models: { + // The gateway loader builds its own client; a bundled npm keeps resolveSDK off the network. + [gatewayModels["REST catalog"]]: { name: "Gemini 2.5 Flash", provider: { npm: "@ai-sdk/openai-compatible" } }, + }, + }, + }, + } +} + +async function firstStreamError(stream: AsyncIterable<{ type: string; error?: unknown }>) { + try { + for await (const part of stream) { + if (part.type === "error") return part.error + } + } catch (error) { + return error + } +} + function providerConfig(url: string, options: Record = {}) { const config = testProviderConfig(url) return { diff --git a/packages/plugin/package.json b/packages/plugin/package.json index efb79b8e94f..dd395423783 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/plugin", - "version": "1.18.32", + "version": "1.18.33", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 4662037d9b3..ce29f29ba07 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/sdk", - "version": "1.18.32", + "version": "1.18.33", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 69cc9580f0c..b31cb1d4655 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "1.18.32", + "version": "1.18.33", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index 2164064facf..1761316a908 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "1.18.32", + "version": "1.18.33", "private": true, "type": "module", "license": "MIT", diff --git a/packages/slack/package.json b/packages/slack/package.json index e106fb7e970..0b29d419e97 100644 --- a/packages/slack/package.json +++ b/packages/slack/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/slack", - "version": "1.18.32", + "version": "1.18.33", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/stats/app/package.json b/packages/stats/app/package.json index 27b6957799c..48a6af3a102 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.32", + "version": "1.18.33", "private": true, "type": "module", "license": "MIT", diff --git a/packages/stats/app/src/i18n.ts b/packages/stats/app/src/i18n.ts index 0b8053694ea..c4c06fb1778 100644 --- a/packages/stats/app/src/i18n.ts +++ b/packages/stats/app/src/i18n.ts @@ -80,6 +80,7 @@ const en = { "range.2W": "2 Weeks", "range.1M": "1 Month", "range.2M": "2 Months", + "chart.weekly": "Weekly", "chart.daily": "Daily", "chart.input": "Input", "chart.output": "Output", diff --git a/packages/stats/app/src/i18n/ar.ts b/packages/stats/app/src/i18n/ar.ts index b2430431027..16c215cf4ce 100644 --- a/packages/stats/app/src/i18n/ar.ts +++ b/packages/stats/app/src/i18n/ar.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "أسبوعان", "range.1M": "شهر واحد", "range.2M": "شهران", + "chart.weekly": "أسبوعي", "chart.daily": "يومي", "chart.input": "الإدخال", "chart.output": "الإخراج", diff --git a/packages/stats/app/src/i18n/br.ts b/packages/stats/app/src/i18n/br.ts index 5bf9e44dc7e..e975aef6d79 100644 --- a/packages/stats/app/src/i18n/br.ts +++ b/packages/stats/app/src/i18n/br.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 semanas", "range.1M": "1 mês", "range.2M": "2 meses", + "chart.weekly": "Semanal", "chart.daily": "Diário", "chart.input": "Entrada", "chart.output": "Saída", diff --git a/packages/stats/app/src/i18n/da.ts b/packages/stats/app/src/i18n/da.ts index 5fdf2841e7b..c92136bab32 100644 --- a/packages/stats/app/src/i18n/da.ts +++ b/packages/stats/app/src/i18n/da.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 uger", "range.1M": "1 måned", "range.2M": "2 måneder", + "chart.weekly": "Ugentligt", "chart.daily": "Dagligt", "chart.input": "Input", "chart.output": "Output", diff --git a/packages/stats/app/src/i18n/de.ts b/packages/stats/app/src/i18n/de.ts index 95a2f06fa7d..5bc6e0ba9cc 100644 --- a/packages/stats/app/src/i18n/de.ts +++ b/packages/stats/app/src/i18n/de.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 Wochen", "range.1M": "1 Monat", "range.2M": "2 Monate", + "chart.weekly": "Wöchentlich", "chart.daily": "Täglich", "chart.input": "Eingabe", "chart.output": "Ausgabe", diff --git a/packages/stats/app/src/i18n/es.ts b/packages/stats/app/src/i18n/es.ts index 78d2e61c24a..6fc92e36b25 100644 --- a/packages/stats/app/src/i18n/es.ts +++ b/packages/stats/app/src/i18n/es.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 semanas", "range.1M": "1 mes", "range.2M": "2 meses", + "chart.weekly": "Semanal", "chart.daily": "Diario", "chart.input": "Entrada", "chart.output": "Salida", diff --git a/packages/stats/app/src/i18n/fr.ts b/packages/stats/app/src/i18n/fr.ts index bbc1d5cca71..99810e039b5 100644 --- a/packages/stats/app/src/i18n/fr.ts +++ b/packages/stats/app/src/i18n/fr.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 semaines", "range.1M": "1 mois", "range.2M": "2 mois", + "chart.weekly": "Hebdomadaire", "chart.daily": "Quotidien", "chart.input": "Entrée", "chart.output": "Sortie", diff --git a/packages/stats/app/src/i18n/it.ts b/packages/stats/app/src/i18n/it.ts index e4a4b12969f..ab3f9c69828 100644 --- a/packages/stats/app/src/i18n/it.ts +++ b/packages/stats/app/src/i18n/it.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 settimane", "range.1M": "1 mese", "range.2M": "2 mesi", + "chart.weekly": "Settimanale", "chart.daily": "Giornaliero", "chart.input": "Input", "chart.output": "Output", diff --git a/packages/stats/app/src/i18n/ja.ts b/packages/stats/app/src/i18n/ja.ts index 1826329523a..1652e9ed026 100644 --- a/packages/stats/app/src/i18n/ja.ts +++ b/packages/stats/app/src/i18n/ja.ts @@ -63,6 +63,7 @@ export const dict = { "range.2W": "2週間", "range.1M": "1か月", "range.2M": "2か月", + "chart.weekly": "週次", "chart.daily": "日次", "chart.input": "入力", "chart.output": "出力", diff --git a/packages/stats/app/src/i18n/ko.ts b/packages/stats/app/src/i18n/ko.ts index d55ba5a606d..dcd5bfcaa06 100644 --- a/packages/stats/app/src/i18n/ko.ts +++ b/packages/stats/app/src/i18n/ko.ts @@ -63,6 +63,7 @@ export const dict = { "range.2W": "2주", "range.1M": "1개월", "range.2M": "2개월", + "chart.weekly": "주간", "chart.daily": "일간", "chart.input": "입력", "chart.output": "출력", diff --git a/packages/stats/app/src/i18n/no.ts b/packages/stats/app/src/i18n/no.ts index 26ec3e80bfb..67de0339bf0 100644 --- a/packages/stats/app/src/i18n/no.ts +++ b/packages/stats/app/src/i18n/no.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 uker", "range.1M": "1 måned", "range.2M": "2 måneder", + "chart.weekly": "Ukentlig", "chart.daily": "Daglig", "chart.input": "Inndata", "chart.output": "Utdata", diff --git a/packages/stats/app/src/i18n/pl.ts b/packages/stats/app/src/i18n/pl.ts index e82ddeff0b9..703372b7a90 100644 --- a/packages/stats/app/src/i18n/pl.ts +++ b/packages/stats/app/src/i18n/pl.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 tygodnie", "range.1M": "1 miesiąc", "range.2M": "2 miesiące", + "chart.weekly": "Tygodniowo", "chart.daily": "Dziennie", "chart.input": "Wejście", "chart.output": "Wyjście", diff --git a/packages/stats/app/src/i18n/ru.ts b/packages/stats/app/src/i18n/ru.ts index fe850a3094e..fdf9b1a90bc 100644 --- a/packages/stats/app/src/i18n/ru.ts +++ b/packages/stats/app/src/i18n/ru.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 недели", "range.1M": "1 месяц", "range.2M": "2 месяца", + "chart.weekly": "Еженедельно", "chart.daily": "Ежедневно", "chart.input": "Ввод", "chart.output": "Вывод", diff --git a/packages/stats/app/src/i18n/th.ts b/packages/stats/app/src/i18n/th.ts index 84f79a21f7f..f27cf62ed16 100644 --- a/packages/stats/app/src/i18n/th.ts +++ b/packages/stats/app/src/i18n/th.ts @@ -63,6 +63,7 @@ export const dict = { "range.2W": "2 สัปดาห์", "range.1M": "1 เดือน", "range.2M": "2 เดือน", + "chart.weekly": "รายสัปดาห์", "chart.daily": "รายวัน", "chart.input": "อินพุต", "chart.output": "เอาต์พุต", diff --git a/packages/stats/app/src/i18n/tr.ts b/packages/stats/app/src/i18n/tr.ts index 27926c9d548..ad3d6e8a818 100644 --- a/packages/stats/app/src/i18n/tr.ts +++ b/packages/stats/app/src/i18n/tr.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 Hafta", "range.1M": "1 Ay", "range.2M": "2 Ay", + "chart.weekly": "Haftalık", "chart.daily": "Günlük", "chart.input": "Girdi", "chart.output": "Çıktı", diff --git a/packages/stats/app/src/i18n/uk.ts b/packages/stats/app/src/i18n/uk.ts index eb43f81aa81..1e0cf2fb375 100644 --- a/packages/stats/app/src/i18n/uk.ts +++ b/packages/stats/app/src/i18n/uk.ts @@ -61,6 +61,7 @@ export const dict = { "range.2W": "2 тижні", "range.1M": "1 місяць", "range.2M": "2 місяці", + "chart.weekly": "Щотижня", "chart.daily": "Щоденно", "chart.input": "Вхід", "chart.output": "Вихід", diff --git a/packages/stats/app/src/i18n/zh.ts b/packages/stats/app/src/i18n/zh.ts index f41222bf82a..ed684c38d43 100644 --- a/packages/stats/app/src/i18n/zh.ts +++ b/packages/stats/app/src/i18n/zh.ts @@ -63,6 +63,7 @@ export const dict = { "range.2W": "2 周", "range.1M": "1 个月", "range.2M": "2 个月", + "chart.weekly": "每周", "chart.daily": "每日", "chart.input": "输入", "chart.output": "输出", diff --git a/packages/stats/app/src/i18n/zht.ts b/packages/stats/app/src/i18n/zht.ts index 9f603436726..218d39db5a4 100644 --- a/packages/stats/app/src/i18n/zht.ts +++ b/packages/stats/app/src/i18n/zht.ts @@ -63,6 +63,7 @@ export const dict = { "range.2W": "2 週", "range.1M": "1 個月", "range.2M": "2 個月", + "chart.weekly": "每週", "chart.daily": "每日", "chart.input": "輸入", "chart.output": "輸出", diff --git a/packages/stats/app/src/routes/compare-radar.tsx b/packages/stats/app/src/routes/compare-radar.tsx index 35ada12885f..3ed23586b94 100644 --- a/packages/stats/app/src/routes/compare-radar.tsx +++ b/packages/stats/app/src/routes/compare-radar.tsx @@ -22,6 +22,12 @@ type RadarAxis = { label: string description: string score: (model: ModelCatalogEntry) => number | undefined + capability?: "reasoning" | "toolCall" +} + +type RadarScore = { + value: number + fallback?: string } type RadarPoint = { @@ -37,7 +43,7 @@ export function ComparisonRadar(props: ComparisonRadarProps) { name: model.name, labName: model.labName, color: radarColors[index % radarColors.length], - scores: axes().map((axis) => (model.catalog ? axis.score(model.catalog) : undefined)), + scores: axes().map((axis) => resolveRadarScore(axis, model.catalog)), })), ) const accessibleDescription = createMemo(() => @@ -62,6 +68,9 @@ export function ComparisonRadar(props: ComparisonRadarProps) { {model.name} {(name) => {name()}} + score.fallback)}> + Hollow points use fallbacks · hover for details + )} @@ -89,10 +98,16 @@ export function ComparisonRadar(props: ComparisonRadarProps) { {(score, index) => { - const point = () => radarPoint(index(), axes().length, score ?? 0) + const point = () => radarPoint(index(), axes().length, score.value) return ( <> - + {axes()[activeAxis() ?? 0]?.label}

    {axes()[activeAxis() ?? 0]?.description}

    + + {(model) => ( +

    + {model.name}: {formatRadarScore(model.scores[activeAxis() ?? 0])} +

    + )} +
    @@ -166,7 +188,7 @@ export function ComparisonRadar(props: ComparisonRadarProps) { ) } -function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[] { +export function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[] { const benchmarks = benchmarkScoreGroups(catalogModels) const toolUseBenchmarks = benchmarkScoreGroups(catalogModels, true) const costs = catalogModels.flatMap((model) => { @@ -180,9 +202,10 @@ function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[ return [ { label: "Reasoning", - description: "Ability to solve complex, multi-step problems. Based on reasoning benchmarks when available.", - score: (model) => - benchmarkPercentile(model, benchmarks, reasoningBenchmarkPattern) ?? (model.reasoning ? 100 : 0), + capability: "reasoning", + description: + "Ability to solve complex, multi-step problems. Benchmarks take priority; reasoning support defaults to 50/100.", + score: (model) => benchmarkPercentile(model, benchmarks, reasoningBenchmarkPattern), }, { label: "Coding", @@ -218,7 +241,8 @@ function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[ }, { label: "Tool use", - description: "Performance on agent benchmarks including Terminal-Bench, Tau3, and Claw-Eval.", + capability: "toolCall", + description: "Agent benchmark performance. Benchmarks take priority; tool calling support defaults to 50/100.", score: (model) => benchmarkPercentile(model, toolUseBenchmarks, toolUseBenchmarkPattern, { aggregate: "average", @@ -324,9 +348,9 @@ function radarPolygonPoints(count: number, score: number) { .join(" ") } -function radarSeriesPolygon(scores: (number | undefined)[]) { +function radarSeriesPolygon(scores: RadarScore[]) { return scores - .map((score, index) => radarPoint(index, scores.length, score ?? 0)) + .map((score, index) => radarPoint(index, scores.length, score.value)) .map((point) => `${point.x},${point.y}`) .join(" ") } @@ -355,6 +379,17 @@ function roundRadarCoordinate(value: number) { return Math.round(value * 1000) / 1000 } -function formatRadarScore(score: number | undefined) { - return score === undefined ? "No data" : `${Math.round(score)}/100` +function formatRadarScore(score: RadarScore) { + return `${Math.round(score.value)}/100${score.fallback ? ` — ${score.fallback}` : ""}` +} + +export function resolveRadarScore(axis: RadarAxis, model: ModelCatalogEntry | null): RadarScore { + const score = model ? axis.score(model) : undefined + if (score !== undefined) return { value: score } + const supported = axis.capability ? model?.[axis.capability] : undefined + if (supported === undefined) return { value: 50, fallback: "No data; neutral placeholder" } + const capability = axis.capability === "toolCall" ? "Tool calling" : "Reasoning" + return supported + ? { value: 50, fallback: `${capability} supported; no comparable benchmark` } + : { value: 0, fallback: `${capability} not supported` } } diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css index e3e2de48e08..472280cca33 100644 --- a/packages/stats/app/src/routes/index.css +++ b/packages/stats/app/src/routes/index.css @@ -1545,6 +1545,25 @@ body { background: var(--stats-layer-2); } +[data-page="stats"] [data-slot="leaderboard-period"] { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 12px; + margin-top: 24px; + color: var(--stats-muted); + font-size: 13px; +} + +[data-page="stats"] [data-slot="leaderboard-period"] [data-component="usage-filter"] button { + width: auto; + padding: 0 8px; +} + +[data-page="stats"] [data-slot="leaderboard-period"] + [data-component="leaderboard"] { + margin-top: 12px; +} + [data-page="stats"] [data-component="chart-tooltip"] { position: absolute; right: auto; @@ -6291,8 +6310,7 @@ body { vector-effect: non-scaling-stroke; } -[data-page="stats"] [data-slot="compare-radar-area"], -[data-page="stats"] [data-slot="compare-radar-line"] { +[data-page="stats"] [data-slot="compare-radar-area"] { stroke: currentColor; stroke-width: 1.5px; stroke-linejoin: round; @@ -6304,10 +6322,6 @@ body { fill-opacity: 0.09; } -[data-page="stats"] [data-slot="compare-radar-line"] { - fill: none; -} - [data-page="stats"] [data-slot="compare-radar-point"] { fill: currentColor; stroke: currentColor; @@ -6315,6 +6329,10 @@ body { vector-effect: non-scaling-stroke; } +[data-page="stats"] [data-slot="compare-radar-point"][data-fallback="true"] { + fill: var(--stats-bg); +} + [data-page="stats"] [data-slot="compare-radar-point-hit"] { fill: transparent; cursor: pointer; diff --git a/packages/stats/app/src/routes/index.tsx b/packages/stats/app/src/routes/index.tsx index b31df5e832d..32de6502b8d 100644 --- a/packages/stats/app/src/routes/index.tsx +++ b/packages/stats/app/src/routes/index.tsx @@ -65,7 +65,7 @@ type StatsHomePageData = { updatedAt: string | null usage: UsagePoint[] users: UsagePoint[] - leaderboard: LeaderboardEntry[] + leaderboard: { daily: LeaderboardEntry[]; weekly: LeaderboardEntry[] } market: MarketDay[] tokenCost: TokenCostEntry[] cacheRatio: CacheRatioEntry[] @@ -86,7 +86,7 @@ const getData = query(async () => { updatedAt: stats.updatedAt, usage: stats.usage.Go["2M"], users: stats.users.Go["2M"], - leaderboard: stats.leaderboard.Go["2M"], + leaderboard: { daily: stats.leaderboard.Go["1D"], weekly: stats.leaderboard.Go["1W"] }, market: stats.market["2M"], tokenCost: priceTokenCostFromCatalog(stats.tokenCost.Go, catalog), cacheRatio: stats.cacheRatio.Go, @@ -162,7 +162,7 @@ export default function StatsHome() { () + const [period, setPeriod] = createSignal<"weekly" | "daily">("weekly") + const leaderboard = createMemo(() => props.leaderboard[period()]) return (
    @@ -397,14 +399,41 @@ function TopModelsSection(props: { > +
    +
    + + +
    +
    0} + when={leaderboard().length > 0} fallback={ } > void catalogLabs: readonly string[] @@ -833,6 +863,7 @@ function Leaderboard(props: { {(entry) => ( ( ( void @@ -882,6 +916,7 @@ function LeaderboardCard(props: { const i18n = useI18n() const language = useLanguage() const hasProvider = () => isKnownCatalogLab(props.entry.provider, props.catalogLabs) + const icon = () => (hasProvider() ? getProviderIconId(props.entry.author) : "synthetic") return ( props.onActiveModelChange(props.entry.model)} > {String(props.entry.rank).padStart(2, "0")} - - +