Compare commits

..
Author SHA1 Message Date
Frank 083ed266e0 docs(go): simplify usage limit explanation 2026-09-28 07:34:04 -04:00
Adam acb6859b06 fix(stats): attribute usage from catalog identity (#51833) 2026-09-28 06:08:52 -05:00
opencode-agent[bot] 9f9e73ae0f chore: generate 2026-09-28 11:04:37 +00:00
Frank c1919a3477 Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-09-28 07:02:20 -04:00
Frank 482a1cdf0d doc: go plus 2026-09-28 06:43:54 -04:00
Jack 725e4ba4b8 feat(go): show Go Plus plans and limits (#51822) 2026-09-28 18:38:26 +08:00
Frank fed9d78791 Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-09-28 06:11:28 -04:00
Frank ddbeaa17c0 doc: go plus 2026-09-28 06:07:01 -04:00
Jack 03e67171ab fix(stats): hide partial-day ranking changes (#51777) 2026-09-28 13:11:35 +08:00
Jack d6963bdf24 fix(stats): localize weekly ranking option (#51776) 2026-09-28 12:52:16 +08:00
Jack 661b7c5548 feat(stats): add daily model rankings (#51774) 2026-09-28 12:44:38 +08:00
opencode 90e65205f6 sync release versions for v1.18.33 2026-09-28 04:22:44 +00:00
opencode-agent[bot]andrekram1-node 1eacc1bdb9 fix: use available model for release changelog (#51773)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-27 23:01:45 -05:00
Dan Lapid 35fc7a776c fix(opencode): apply provider timeouts to Cloudflare AI Gateway models (#51549) 2026-09-27 22:22:28 -05:00
opencode-agent[bot]andrekram1-node b471c2b449 fix(opencode): catch completed MCP browser launcher failures (#51538)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-26 14:56:38 -05:00
Tommy D. Rossi a42f393c85 docs(web): link kimaki to product website (#49735) 2026-09-26 10:55:17 -05:00
Jack b65de4d694 docs(go): add LongCat 2.5 Preview Free to V1 pages (#51488) 2026-09-26 21:16:22 +08:00
opencode-agent[bot] 696f41bc8e chore: update nix node_modules hashes 2026-09-25 23:27:56 +00:00
Aiden Cline 29f07e0c69 refactor(core): share a browser opener across opencode and tui (#51414) 2026-09-25 18:09:47 -05:00
opencode-agent[bot] adee738d1e chore: generate 2026-09-25 16:14:58 +00:00
Jack 543c686c0d fix(go): make DeepSeek $60 allowance permanent (#51364) 2026-09-26 00:13:42 +08:00
Jack 34aa427434 docs(zen): document Qwen3.8 Max (#51321) 2026-09-25 19:00:58 +08:00
opencode-agent[bot]andrekram1-node 16c56fe5ec fix(ci): use GPT-6 Luna for issue automation (#51261)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
2026-09-24 22:13:00 -05:00
Adam 6df0d5d951 fix(stats): use capability fallbacks in comparison radar (#51161) 2026-09-24 14:26:39 -05:00
JackandMrMushrooooom 0f549842ee fix(stats): attribute Hy4 preview traffic to Tencent (#51050)
Co-authored-by: MrMushrooooom <19261047+MrMushrooooom@users.noreply.github.com>
2026-09-24 14:20:31 +08:00
Jack 0027387dc5 fix(go): include DeepSeek V4 Flash in chart (#51032) 2026-09-24 12:32:44 +08:00
169 changed files with 6449 additions and 3127 deletions
+11 -15
View File
@@ -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') }}
+2 -2
View File
@@ -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 }}.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
---
mode: primary
hidden: true
model: opencode/gpt-5.4-mini
model: opencode/gpt-6-luna
color: "#44BA81"
tools:
"*": false
+1 -1
View File
@@ -1,5 +1,5 @@
---
model: opencode/gpt-5.4
model: opencode/gpt-6-luna
---
Create `UPCOMING_CHANGELOG.md` from the structured changelog input below.
+1 -1
View File
@@ -1,6 +1,6 @@
---
description: translate English to other languages
model: opencode/gpt-5.6-sol
model: opencode/gpt-6-sol
---
run git diff and translate changed english doc and UI copy files to other international languages. Translate all languages in parallel to save time.
+36 -44
View File
@@ -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=="],
+4 -4
View File
@@ -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="
}
}
+2 -1
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.32",
"version": "1.18.33",
"description": "",
"type": "module",
"exports": {
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.18.32",
"version": "1.18.33",
"type": "module",
"license": "MIT",
"scripts": {
@@ -4,19 +4,15 @@ export const goModels = [
{ id: "qwen3.8-max", name: "Qwen3.8 Max", requests: 160, allowance: 15 },
{ id: "grok-4.7", name: "Grok 4.7", requests: 169, allowance: 15 },
{ id: "grok-4.6", name: "Grok 4.6", requests: 169, allowance: 15 },
{ id: "qwen3.7-max", name: "Qwen3.7 Max", requests: 170, allowance: 30 },
{ id: "glm-5.3", name: "GLM-5.3", requests: 220, allowance: 15 },
{ id: "glm-5.2", name: "GLM-5.2", requests: 880, allowance: 60 },
{ id: "glm-5.1", name: "GLM-5.1", requests: 880, allowance: 60 },
{ id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", requests: 1050, allowance: 15 },
{ id: "kimi-k2.6", name: "Kimi K2.6", requests: 1150, allowance: 60 },
{ id: "kimi-k2.7-code", name: "Kimi K2.7 Code", requests: 1350, allowance: 60, featured: true },
{ id: "hy4-preview", name: "Hy4 preview", requests: 1350, allowance: 30 },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", requests: 2050, allowance: 15 },
{ id: "minimax-m3", name: "MiniMax M3", requests: 3200, allowance: 60, featured: true },
{ id: "mimo-v2.6-pro", name: "MiMo-V2.6-Pro", requests: 3250, allowance: 15, fresh: true },
{ id: "mimo-v2.5-pro", name: "MiMo-V2.5-Pro", requests: 3250, allowance: 15 },
{ id: "qwen3.6-plus", name: "Qwen3.6 Plus", requests: 3300, allowance: 60 },
{ id: "minimax-m2.7", name: "MiniMax M2.7", requests: 3400, allowance: 60 },
{ id: "deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision Exp", requests: 6500, allowance: 15 },
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", requests: 4300, allowance: 60, featured: true },
@@ -28,14 +24,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 +63,52 @@ 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)
// Go Plus limits are published per model in the Console, not a fixed multiplier of Go.
const plusLimits: Record<string, [number, number]> = {
"kimi-k3": [440, 60],
"qwen3.8-max": [640, 60],
"grok-4.7": [676, 60],
"grok-4.6": [676, 60],
"glm-5.3": [1760, 120],
"glm-5.2": [2640, 180],
"deepseek-v4-pro": [4200, 60],
"kimi-k2.7-code": [4050, 180],
"hy4-preview": [5400, 120],
"gpt-5.6-luna": [8200, 60],
"minimax-m3": [9600, 180],
"mimo-v2.6-pro": [13000, 60],
"mimo-v2.5-pro": [13000, 60],
"minimax-m2.7": [13600, 240],
"deepseek-v4-flash-vision-exp": [26000, 60],
"qwen3.7-plus": [12900, 180],
"gpt-6-luna": [16920, 60],
hy3: [17200, 240],
"qwen3.8-flash": [16200, 90],
"glm-5.3-flash": [18960, 180],
"deepseek-flash": [52000, 120],
"longcat-2.0": [45600, 240],
"deepseek-v4-flash": [52000, 120],
"mimo-v2.6-flash": [60200, 120],
"mimo-v2.5": [60200, 120],
"muse-spark-1.3-contributor": [90600, 120],
"muse-spark-1.2-contributor": [90600, 120],
"space-bunny-free": [Infinity, Infinity],
"longcat-2.5-preview-free": [Infinity, Infinity],
}
export const goPlanModels = goModels.map((model) => ({
...model,
plusRequests: plusLimits[model.id]![0],
plusAllowance: plusLimits[model.id]![1],
}))
@@ -0,0 +1,288 @@
@import "@opencode-ai/ui/v2/styles/colors.css";
@font-face {
font-family: "Inter";
src: url("@opencode-ai/ui/fonts/Inter.ttf") format("truetype");
font-style: normal;
font-weight: 100 900;
font-display: swap;
}
[data-page="go"] {
--go-console-bg: var(--v2-grey-50);
--go-console-surface: var(--v2-grey-100);
--go-console-track: var(--v2-grey-200);
--go-console-text: var(--v2-grey-1100);
--go-console-secondary: var(--v2-grey-700);
--go-console-muted: var(--v2-grey-600);
--go-console-border: var(--v2-alpha-dark-8);
--go-console-icon: var(--v2-grey-600);
@media (prefers-color-scheme: dark) {
--go-console-bg: var(--v2-grey-1000);
--go-console-surface: var(--v2-grey-900);
--go-console-track: var(--v2-grey-800);
--go-console-text: var(--v2-grey-200);
--go-console-secondary: var(--v2-grey-500);
--go-console-muted: var(--v2-grey-600);
--go-console-border: var(--v2-alpha-light-8);
--go-console-icon: var(--v2-grey-600);
}
}
[data-component="go-plan-chart"] {
margin: 0;
padding: 2rem var(--padding) 3rem;
background: var(--go-console-bg);
color: var(--go-console-text);
font-family: "Inter", sans-serif;
font-size: 0.8125rem;
font-weight: 440;
line-height: 1.25rem;
letter-spacing: -0.04px;
text-rendering: geometricPrecision;
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
[data-slot="heading"] {
display: flex;
align-items: end;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
h2 {
font-size: 0.9375rem;
font-weight: 530;
line-height: 1.25rem;
margin-bottom: 0.25rem;
}
[data-slot="heading"] p {
color: var(--go-console-muted);
line-height: 1.25rem;
}
[data-slot="tier-switch"] {
display: flex;
align-items: center;
gap: 0.25rem;
}
[data-slot="tier-switch"] i {
width: 3px;
height: 12px;
border-radius: 1px;
}
[data-tier="go"] {
background: linear-gradient(var(--v2-yellow-500), var(--v2-yellow-600));
}
[data-tier="plus"] {
background: linear-gradient(var(--v2-orange-500), var(--v2-orange-600));
}
button {
display: flex;
align-items: center;
gap: 0.5rem;
background: none;
border: 0;
color: inherit;
font: inherit;
font-weight: 530;
padding: 0.375rem 0.5rem;
cursor: pointer;
white-space: nowrap;
}
[data-slot="tier-switch"] button {
color: var(--go-console-secondary);
border-radius: 4px;
transition:
background-color 180ms ease,
opacity 180ms ease;
}
[data-slot="tier-switch"] button[aria-pressed="false"] {
opacity: 0.4;
font-weight: 440;
}
[data-slot="tier-switch"] button[aria-pressed="false"]:is(:hover, :focus-visible) {
opacity: 0.75;
}
[data-slot="tier-switch"] button[aria-pressed="true"] {
background: var(--go-console-surface);
color: var(--go-console-text);
}
[data-slot="tier-switch"] button:hover {
color: var(--go-console-text);
}
[data-slot="expand"][aria-expanded="true"] svg {
transform: rotate(180deg);
}
[data-slot="scroll"] {
overflow-x: auto;
}
table {
width: 100%;
min-width: 48rem;
table-layout: fixed;
border-spacing: 0;
font-size: 0.8125rem;
}
thead {
color: var(--go-console-secondary);
}
thead tr {
background: var(--go-console-surface);
}
thead th {
padding: 0.5rem 1rem;
border-block: 1px solid var(--go-console-border);
font-weight: 440;
text-align: end;
white-space: nowrap;
}
thead th:first-child {
text-align: start;
border-inline-start: 1px solid var(--go-console-border);
border-start-start-radius: 6px;
border-end-start-radius: 6px;
}
thead th:last-child {
border-inline-end: 1px solid var(--go-console-border);
border-start-end-radius: 6px;
border-end-end-radius: 6px;
}
tbody th {
padding: 0.5rem 1rem;
text-align: start;
font-weight: 400;
color: var(--go-console-text);
}
[data-slot="model"] {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 0.375rem;
}
[data-slot="model"] small {
border: 1px solid var(--go-console-border);
padding: 0 0.25rem;
font-size: 0.625rem;
}
[data-slot="model"] a {
display: inline-flex;
align-items: center;
color: var(--go-console-muted);
text-decoration: none;
}
[data-slot="bars"] {
position: relative;
}
[data-slot="gridline"] {
position: absolute;
inset-block: 0;
border-inline-start: 1px dashed var(--go-console-border);
transition: inset-inline-start 600ms cubic-bezier(0.22, 1, 0.36, 1);
}
[data-slot="track"] {
position: relative;
display: flex;
gap: 0.25rem;
height: 0.5rem;
margin: 0.5rem 0;
}
[data-slot="track"] > [data-tier] {
position: relative;
border-radius: 2px;
background: linear-gradient(var(--v2-yellow-500), var(--v2-yellow-600));
transition: width 600ms cubic-bezier(0.22, 1, 0.36, 1);
}
[data-slot="track"] > [data-tier]::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(var(--v2-orange-500), var(--v2-orange-600));
opacity: 0;
transition: opacity 300ms ease;
}
[data-slot="track"] > [data-tier="plus"]::after {
opacity: 1;
}
[data-slot="remainder"] {
flex: 1;
border-radius: 2px;
background: var(--go-console-track);
}
[data-slot="number"] {
padding: 0.5rem 1rem;
text-align: end;
white-space: nowrap;
}
[data-slot="number"] span {
display: block;
line-height: 1.25rem;
}
[data-slot="plan-value"] {
animation: go-plan-value-in 300ms ease-out both;
}
[data-slot="axis"] {
position: relative;
height: 2rem;
color: var(--go-console-muted);
font-size: 0.6875rem;
}
[data-slot="axis"] span {
position: absolute;
top: 0.25rem;
transform: translateX(-50%);
transition: inset-inline-start 600ms cubic-bezier(0.22, 1, 0.36, 1);
}
&:dir(rtl) [data-slot="axis"] span {
transform: translateX(50%);
}
figcaption {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-top: 0.75rem;
border-top: 1px solid var(--go-console-border);
padding-top: 0.75rem;
}
[data-slot="expand"] {
padding-inline: 0;
}
figcaption a {
color: var(--go-console-secondary);
}
:is(button, a):focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
@media (max-width: 55rem) {
[data-slot="heading"] {
align-items: start;
flex-direction: column;
}
}
@media (prefers-reduced-motion: reduce) {
[data-slot="tier-switch"] button,
[data-slot="gridline"],
[data-slot="track"] > [data-tier],
[data-slot="track"] > [data-tier]::after,
[data-slot="axis"] span {
transition: none;
}
[data-slot="plan-value"] {
animation: none;
}
}
}
@keyframes go-plan-value-in {
from {
opacity: 0.3;
transform: translateY(3px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@@ -0,0 +1,171 @@
import "./go-plan-chart.css"
import { createMemo, createSignal, createUniqueId, For, Show } from "solid-js"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { goPlanModels } from "./go-models"
const ticks = [100, 1000, 10000]
export function GoPlanChart(props: { href: string }) {
const i18n = useI18n()
const language = useLanguage()
const id = createUniqueId()
const [expanded, setExpanded] = createSignal(false)
const [tier, setTier] = createSignal<"go" | "go-plus">("go")
const models = createMemo(() => goPlanModels.filter((model) => expanded() || model.featured))
const max = createMemo(() =>
Math.max(
...goPlanModels.map((model) => (tier() === "go" ? model.requests : model.plusRequests)).filter(Number.isFinite),
),
)
const position = (requests: number) =>
Number.isFinite(requests)
? 4 + Math.pow(Math.log10(Math.max(requests / 100, 1)) / Math.log10(max() / 100), 2.2) * 78
: 100
const format = createMemo(() => new Intl.NumberFormat(language.tag(language.locale())))
const currency = createMemo(
() =>
new Intl.NumberFormat(language.tag(language.locale()), {
style: "currency",
currency: "USD",
currencyDisplay: "narrowSymbol",
maximumFractionDigits: 0,
}),
)
return (
<figure data-component="go-plan-chart" dir={language.dir(language.locale())} aria-labelledby={`${id}-title`}>
<div data-slot="heading">
<div>
<h2 id={`${id}-title`}>{i18n.t("go.plans.limits")}</h2>
<p>{i18n.t("go.plans.description")}</p>
</div>
<div data-slot="tier-switch" role="group" aria-label={i18n.t("go.plans.legend")}>
<button type="button" aria-pressed={tier() === "go"} onClick={() => setTier("go")}>
<i data-tier="go" />
Go
</button>
<button type="button" aria-pressed={tier() === "go-plus"} onClick={() => setTier("go-plus")}>
<i data-tier="plus" />
Go Plus
</button>
</div>
</div>
<div data-slot="scroll">
<table id={id}>
<colgroup>
<col style={{ width: "26%" }} />
<col style={{ width: "40%" }} />
<col style={{ width: "17%" }} />
<col style={{ width: "17%" }} />
</colgroup>
<thead>
<tr>
<th scope="col">{i18n.t("go.graph.model")}</th>
<th scope="col" colspan="2">
{i18n.t("go.graph.requests")}
</th>
<th scope="col">{i18n.t("go.graph.allowance")}</th>
</tr>
</thead>
<tbody>
<For each={models()}>
{(model) => (
<tr>
<th scope="row">
<span data-slot="model">
<bdi>{model.name}</bdi>
<Show when={model.fresh}>
<small>{i18n.t("go.graph.new")}</small>
</Show>
<Show when={model.limitedTime}>
<small>{i18n.t("go.graph.limitedTime")}</small>
</Show>
<Show when={model.regions}>
<a
href="https://ai.developer.meta.com/legal/geographic-use-policy"
title={i18n.t("go.graph.limitedRegions")}
aria-label={`${model.name}: ${i18n.t("go.graph.limitedRegions")}`}
>
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" aria-hidden="true">
<circle cx="8" cy="8" r="6" stroke="currentColor" />
<ellipse cx="8" cy="8" rx="2.5" ry="6" stroke="currentColor" />
<path d="M2 8h12" stroke="currentColor" />
</svg>
</a>
</Show>
</span>
</th>
<td data-slot="bars" aria-hidden="true">
<For each={ticks}>
{(tick) => <span data-slot="gridline" style={{ "inset-inline-start": `${position(tick)}%` }} />}
</For>
<div data-slot="track">
<span
data-tier={tier() === "go" ? "go" : "plus"}
style={{ width: `${position(tier() === "go" ? model.requests : model.plusRequests)}%` }}
/>
<span data-slot="remainder" />
</div>
</td>
<td data-slot="number">
<Show when={tier()} keyed>
{(selected) => (
<span data-slot="plan-value">
{format().format(selected === "go" ? model.requests : model.plusRequests)}
</span>
)}
</Show>
</td>
<td data-slot="number">
<Show when={tier()} keyed>
{(selected) => (
<span data-slot="plan-value">
{Number.isFinite(selected === "go" ? model.allowance : model.plusAllowance)
? currency().format(selected === "go" ? model.allowance : model.plusAllowance)
: "∞"}
</span>
)}
</Show>
</td>
</tr>
)}
</For>
</tbody>
<tfoot aria-hidden="true">
<tr>
<td />
<td data-slot="axis">
<For each={ticks}>
{(tick) => (
<span style={{ "inset-inline-start": `${position(tick)}%` }}>
{tick < 1000 ? format().format(tick) : `${tick / 1000}K`}
</span>
)}
</For>
</td>
<td colspan="2" />
</tr>
</tfoot>
</table>
</div>
<figcaption>
<button
type="button"
data-slot="expand"
aria-expanded={expanded()}
aria-controls={id}
onClick={() => setExpanded(!expanded())}
>
{expanded()
? i18n.t("go.graph.showLess")
: i18n.t("go.graph.showAll", { count: format().format(goPlanModels.length) })}
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="m4 6 4 4 4-4" stroke="currentColor" stroke-width="1.25" />
</svg>
</button>
<a href={props.href}>{i18n.t("go.graph.usageLimits")}</a>
</figcaption>
</figure>
)
}
@@ -88,9 +88,6 @@ export function LimitsGraph(props: { href: string }) {
<Show when={model.fresh}>
<span data-slot="badge">{i18n.t("go.graph.new")}</span>
</Show>
<Show when={model.bonus}>
<span data-slot="badge">{i18n.t("go.graph.bonus", { count: model.bonus! })}</span>
</Show>
<Show when={model.limitedTime}>
<span data-slot="badge">{i18n.t("go.graph.limitedTime")}</span>
</Show>
@@ -117,16 +114,10 @@ export function LimitsGraph(props: { href: string }) {
<div data-slot="bar" style={{ "--width": `${position(model.requests)}%` }} />
</div>
<div data-slot="requests">
<Show when={model.baseRequests}>
<s>{format().format(model.baseRequests!)}</s>{" "}
</Show>
<bdi>{format().format(model.requests)}</bdi>
</div>
</div>
<div role="cell" data-slot="allowance" data-high={model.allowance >= 60 ? "" : undefined}>
<Show when={model.baseAllowance}>
<s>{currency().format(model.baseAllowance!)}</s>{" "}
</Show>
<bdi>
<Show when={Number.isFinite(model.allowance)} fallback="∞">
<For each={currency().formatToParts(model.allowance)}>
+13 -1
View File
@@ -267,6 +267,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "اشترك في Go",
"go.cta.price": "$10/شهر",
"go.plans.month": "/شهر",
"go.plans.plus.cta": "الاشتراك في Go Plus",
"go.plans.plus.description": "تبلغ تكلفة Go Plus ‏$40/شهر مع حدود استخدام أعلى.",
"go.plans.go.feature1": "نماذج مختارة بأسعار معقولة",
"go.plans.go.feature2": "مختبرة للبرمجة باستخدام الوكلاء",
"go.plans.go.feature3": "حدود سخية ووصول موثوق",
"go.plans.plus.feature1": "يشمل كل مزايا Go",
"go.plans.plus.feature2": "حدود أعلى لجلسات برمجة أطول وأكثر تركيزًا",
"go.plans.plus.feature3": "للمشاريع الأكبر والأكثر تطلبًا",
"go.plans.limits": "الحدود",
"go.plans.description": "الطلبات المقدرة لكل 5 ساعات وحدود الاستخدام الشهرية لكل نموذج",
"go.plans.legend": "الخطط",
"go.pricing.body": "استخدمه مع أي وكيل. $10/شهر. قم بزيادة الرصيد إذا لزم الأمر. الإلغاء في أي وقت.",
"go.graph.free": "مجاني",
"go.graph.freePill": "Big Pickle ونماذج مجانية",
@@ -375,7 +387,7 @@ export const dict = {
"go.faq.q9": "ما الفرق بين النماذج المجانية وGo؟",
"go.faq.a9":
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة قدرها 200 طلب/يوم. يقدّم Go مجموعة منسقة من النماذج مع حصص طلبات أعلى مطبقة عبر نوافذ متجددة (5 ساعات، وأسبوعية، وشهرية)، تعادل الحصص الأساسية فيها تقريبًا $12 لكل 5 ساعات و$30 في الأسبوع و$60 في الشهر؛ وقد تختلف الحصص حسب النموذج (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
"تشمل النماذج المجانية Big Pickle بالإضافة إلى النماذج الترويجية المتاحة في ذلك الوقت، مع حصة قدرها 200 طلب/يوم. يقدّم Go مجموعة منسقة من النماذج مع حصص طلبات أعلى عبر نوافذ متجددة: 20% من الحصة الشهرية لكل 5 ساعات، و50% لكل أسبوع، و100% لكل شهر. وقد تختلف الحصص حسب النموذج (تختلف أعداد الطلبات الفعلية حسب النموذج والاستخدام).",
"go.faq.q10": "هل يمكنني استرداد أموالي؟",
"go.faq.a10":
"قد تكون مؤهلًا لاسترداد أموالك إذا تم الخصم خلال آخر 14 يومًا ولم تستخدم مخصصات Go خلال فترة الفوترة تلك. {{contact}} لطلب استرداد الأموال.",
+13 -1
View File
@@ -272,6 +272,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Assinar o Go",
"go.cta.price": "$10/mês",
"go.plans.month": "/mês",
"go.plans.plus.cta": "Assinar Go Plus",
"go.plans.plus.description": "O Go Plus custa $40/mês e oferece limites maiores.",
"go.plans.go.feature1": "Modelos selecionados e acessíveis",
"go.plans.go.feature2": "Testados para programação com agentes",
"go.plans.go.feature3": "Limites generosos e acesso confiável",
"go.plans.plus.feature1": "Tudo o que está incluído no Go",
"go.plans.plus.feature2": "Limites maiores para sessões de programação mais longas e focadas",
"go.plans.plus.feature3": "Para projetos maiores e exigentes",
"go.plans.limits": "Limites",
"go.plans.description": "Solicitações estimadas a cada 5 horas e limites mensais por modelo",
"go.plans.legend": "Planos",
"go.pricing.body":
"Use com qualquer agente. $10/mês. Recarregue o crédito se necessário. Cancele a qualquer momento.",
"go.graph.free": "Grátis",
@@ -385,7 +397,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 cotas básicas de $12 por 5 horas, $30 por semana e $60 por mês; as cotas específicas podem variar por modelo (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 oferece uma seleção de modelos com cotas de requisição mais altas em janelas móveis: 20% da cota mensal a cada 5 horas, 50% por semana e 100% por mês. As cotas específicas podem variar por modelo (as contagens reais de requisições variam de acordo com o modelo e o uso).",
"go.faq.q10": "Posso receber um reembolso?",
"go.faq.a10":
"Você pode ter direito a um reembolso se a cobrança foi feita nos últimos 14 dias e você não usou sua cota do Go durante esse período de faturamento. {{contact}} para solicitar um reembolso.",
+13 -1
View File
@@ -270,6 +270,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned",
"go.plans.month": "/måned",
"go.plans.plus.cta": "Abonnér på Go Plus",
"go.plans.plus.description": "Go Plus koster $40/måned og giver højere brugsgrænser.",
"go.plans.go.feature1": "Udvalgte, prisvenlige modeller",
"go.plans.go.feature2": "Testet til agentbaseret kodning",
"go.plans.go.feature3": "Generøse grænser og pålidelig adgang",
"go.plans.plus.feature1": "Alt i Go er inkluderet",
"go.plans.plus.feature2": "Højere grænser til længere, fokuserede kodningsforløb",
"go.plans.plus.feature3": "Til større, krævende projekter",
"go.plans.limits": "Grænser",
"go.plans.description": "Anslåede anmodninger pr. 5 timer og månedlige grænser pr. model",
"go.plans.legend": "Abonnementer",
"go.pricing.body": "Brug med enhver agent. $10/måned. Tank op med kredit efter behov. Afmeld når som helst.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller",
@@ -381,7 +393,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 basiskvoter på $12 pr. 5 timer, $30 pr. uge og $60 pr. måned; modelspecifikke kvoter kan variere (det faktiske antal forespørgsler varierer efter model og brug).",
"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 i rullende perioder: 20 % af den månedlige kvote pr. 5 timer, 50 % pr. uge og 100 % pr. måned. Modelspecifikke kvoter kan variere (det faktiske antal forespørgsler varierer efter model og brug).",
"go.faq.q10": "Kan jeg få en refusion?",
"go.faq.a10":
"Du kan muligvis få en refusion, hvis opkrævningen blev foretaget inden for de seneste 14 dage, og du ikke har brugt din Go-kvote i den pågældende faktureringsperiode. {{contact}} for at anmode om en refusion.",
+13 -1
View File
@@ -272,6 +272,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go abonnieren",
"go.cta.price": "$10/Monat",
"go.plans.month": "/Monat",
"go.plans.plus.cta": "Go Plus abonnieren",
"go.plans.plus.description": "Go Plus kostet $40/Monat und bietet höhere Nutzungslimits.",
"go.plans.go.feature1": "Ausgewählte, erschwingliche Modelle",
"go.plans.go.feature2": "Für agentisches Programmieren getestet",
"go.plans.go.feature3": "Großzügige Limits und zuverlässiger Zugriff",
"go.plans.plus.feature1": "Alle Vorteile von Go",
"go.plans.plus.feature2": "Höhere Limits für längere, konzentrierte Coding-Sessions",
"go.plans.plus.feature3": "Für größere, anspruchsvolle Projekte",
"go.plans.limits": "Limits",
"go.plans.description": "Geschätzte Anfragen pro 5 Stunden und monatliche Nutzungslimits je Modell",
"go.plans.legend": "Tarife",
"go.pricing.body": "Mit jedem Agenten nutzbar. $10/Monat. Guthaben bei Bedarf aufladen. Jederzeit kündbar.",
"go.graph.free": "Kostenlos",
"go.graph.freePill": "Big Pickle und kostenlose Modelle",
@@ -383,7 +395,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 Basiskontingenten von $12 pro 5 Stunden, $30 pro Woche und $60 pro Monat; modellspezifische Kontingente können abweichen (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 bietet eine kuratierte Modellauswahl mit höheren Anfragekontingenten in rollierenden Zeitfenstern: 20 % des monatlichen Kontingents pro 5 Stunden, 50 % pro Woche und 100 % pro Monat. Modellspezifische Kontingente können abweichen (tatsächliche Anfragezahlen variieren je nach Modell und Nutzung).",
"go.faq.q10": "Kann ich eine Rückerstattung erhalten?",
"go.faq.a10":
"Du hast möglicherweise Anspruch auf eine Rückerstattung, wenn die Zahlung innerhalb der letzten 14 Tage erfolgt ist und du dein Go-Kontingent in diesem Abrechnungszeitraum nicht genutzt hast. {{contact}}, um eine Rückerstattung anzufordern.",
+13 -1
View File
@@ -267,6 +267,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Subscribe to Go",
"go.cta.price": "$10/month",
"go.plans.month": "/month",
"go.plans.plus.cta": "Subscribe to Go Plus",
"go.plans.plus.description": "Go Plus costs $40/month with higher limits.",
"go.plans.go.feature1": "Curated, affordable models",
"go.plans.go.feature2": "Tested for agentic coding",
"go.plans.go.feature3": "Generous limits and reliable access",
"go.plans.plus.feature1": "Everything included in Go",
"go.plans.plus.feature2": "Higher limits and fewer interruptions",
"go.plans.plus.feature3": "Built for larger, demanding projects",
"go.plans.limits": "Limits",
"go.plans.description": "Estimated requests per 5 hours and monthly usage limits by model",
"go.plans.legend": "Plans",
"go.pricing.body": "Use with any agent. $10/month. Top up credit if needed. Cancel any time.",
"go.graph.free": "Free",
"go.graph.freePill": "Big Pickle and free models",
@@ -379,7 +391,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 base allowances of $12 per 5 hours, $30 per week, and $60 per month; model-specific allowances may differ (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 offers a curated model lineup with higher request quotas enforced across rolling windows: 20% of the monthly allowance per 5 hours, 50% per week, and 100% per month. Model-specific allowances may differ (actual request counts vary by model and usage).",
"go.faq.q10": "Can I get a refund?",
"go.faq.a10":
"You may qualify for a refund if the charge was made within the last 14 days and you have not used your Go allowance during that billing period. {{contact}} to request a refund.",
+13 -1
View File
@@ -273,6 +273,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Suscribirse a Go",
"go.cta.price": "10 $/mes",
"go.plans.month": "/mes",
"go.plans.plus.cta": "Suscribirse a Go Plus",
"go.plans.plus.description": "Go Plus cuesta 40 $/mes y ofrece límites más altos.",
"go.plans.go.feature1": "Modelos seleccionados y asequibles",
"go.plans.go.feature2": "Probados para programación con agentes",
"go.plans.go.feature3": "Límites generosos y acceso fiable",
"go.plans.plus.feature1": "Todo lo incluido en Go",
"go.plans.plus.feature2": "Límites más altos para programar durante más tiempo con concentración",
"go.plans.plus.feature3": "Para proyectos más grandes y exigentes",
"go.plans.limits": "Límites",
"go.plans.description": "Solicitudes estimadas cada 5 horas y límites mensuales por modelo",
"go.plans.legend": "Planes",
"go.pricing.body":
"Úsalo con cualquier agente. 10 $/mes. Recarga crédito si es necesario. Cancela en cualquier momento.",
"go.graph.free": "Gratis",
@@ -386,7 +398,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 cuotas base de 12 $ por 5 horas, 30 $ por semana y 60 $ por mes; las cuotas específicas pueden variar según el modelo (la cantidad real de solicitudes varía según el modelo y el uso).",
"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 en ventanas móviles: el 20 % de la cuota mensual cada 5 horas, el 50 % por semana y el 100 % por mes. Las cuotas específicas pueden variar según el modelo (la cantidad real de solicitudes varía según el modelo y el uso).",
"go.faq.q10": "¿Puedo obtener un reembolso?",
"go.faq.a10":
"Podrías tener derecho a un reembolso si el cargo se realizó en los últimos 14 días y no has utilizado tu cuota de Go durante ese periodo de facturación. {{contact}} para solicitar un reembolso.",
+13 -1
View File
@@ -274,6 +274,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "S'abonner à Go",
"go.cta.price": "10 $/mois",
"go.plans.month": "/mois",
"go.plans.plus.cta": "S'abonner à Go Plus",
"go.plans.plus.description": "Go Plus coûte 40 $/mois et offre des limites plus élevées.",
"go.plans.go.feature1": "Modèles sélectionnés et abordables",
"go.plans.go.feature2": "Testés pour le codage agentique",
"go.plans.go.feature3": "Limites généreuses et accès fiable",
"go.plans.plus.feature1": "Tous les avantages de Go",
"go.plans.plus.feature2": "Des limites plus élevées pour coder plus longtemps sans perdre le fil",
"go.plans.plus.feature3": "Pour les projets plus vastes et exigeants",
"go.plans.limits": "Limites",
"go.plans.description": "Requêtes estimées par 5 heures et limites mensuelles par modèle",
"go.plans.legend": "Offres",
"go.pricing.body":
"Utilisez-le avec n'importe quel agent. 10 $/mois. Rechargez du crédit si nécessaire. Annulez à tout moment.",
"go.graph.free": "Gratuit",
@@ -386,7 +398,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 à des quotas de base de 12 $ par 5 heures, 30 $ par semaine et 60 $ par mois ; les quotas propres à chaque modèle peuvent varier (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
"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 sur des fenêtres glissantes : 20 % du quota mensuel par 5 heures, 50 % par semaine et 100 % par mois. Les quotas propres à chaque modèle peuvent varier (le nombre réel de requêtes varie selon le modèle et l'utilisation).",
"go.faq.q10": "Puis-je obtenir un remboursement ?",
"go.faq.a10":
"Vous pourriez avoir droit à un remboursement si le paiement a été effectué au cours des 14 derniers jours et que vous n’avez pas utilisé votre quota Go pendant cette période de facturation. {{contact}} pour demander un remboursement.",
+13 -1
View File
@@ -270,6 +270,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abbonati a Go",
"go.cta.price": "$10/mese",
"go.plans.month": "/mese",
"go.plans.plus.cta": "Abbonati a Go Plus",
"go.plans.plus.description": "Go Plus costa $40/mese e offre limiti più alti.",
"go.plans.go.feature1": "Modelli selezionati e convenienti",
"go.plans.go.feature2": "Testati per la programmazione agentica",
"go.plans.go.feature3": "Limiti generosi e accesso affidabile",
"go.plans.plus.feature1": "Tutto ciò che include Go",
"go.plans.plus.feature2": "Limiti più alti per sessioni di coding più lunghe e concentrate",
"go.plans.plus.feature3": "Per progetti più grandi e impegnativi",
"go.plans.limits": "Limiti",
"go.plans.description": "Richieste stimate ogni 5 ore e limiti mensili per modello",
"go.plans.legend": "Piani",
"go.pricing.body":
"Usalo con qualsiasi agente. $10/mese. Ricarica il credito se necessario. Annulla in qualsiasi momento.",
"go.graph.free": "Gratis",
@@ -382,7 +394,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 quote base di $12 ogni 5 ore, $30 a settimana e $60 al mese; le quote specifiche possono variare in base al modello (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
"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 su finestre mobili: il 20% della quota mensile ogni 5 ore, il 50% a settimana e il 100% al mese. Le quote specifiche possono variare in base al modello (il conteggio effettivo delle richieste varia in base al modello e all'utilizzo).",
"go.faq.q10": "Posso ottenere un rimborso?",
"go.faq.a10":
"Potresti avere diritto a un rimborso se l'addebito è stato effettuato negli ultimi 14 giorni e non hai utilizzato la tua quota Go durante quel periodo di fatturazione. {{contact}} per richiedere un rimborso.",
+13 -1
View File
@@ -269,6 +269,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Goを購読する",
"go.cta.price": "$10/月",
"go.plans.month": "/月",
"go.plans.plus.cta": "Go Plus に登録",
"go.plans.plus.description": "Go Plus は月額$40で、より高い利用枠を提供します。",
"go.plans.go.feature1": "厳選された手頃なモデル",
"go.plans.go.feature2": "エージェント型コーディングで検証済み",
"go.plans.go.feature3": "十分な利用枠と安定したアクセス",
"go.plans.plus.feature1": "Go のすべての特典を含む",
"go.plans.plus.feature2": "より高い利用枠で、長時間のコーディングに集中",
"go.plans.plus.feature3": "大規模で高度なプロジェクト向け",
"go.plans.limits": "利用枠",
"go.plans.description": "モデル別の5時間あたりの推定リクエスト数と月間利用枠",
"go.plans.legend": "プラン",
"go.pricing.body":
"どのエージェントでも使えます。月額$10。必要に応じてクレジットを追加。いつでもキャンセルできます。",
"go.graph.free": "無料",
@@ -380,7 +392,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では厳選されたモデルラインナップを利用でき、ローリングウィンドウで月間利用枠の20%を5時間ごと、50%を週ごと、100%を月ごとの上限として適用します。モデル別の利用枠は異なる場合があります(実際のリクエスト数はモデルと使用状況により異なります)。",
"go.faq.q10": "返金を受けられますか?",
"go.faq.a10":
"請求から14日以内で、その請求期間中にGoの利用枠を一切使用していない場合、返金の対象となる可能性があります。返金を希望する場合は、{{contact}}ください。",
+13 -1
View File
@@ -265,6 +265,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go 구독하기",
"go.cta.price": "$10/월",
"go.plans.month": "/월",
"go.plans.plus.cta": "Go Plus 구독하기",
"go.plans.plus.description": "Go Plus는 월 $40로 더 높은 사용 한도를 제공합니다.",
"go.plans.go.feature1": "엄선된 합리적인 가격의 모델",
"go.plans.go.feature2": "에이전트 코딩에 최적화된 테스트",
"go.plans.go.feature3": "넉넉한 한도와 안정적인 접근",
"go.plans.plus.feature1": "Go의 모든 혜택 포함",
"go.plans.plus.feature2": "더 높은 한도로 장시간 코딩에 집중",
"go.plans.plus.feature3": "더 크고 복잡한 프로젝트에 적합",
"go.plans.limits": "사용 한도",
"go.plans.description": "모델별 5시간당 예상 요청 횟수 및 월간 사용 한도",
"go.plans.legend": "요금제",
"go.pricing.body":
"어떤 에이전트와도 사용할 수 있습니다. 월 $10. 필요하면 크레딧을 충전하세요. 언제든지 취소할 수 있습니다.",
"go.graph.free": "무료",
@@ -374,7 +386,7 @@ export const dict = {
"go.faq.q9": "무료 모델과 Go의 차이점은 무엇인가요?",
"go.faq.a9":
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 엄선된 모델 라인업을 제공하며, 롤링 윈도우(5시간, 주간, 월간)에 걸쳐 더 높은 요청 할당량을 적용합니다. 기본 할당량은 대략 5시간당 $12, 주당 $30, 월 $60에 해당하며 모델별 할당량은 다를 수 있습니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
"무료 모델에는 Big Pickle과 당시 사용 가능한 프로모션 모델이 포함되며, 하루 200회 요청 할당량이 적용됩니다. Go는 엄선된 모델 라인업을 제공하며, 이동 기간별 요청 한도는 5시간당 월간 한도의 20%, 주간 50%, 월간 100%입니다. 모델별 한도는 다를 수 있습니다(실제 요청 수는 모델 및 사용량에 따라 다름).",
"go.faq.q10": "환불받을 수 있나요?",
"go.faq.a10":
"결제일로부터 14일 이내이며 해당 결제 기간에 Go 사용 한도를 전혀 사용하지 않은 경우 환불 대상이 될 수 있습니다. 환불을 요청하려면 {{contact}}를 선택해 주세요.",
+13 -1
View File
@@ -270,6 +270,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Abonner på Go",
"go.cta.price": "$10/måned",
"go.plans.month": "/måned",
"go.plans.plus.cta": "Abonner på Go Plus",
"go.plans.plus.description": "Go Plus koster $40/måned og gir høyere bruksgrenser.",
"go.plans.go.feature1": "Utvalgte, rimelige modeller",
"go.plans.go.feature2": "Testet for agentbasert koding",
"go.plans.go.feature3": "Romslige grenser og pålitelig tilgang",
"go.plans.plus.feature1": "Alt i Go er inkludert",
"go.plans.plus.feature2": "Høyere grenser for lengre, fokuserte kodeøkter",
"go.plans.plus.feature3": "For større, krevende prosjekter",
"go.plans.limits": "Grenser",
"go.plans.description": "Anslåtte forespørsler per 5 timer og månedlige grenser per modell",
"go.plans.legend": "Abonnementer",
"go.pricing.body": "Bruk med hvilken som helst agent. $10/måned. Fyll på kreditt ved behov. Avslutt når som helst.",
"go.graph.free": "Gratis",
"go.graph.freePill": "Big Pickle og gratis modeller",
@@ -382,7 +394,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 basiskvoter på $12 per 5 timer, $30 per uke og $60 per måned; modellspesifikke kvoter kan variere (faktiske forespørselsantall varierer etter modell og bruk).",
"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 i rullerende perioder: 20 % av den månedlige kvoten per 5 timer, 50 % per uke og 100 % per måned. Modellspesifikke kvoter kan variere (faktisk antall forespørsler varierer etter modell og bruk).",
"go.faq.q10": "Kan jeg få refusjon?",
"go.faq.a10":
"Du kan ha rett på refusjon hvis belastningen ble gjort i løpet av de siste 14 dagene og du ikke har brukt noe av Go-kvoten i den faktureringsperioden. {{contact}} for å be om refusjon.",
+13 -1
View File
@@ -271,6 +271,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Zasubskrybuj Go",
"go.cta.price": "$10/miesiąc",
"go.plans.month": "/miesiąc",
"go.plans.plus.cta": "Subskrybuj Go Plus",
"go.plans.plus.description": "Go Plus kosztuje $40/miesiąc i oferuje wyższe limity.",
"go.plans.go.feature1": "Wyselekcjonowane, przystępne modele",
"go.plans.go.feature2": "Przetestowane do kodowania agentowego",
"go.plans.go.feature3": "Hojne limity i niezawodny dostęp",
"go.plans.plus.feature1": "Wszystko, co obejmuje Go",
"go.plans.plus.feature2": "Wyższe limity na dłuższe sesje kodowania w skupieniu",
"go.plans.plus.feature3": "Do większych, wymagających projektów",
"go.plans.limits": "Limity",
"go.plans.description": "Szacowana liczba żądań na 5 godzin i miesięczne limity dla modeli",
"go.plans.legend": "Plany",
"go.pricing.body":
"Używaj z dowolnym agentem. $10/miesiąc. Doładuj konto w razie potrzeby. Anuluj w dowolnym momencie.",
"go.graph.free": "Darmowe",
@@ -383,7 +395,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 bazowym limitom $12 na 5 godzin, $30 tygodniowo i $60 miesięcznie; limity mogą się różnić zależnie od modelu (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 oferuje starannie dobrany zestaw modeli z wyższymi limitami zapytań w oknach kroczących: 20% miesięcznego limitu na 5 godzin, 50% na tydzień i 100% na miesiąc. Limity mogą się różnić zależnie od modelu (rzeczywista liczba zapytań zależy od modelu i użycia).",
"go.faq.q10": "Czy mogę otrzymać zwrot pieniędzy?",
"go.faq.a10":
"Możesz kwalifikować się do zwrotu, jeśli opłata została pobrana w ciągu ostatnich 14 dni i nie wykorzystano żadnej części limitu Go w tym okresie rozliczeniowym. {{contact}}, aby poprosić o zwrot.",
+13 -1
View File
@@ -274,6 +274,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Подписаться на Go",
"go.cta.price": "$10/месяц",
"go.plans.month": "/мес.",
"go.plans.plus.cta": "Подписаться на Go Plus",
"go.plans.plus.description": "Go Plus стоит $40/месяц и предлагает более высокие лимиты.",
"go.plans.go.feature1": "Отобранные доступные модели",
"go.plans.go.feature2": "Проверены для агентного программирования",
"go.plans.go.feature3": "Щедрые лимиты и надёжный доступ",
"go.plans.plus.feature1": "Все возможности Go",
"go.plans.plus.feature2": "Повышенные лимиты для длительной сосредоточенной работы над кодом",
"go.plans.plus.feature3": "Для крупных и сложных проектов",
"go.plans.limits": "Лимиты",
"go.plans.description": "Примерное число запросов за 5 часов и месячные лимиты по моделям",
"go.plans.legend": "Тарифы",
"go.pricing.body":
"Используйте с любым агентом. $10/месяц. Пополняйте баланс при необходимости. Отменить можно в любое время.",
"go.graph.free": "Бесплатно",
@@ -388,7 +400,7 @@ export const dict = {
"go.faq.q9": "В чем разница между бесплатными моделями и Go?",
"go.faq.a9":
"Бесплатные модели включают Big Pickle и доступные на данный момент промо-модели с квотой 200 запросов/день. Go предлагает набор отобранных моделей с более высокими квотами запросов, применяемыми в скользящих окнах (5 часов, неделя и месяц), что примерно эквивалентно базовым лимитам $12 за 5 часов, $30 в неделю и $60 в месяц; лимиты для отдельных моделей могут отличаться (фактическое количество запросов зависит от модели и использования).",
"Бесплатные модели включают Big Pickle и доступные на данный момент промо-модели с квотой 200 запросов/день. Go предлагает набор отобранных моделей с более высокими квотами запросов в скользящих окнах: 20% месячного лимита за 5 часов, 50% за неделю и 100% за месяц. Лимиты для отдельных моделей могут отличаться (фактическое количество запросов зависит от модели и использования).",
"go.faq.q10": "Могу ли я получить возврат средств?",
"go.faq.a10":
"Вы можете претендовать на возврат, если списание произошло в течение последних 14 дней и вы не использовали лимит Go в этом расчетном периоде. {{contact}}, чтобы запросить возврат.",
+13 -1
View File
@@ -269,6 +269,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "สมัครสมาชิก Go",
"go.cta.price": "$10/เดือน",
"go.plans.month": "/เดือน",
"go.plans.plus.cta": "สมัคร Go Plus",
"go.plans.plus.description": "Go Plus ราคา $40/เดือน พร้อมขีดจำกัดการใช้งานที่สูงขึ้น",
"go.plans.go.feature1": "โมเดลคัดสรรในราคาคุ้มค่า",
"go.plans.go.feature2": "ทดสอบสำหรับการเขียนโค้ดด้วยเอเจนต์",
"go.plans.go.feature3": "ขีดจำกัดสูงและเข้าถึงได้อย่างเสถียร",
"go.plans.plus.feature1": "รวมทุกสิ่งที่มีใน Go",
"go.plans.plus.feature2": "ขีดจำกัดสูงขึ้น เพื่อเขียนโค้ดอย่างต่อเนื่องและมีสมาธิได้นานขึ้น",
"go.plans.plus.feature3": "เหมาะสำหรับโปรเจกต์ขนาดใหญ่ที่ซับซ้อน",
"go.plans.limits": "ขีดจำกัด",
"go.plans.description": "จำนวนคำขอโดยประมาณต่อ 5 ชั่วโมงและขีดจำกัดรายเดือนแยกตามโมเดล",
"go.plans.legend": "แพ็กเกจ",
"go.pricing.body": "ใช้กับเอเจนต์ใดก็ได้ $10/เดือน เติมเครดิตหากจำเป็น ยกเลิกได้ตลอดเวลา",
"go.graph.free": "ฟรี",
"go.graph.freePill": "Big Pickle และโมเดลฟรี",
@@ -379,7 +391,7 @@ export const dict = {
"go.faq.q9": "ความแตกต่างระหว่างโมเดลฟรีและ Go คืออะไร?",
"go.faq.a9":
"โมเดลฟรีประกอบด้วย Big Pickle และโมเดลโปรโมชันที่มีให้บริการในขณะนั้น โดยมีโควตา 200 คำขอ/วัน Go นำเสนอชุดโมเดลที่คัดสรร พร้อมโควตาคำขอที่สูงกว่าซึ่งบังคับใช้ตามกรอบเวลาแบบต่อเนื่อง (5 ชั่วโมง, รายสัปดาห์ และรายเดือน) เทียบเท่าโควตาพื้นฐานประมาณ $12 ต่อ 5 ชั่วโมง, $30 ต่อสัปดาห์ และ $60 ต่อเดือน โดยโควตาเฉพาะอาจแตกต่างกันไปตามโมเดล (จำนวนคำขอจริงแตกต่างกันไปตามโมเดลและการใช้งาน)",
"โมเดลฟรีประกอบด้วย Big Pickle และโมเดลโปรโมชันที่มีให้บริการในขณะนั้น โดยมีโควตา 200 คำขอ/วัน Go นำเสนอชุดโมเดลที่คัดสรร พร้อมโควตาคำขอที่สูงกว่าในกรอบเวลาแบบต่อเนื่อง ได้แก่ 20% ของโควตารายเดือนต่อ 5 ชั่วโมง 50% ต่อสัปดาห์ และ 100% ต่อเดือน โควตาเฉพาะอาจแตกต่างกันไปตามโมเดล (จำนวนคำขอจริงแตกต่างกันไปตามโมเดลและการใช้งาน)",
"go.faq.q10": "ฉันขอเงินคืนได้หรือไม่?",
"go.faq.a10":
"หากมีการเรียกเก็บเงินภายใน 14 วันที่ผ่านมาและคุณยังไม่ได้ใช้สิทธิ์การใช้งาน Go เลยในรอบการเรียกเก็บเงินนั้น คุณอาจมีสิทธิ์ได้รับเงินคืน {{contact}}เพื่อขอเงินคืน",
+13 -1
View File
@@ -272,6 +272,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Go'ya abone ol",
"go.cta.price": "Ayda 10$",
"go.plans.month": "/ay",
"go.plans.plus.cta": "Go Plus'a abone ol",
"go.plans.plus.description": "Go Plus ayda 40$'dır ve daha yüksek limitler sunar.",
"go.plans.go.feature1": "Seçilmiş, uygun fiyatlı modeller",
"go.plans.go.feature2": "Ajan destekli kodlama için test edildi",
"go.plans.go.feature3": "Cömert limitler ve güvenilir erişim",
"go.plans.plus.feature1": "Go'daki her şey dahil",
"go.plans.plus.feature2": "Daha uzun, odaklı kodlama oturumları için daha yüksek limitler",
"go.plans.plus.feature3": "Daha büyük ve zorlu projeler için",
"go.plans.limits": "Limitler",
"go.plans.description": "Model başına 5 saatlik tahmini istek ve aylık kullanım limitleri",
"go.plans.legend": "Planlar",
"go.pricing.body": "Herhangi bir ajanla kullanın. Ayda 10$. Gerekirse kredi yükleyin. İstediğiniz zaman iptal edin.",
"go.graph.free": "Ücretsiz",
"go.graph.freePill": "Big Pickle ve ücretsiz modeller",
@@ -385,7 +397,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ğerindeki temel kullanım haklarına eşdeğerdir; modele özgü kullanım hakları farklılık gösterebilir (gerçek istek sayıları modele ve kullanıma göre değişir).",
"Ü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 daha yüksek istek kotalarıyla özenle seçilmiş model seçenekleri sunar: aylık kotanın %20'si her 5 saatte, %50'si haftada ve %100'ü ayda kullanılabilir. Modele özgü kullanım hakları farklılık gösterebilir (gerçek istek sayıları modele ve kullanıma göre değişir).",
"go.faq.q10": "Para iadesi alabilir miyim?",
"go.faq.a10":
"Ücret son 14 gün içinde tahsil edildiyse ve ilgili faturalandırma döneminde Go kullanım hakkınızı hiç kullanmadıysanız para iadesine hak kazanabilirsiniz. {{contact}} ve para iadesi talep edin.",
+13 -1
View File
@@ -270,6 +270,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "Підписатися на Go",
"go.cta.price": "$10/місяць",
"go.plans.month": "/місяць",
"go.plans.plus.cta": "Підписатися на Go Plus",
"go.plans.plus.description": "Go Plus коштує $40/місяць і пропонує вищі ліміти.",
"go.plans.go.feature1": "Відібрані доступні моделі",
"go.plans.go.feature2": "Перевірені для агентного програмування",
"go.plans.go.feature3": "Щедрі ліміти й надійний доступ",
"go.plans.plus.feature1": "Усе, що входить до Go",
"go.plans.plus.feature2": "Вищі ліміти для тривалішої зосередженої роботи над кодом",
"go.plans.plus.feature3": "Для більших і складніших проєктів",
"go.plans.limits": "Ліміти",
"go.plans.description": "Орієнтовні запити за 5 годин і місячні ліміти за моделями",
"go.plans.legend": "Плани",
"go.pricing.body": "Використовуйте з будь-яким агентом. $10/місяць. Поповнюйте за потреби. Скасуйте в будь-який час.",
"go.graph.free": "Безкоштовно",
"go.graph.freePill": "Big Pickle та безкоштовні моделі",
@@ -380,7 +392,7 @@ export const dict = {
"go.faq.q9": "Яка різниця між безкоштовними моделями та Go?",
"go.faq.a9":
"Безкоштовні моделі включають Big Pickle та доступні на той момент акційні моделі з квотою 200 запитів/день. Go пропонує добірку моделей із вищими квотами запитів, що застосовуються протягом ковзних періодів (5 годин, тижня та місяця), приблизно еквівалентними базовим лімітам $12 за 5 годин, $30 на тиждень і $60 на місяць; ліміти для окремих моделей можуть відрізнятися (фактична кількість запитів залежить від моделі та використання).",
"Безкоштовні моделі включають Big Pickle та доступні на той момент акційні моделі з квотою 200 запитів/день. Go пропонує добірку моделей із вищими квотами запитів протягом ковзних періодів: 20% місячного ліміту за 5 годин, 50% за тиждень і 100% за місяць. Ліміти для окремих моделей можуть відрізнятися (фактична кількість запитів залежить від моделі та використання).",
"go.faq.q10": "Чи можу я отримати повернення коштів?",
"go.faq.a10":
"Ви можете претендувати на повернення, якщо кошти було списано протягом останніх 14 днів і ви не використали ліміт Go протягом цього розрахункового періоду. {{contact}}, щоб подати запит на повернення.",
+13 -1
View File
@@ -259,6 +259,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "订阅 Go",
"go.cta.price": "$10/月",
"go.plans.month": "/月",
"go.plans.plus.cta": "订阅 Go Plus",
"go.plans.plus.description": "Go Plus 每月 $40,提供更高的使用额度。",
"go.plans.go.feature1": "精选实惠的模型",
"go.plans.go.feature2": "专为智能体编程测试",
"go.plans.go.feature3": "充足额度与稳定访问",
"go.plans.plus.feature1": "包含 Go 的全部权益",
"go.plans.plus.feature2": "更高额度,支持更长时间的专注编码",
"go.plans.plus.feature3": "适合更大型、更复杂的项目",
"go.plans.limits": "使用额度",
"go.plans.description": "各模型每 5 小时的预计请求次数和每月使用额度",
"go.plans.legend": "套餐",
"go.pricing.body": "可配合任何代理使用。每月 $10。如有需要可充值。随时取消。",
"go.graph.free": "免费",
"go.graph.freePill": "Big Pickle 和免费模型",
@@ -361,7 +373,7 @@ export const dict = {
"go.faq.q9": "免费模型和 Go 之间的区别是什么?",
"go.faq.a9":
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 提供精选模型阵容,并在滚动窗口(5 小时、每周和每月)内执行更高的请求配额,大致相当于每 5 小时 $12、每周 $30 和每月 $60 的基础额度;具体额度可能因模型而异(实际请求计数因模型和使用情况而异)。",
"免费模型包含 Big Pickle 加上当时可用的促销模型,每天有 200 次请求的配额。Go 提供精选模型阵容,并在滚动窗口内执行更高的请求配额:每 5 小时为月额度的 20%,每周为 50%,每月为 100%。具体额度可能因模型而异(实际请求次数因模型和使用情况而异)。",
"go.faq.q10": "我可以退款吗?",
"go.faq.a10":
"如果扣款发生在过去 14 天内,并且您在该计费周期内尚未使用任何 Go 额度,您可能符合退款条件。请{{contact}}申请退款。",
+13 -1
View File
@@ -258,6 +258,18 @@ export const dict = {
"go.cta.template": "{{text}} {{price}}",
"go.cta.text": "訂閱 Go",
"go.cta.price": "$10/月",
"go.plans.month": "/月",
"go.plans.plus.cta": "訂閱 Go Plus",
"go.plans.plus.description": "Go Plus 每月 $40,提供更高的使用額度。",
"go.plans.go.feature1": "精選實惠的模型",
"go.plans.go.feature2": "專為代理程式編碼測試",
"go.plans.go.feature3": "充足額度與穩定存取",
"go.plans.plus.feature1": "包含 Go 的所有權益",
"go.plans.plus.feature2": "更高額度,支援更長時間的專注編碼",
"go.plans.plus.feature3": "適合更大型、更複雜的專案",
"go.plans.limits": "使用額度",
"go.plans.description": "各模型每 5 小時的預估請求次數及每月使用額度",
"go.plans.legend": "方案",
"go.pricing.body": "可搭配任何代理使用。每月 $10。如有需要可儲值。隨時取消。",
"go.graph.free": "免費",
"go.graph.freePill": "Big Pickle 與免費模型",
@@ -360,7 +372,7 @@ export const dict = {
"go.faq.q9": "免費模型與 Go 有什麼區別?",
"go.faq.a9":
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為 200 次請求/天。Go 提供精選模型陣容,並在滾動視窗(5 小時、每週和每月)內提供更高的請求配額,大約相當於每 5 小時 $12、每週 $30 和每月 $60 的基礎額度;具體額度可能因模型而異(實際請求數因模型和使用情況而異)。",
"免費模型包括 Big Pickle 以及當時可用的促銷模型,配額為每天 200 次請求。Go 提供精選模型陣容,並在滾動視窗內執行較高的請求配額:每 5 小時為每月額度的 20%、每週 50%、每月 100%。具體額度可能因模型而異(實際請求次數因模型和使用情況而異)。",
"go.faq.q10": "我可以退款嗎?",
"go.faq.a10":
"若扣款發生在過去 14 天內,且你在該計費期間尚未使用任何 Go 額度,你可能符合退款資格。請{{contact}}申請退款。",
@@ -1069,6 +1069,101 @@ body {
}
}
[data-slot="plans"] {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.75rem;
margin-top: 2rem;
font-family: "Inter", sans-serif;
font-size: 0.8125rem;
font-weight: 440;
line-height: 1.25rem;
letter-spacing: -0.04px;
text-rendering: geometricPrecision;
-webkit-font-smoothing: antialiased;
[data-slot="plan"] {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1.75rem;
padding: 1rem;
border: 1px solid var(--go-console-border);
border-radius: 6px;
background: var(--go-console-surface);
color: var(--go-console-text);
}
[data-slot="name"] {
color: var(--go-console-secondary);
line-height: 1.25rem;
}
[data-slot="price"] {
margin-top: 1rem;
font-size: 1.625rem;
font-weight: 610;
line-height: 2rem;
letter-spacing: -0.5px;
span {
font-size: 0.8125rem;
font-weight: 440;
letter-spacing: -0.04px;
color: var(--go-console-secondary);
}
}
ul {
display: grid;
gap: 0.5rem;
margin-top: 1.25rem;
padding: 0;
list-style: none;
color: var(--go-console-secondary);
}
li {
display: flex;
align-items: center;
gap: 0.5rem;
}
li span {
display: inline-flex;
flex: none;
color: var(--go-console-icon);
}
a {
display: flex;
align-items: center;
justify-content: center;
height: 2rem;
padding: 0 1rem;
font-size: 0.8125rem;
font-weight: 530;
border: 1px solid var(--go-console-border);
border-radius: 6px;
background: var(--go-console-bg);
color: var(--go-console-text);
text-align: center;
text-decoration: none;
}
a:hover {
background: var(--go-console-track);
}
a:focus-visible {
outline: 2px solid var(--go-console-text);
outline-offset: 2px;
}
@media (max-width: 42rem) {
grid-template-columns: 1fr;
}
}
[data-component="comparison"] {
background: var(--go-console-bg);
border-top: 0;
}
[data-component="hero"] {
padding-bottom: 2rem;
}
[data-slot="section-title"] {
margin-bottom: 24px;
+81 -45
View File
@@ -1,7 +1,7 @@
import "./index.css"
import { createAsync, query } from "@solidjs/router"
import { Title, Meta } from "@solidjs/meta"
import { For, createMemo } from "solid-js"
import { For } from "solid-js"
//import { HttpHeader } from "@solidjs/start"
import goLogoLight from "../../asset/go-ornate-light.svg"
import goLogoDark from "../../asset/go-ornate-dark.svg"
@@ -10,7 +10,7 @@ import { Faq } from "~/component/faq"
import { Legal } from "~/component/legal"
import { Footer } from "~/component/footer"
import { Header } from "~/component/header"
import { LimitsGraph } from "~/component/limits-graph"
import { GoPlanChart } from "~/component/go-plan-chart"
import { config } from "~/config"
import { getLastSeenWorkspaceID } from "../workspace/common"
import { IconMiniMax, IconMiMo, IconZai, IconAlibaba, IconDeepSeek } from "~/component/icon"
@@ -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" },
@@ -33,10 +34,8 @@ const models = [
{ name: "GLM-5.3-Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "GLM-5.3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ 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: "LongCat-2.0", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "MiMo-V2.6-Pro", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "MiMo-V2.6-Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
@@ -44,9 +43,7 @@ const models = [
{ 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.8 Flash", 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: "Muse Spark 1.3 Contributor", training: "go.faq.a5.used", retention: "go.faq.a5.notZdr" },
@@ -61,23 +58,29 @@ const models = [
export default function Home() {
const workspaceID = createAsync(() => checkLoggedIn())
const subscribeUrl = createMemo(() => "/console/go")
const subscribeUrl = "https://opencode.ai/console/go"
const i18n = useI18n()
const language = useLanguage()
return (
<main data-page="go">
{/*<HttpHeader name="Cache-Control" value="public, max-age=1, s-maxage=3600, stale-while-revalidate=86400" />*/}
<Title>{i18n.t("go.title")}</Title>
<Meta name="description" content={i18n.t("go.meta.description")} />
<Meta name="description" content={`${i18n.t("go.meta.description")} ${i18n.t("go.plans.plus.description")}`} />
<LocaleLinks path="/go" />
<Meta property="og:type" content="website" />
<Meta property="og:url" content={`${config.baseUrl}${language.route("/go")}`} />
<Meta property="og:title" content={i18n.t("go.title")} />
<Meta property="og:description" content={i18n.t("go.meta.description")} />
<Meta
property="og:description"
content={`${i18n.t("go.meta.description")} ${i18n.t("go.plans.plus.description")}`}
/>
<Meta property="og:image" content="/social-share-black.png" />
<Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={i18n.t("go.title")} />
<Meta name="twitter:description" content={i18n.t("go.meta.description")} />
<Meta
name="twitter:description"
content={`${i18n.t("go.meta.description")} ${i18n.t("go.plans.plus.description")}`}
/>
<Meta name="twitter:image" content="/social-share-black.png" />
<Meta name="opencode:auth" content={workspaceID() ? "true" : "false"} />
@@ -181,46 +184,74 @@ export default function Home() {
</div>
*/}
</div>
<a href={subscribeUrl()}>
<span>
<For
each={i18n
.t("go.cta.template")
.split(/(\{\{text\}\}|\{\{price\}\})/g)
.filter(Boolean)}
>
{(part) => {
if (part === "{{text}}") return <span>{i18n.t("go.cta.text")}</span>
if (part === "{{price}}") {
return <span data-slot="cta-price">{i18n.t("go.cta.price")}</span>
}
return part
}}
</For>
</span>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.5 12L17 12M13 16.5L17.5 12L13 7.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
</a>
</div>
<div data-slot="pricing-copy">
<p>{i18n.t("go.pricing.body")}</p>
<div data-slot="plans">
<For
each={
[
{
name: "Go",
price: "$10",
cta: "go.cta.text",
features: ["go.plans.go.feature1", "go.plans.go.feature2", "go.plans.go.feature3"],
icon: "✓",
},
{
name: "Go Plus",
price: "$40",
cta: "go.plans.plus.cta",
features: ["go.plans.plus.feature1", "go.plans.plus.feature2", "go.plans.plus.feature3"],
icon: "+",
},
] as const
}
>
{(plan) => (
<div data-slot="plan">
<div>
<p data-slot="name">{plan.name}</p>
<p data-slot="price">
{plan.price} <span>{i18n.t("go.plans.month")}</span>
</p>
<ul>
<For each={plan.features}>
{(feature) => (
<li>
<span aria-hidden="true">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path
d={
plan.icon === "✓"
? "M3.53613 8.17857L6.39328 11.75L12.4647 4.25"
: "M8 2.88867V13.1109M2.88867 8H13.1109"
}
stroke="currentColor"
/>
</svg>
</span>
{i18n.t(feature)}
</li>
)}
</For>
</ul>
</div>
<a href={subscribeUrl}>{i18n.t(plan.cta)}</a>
</div>
)}
</For>
</div>
</section>
<section data-component="comparison">
<LimitsGraph href={goUsageLimits(language.locale())} />
<GoPlanChart href={goUsageLimits(language.locale())} />
</section>
<section data-component="problem">
<div data-slot="section-title">
<h3>{i18n.t("go.problem.title")}</h3>
<p>{i18n.t("go.problem.body")}</p>
<p>
{i18n.t("go.problem.body")} {i18n.t("go.plans.plus.description")}
</p>
</div>
<p>{i18n.t("go.problem.subtitle")}</p>
<ul>
@@ -242,7 +273,9 @@ export default function Home() {
<section data-component="how">
<div data-slot="section-title">
<h3>{i18n.t("go.how.title")}</h3>
<p>{i18n.t("go.how.body")}</p>
<p>
{i18n.t("go.how.body")} {i18n.t("go.plans.plus.description")}
</p>
</div>
<ul>
<li>
@@ -259,7 +292,7 @@ export default function Home() {
<div>
<strong>{i18n.t("go.how.step2.title")}</strong> -{" "}
<a href={language.route("/docs/go/#pricing")}>{i18n.t("go.how.step2.link")}</a>{" "}
{i18n.t("go.how.step2.afterLink")}
{i18n.t("go.how.step2.afterLink")} · {i18n.t("go.plans.plus.description")}
</div>
</li>
<li>
@@ -291,14 +324,17 @@ export default function Home() {
<Faq question={i18n.t("go.faq.q9")}>{i18n.t("go.faq.a9")}</Faq>
</li>
<li>
<Faq question={i18n.t("go.faq.q3")}>{i18n.t("go.faq.a3")}</Faq>
<Faq question={i18n.t("go.faq.q3")}>
{i18n.t("go.faq.a3")} {i18n.t("go.plans.plus.description")}
</Faq>
</li>
<li>
<Faq question={i18n.t("go.faq.q4")}>
{i18n.t("go.faq.a4.p1.beforePricing")}{" "}
<a href={language.route("/docs/go/#pricing")}>{i18n.t("go.faq.a4.p1.pricingLink")}</a>{" "}
{i18n.t("go.faq.a4.p1.afterPricing")} {i18n.t("go.faq.a4.p2.beforeAccount")}{" "}
<a href={subscribeUrl()}>{i18n.t("go.faq.a4.p2.accountLink")}</a>. {i18n.t("go.faq.a4.p3")}
{i18n.t("go.faq.a4.p1.afterPricing")} {i18n.t("go.plans.plus.description")}{" "}
{i18n.t("go.faq.a4.p2.beforeAccount")} <a href={subscribeUrl}>{i18n.t("go.faq.a4.p2.accountLink")}</a>
. {i18n.t("go.faq.a4.p3")}
</Faq>
</li>
<li>
@@ -662,7 +662,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
<li>Qwen3.7 Max</li>
<li>Qwen3.7 Plus</li>
<li>Qwen3.6 Plus</li>
<li>DeepSeek V4.1 Flash — {i18n.t("go.graph.bonus", { count: 4 })}</li>
<li>DeepSeek V4.1 Flash</li>
<li>DeepSeek V4 Pro</li>
<li>DeepSeek V4 Flash</li>
<li>DeepSeek V4 Flash Vision Exp</li>
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.18.32",
"version": "1.18.33",
"type": "module",
"license": "MIT",
"scripts": {
+2 -1
View File
@@ -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",
+8
View File
@@ -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)
}
+16
View File
@@ -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")
})
})
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.18.32",
"version": "1.18.33",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -2
View File
@@ -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:",
+2 -2
View File
@@ -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))
+3 -3
View File
@@ -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
+7 -4
View File
@@ -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<void, Error>
@@ -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<void, Error>((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)
})
}),
}),
@@ -125,6 +125,10 @@ export class McpOAuthProvider implements OAuthClientProvider {
}
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
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)
}
+2 -2
View File
@@ -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<Hooks>
const state = generateState()
const callbackPromise = waitForOAuthCallback(state)
const url = buildAuthorizeUrl(state)
await open(url).catch(() => undefined)
await openUrl(url).catch(() => undefined)
return {
url,
instructions:
@@ -79,17 +79,6 @@ type CopilotModel = Omit<Model, "api"> & {
const decodeModels = Schema.decodeUnknownSync(schema)
const decodeItem = Schema.decodeUnknownOption(item)
// Copilot serves these families only on /responses, so route them there when the
// live model metadata does not advertise supported_endpoints. Older chat-only
// families (gpt-5-mini, gpt-4*, Claude) stay on chat.
export function fallbackEndpoint(modelID: string): "responses" | undefined {
const match = /^gpt-(\d+)/.exec(modelID)
if (match && Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")) return "responses"
if (modelID.startsWith("grok-") || modelID.startsWith("gemini-") || modelID.startsWith("mai-code-"))
return "responses"
return undefined
}
function build(key: string, remote: SelectableItem, url: string, prev?: Model): Model {
const reasoning =
!!remote.capabilities.supports.adaptive_thinking ||
@@ -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<Ho
const role = (inputs.role || "").trim() || undefined
const url = buildAuthorizeUrl(account, role, state, pkce)
const callbackPromise = waitForOAuthCallback(account, pkce, state)
await open(url).catch(() => undefined)
await openUrl(url).catch(() => undefined)
return {
url,
+67 -49
View File
@@ -8,7 +8,6 @@ import { NoSuchModelError, type Provider as SDK } from "ai"
import { Npm } from "@opencode-ai/core/npm"
import { Hash } from "@opencode-ai/core/util/hash"
import { Plugin } from "../plugin"
import { CopilotModels } from "../plugin/github-copilot/models"
import { serviceUse } from "@opencode-ai/core/effect/service-use"
import { type LanguageModelV3 } from "@ai-sdk/provider"
import { ModelsDev } from "@opencode-ai/core/models-dev"
@@ -92,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<string, any>) {
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
@@ -238,7 +272,8 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
if (model.api.endpoint === "responses" && sdk.responses) return sdk.responses(modelID)
if (model.api.endpoint === "chat" && sdk.chat) return sdk.chat(modelID)
}
if (CopilotModels.fallbackEndpoint(modelID) === "responses") return sdk.responses(modelID)
const match = /^gpt-(\d+)/.exec(modelID)
if (match && Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")) return sdk.responses(modelID)
return sdk.chat(modelID)
},
options: {},
@@ -810,7 +845,7 @@ function custom(dep: CustomDep): Record<string, CustomLoader> {
)
}
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<string, CustomLoader> {
},
}
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<string, any>) {
async getModel(_sdk: any, modelID: string, options?: Record<string, any>) {
// 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<string, CustomLoader> {
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: {},
@@ -1497,15 +1548,11 @@ const layer = Layer.effect(
model.provider?.npm ??
provider.npm ??
existingModel?.api.npm ??
// Provider-specific SDKs that must not inherit the models.dev catalog
// default: GitHub Copilot needs its bundled SDK, and gateway models
// bypass fromModelsDevModel so they resolve their native passthrough npm
// before falling back to the catalog default.
(providerID === ProviderV2.ID.githubCopilot
? "@ai-sdk/github-copilot"
: (cloudflareGatewayNpm(providerID, apiID) ??
modelsDev[providerID]?.npm ??
"@ai-sdk/openai-compatible"))
// Config-defined gateway models bypass fromModelsDevModel, so resolve the
// native passthrough npm here before falling back to the catalog default.
cloudflareGatewayNpm(providerID, apiID) ??
modelsDev[providerID]?.npm ??
"@ai-sdk/openai-compatible"
const name = iife(() => {
if (model.name) return model.name
if (model.id && model.id !== modelID) return modelID
@@ -1799,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()
@@ -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))),
)
})
@@ -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 })
@@ -490,14 +490,3 @@ test("remaps fallback oauth model urls to the enterprise host", async () => {
expect(models.claude.api.url).toBe("https://copilot-api.ghe.example.com")
expect(models.claude.api.npm).toBe("@ai-sdk/github-copilot")
})
test("falls back to responses for Copilot families that require it", () => {
expect(CopilotModels.fallbackEndpoint("gpt-5.6-sol")).toBe("responses")
expect(CopilotModels.fallbackEndpoint("grok-4.5")).toBe("responses")
expect(CopilotModels.fallbackEndpoint("grok-4.6")).toBe("responses")
expect(CopilotModels.fallbackEndpoint("gemini-3.5-flash")).toBe("responses")
expect(CopilotModels.fallbackEndpoint("mai-code-1.1-flash")).toBe("responses")
expect(CopilotModels.fallbackEndpoint("gpt-5-mini")).toBeUndefined()
expect(CopilotModels.fallbackEndpoint("gpt-4o")).toBeUndefined()
expect(CopilotModels.fallbackEndpoint("claude-sonnet-4.6")).toBeUndefined()
})
@@ -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<Response>) {
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<string, unknown>) {
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<string, unknown> = {}) {
const config = testProviderConfig(url)
return {
@@ -1348,31 +1348,6 @@ it.instance(
},
)
it.instance(
"custom github-copilot models use the bundled Copilot SDK",
Effect.gen(function* () {
yield* set("GITHUB_TOKEN", "test-token")
const providers = yield* list
const model = providers[ProviderV2.ID.githubCopilot].models["copilot-grok-4.6"]
expect(model).toBeDefined()
expect(model.api.npm).toBe("@ai-sdk/github-copilot")
}),
{
config: {
provider: {
"github-copilot": {
models: {
"copilot-grok-4.6": {
name: "Copilot Grok 4.6",
limit: { context: 128000, output: 64000 },
},
},
},
},
},
},
)
it.instance(
"custom model inherits api.url from models.dev provider",
Effect.gen(function* () {
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.32",
"version": "1.18.33",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.32",
"version": "1.18.33",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -61,6 +61,7 @@ export const dict = {
"range.2W": "أسبوعان",
"range.1M": "شهر واحد",
"range.2M": "شهران",
"chart.weekly": "أسبوعي",
"chart.daily": "يومي",
"chart.input": "الإدخال",
"chart.output": "الإخراج",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -63,6 +63,7 @@ export const dict = {
"range.2W": "2週間",
"range.1M": "1か月",
"range.2M": "2か月",
"chart.weekly": "週次",
"chart.daily": "日次",
"chart.input": "入力",
"chart.output": "出力",
+1
View File
@@ -63,6 +63,7 @@ export const dict = {
"range.2W": "2주",
"range.1M": "1개월",
"range.2M": "2개월",
"chart.weekly": "주간",
"chart.daily": "일간",
"chart.input": "입력",
"chart.output": "출력",
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -61,6 +61,7 @@ export const dict = {
"range.2W": "2 недели",
"range.1M": "1 месяц",
"range.2M": "2 месяца",
"chart.weekly": "Еженедельно",
"chart.daily": "Ежедневно",
"chart.input": "Ввод",
"chart.output": "Вывод",
+1
View File
@@ -63,6 +63,7 @@ export const dict = {
"range.2W": "2 สัปดาห์",
"range.1M": "1 เดือน",
"range.2M": "2 เดือน",
"chart.weekly": "รายสัปดาห์",
"chart.daily": "รายวัน",
"chart.input": "อินพุต",
"chart.output": "เอาต์พุต",
+1
View File
@@ -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ı",
+1
View File
@@ -61,6 +61,7 @@ export const dict = {
"range.2W": "2 тижні",
"range.1M": "1 місяць",
"range.2M": "2 місяці",
"chart.weekly": "Щотижня",
"chart.daily": "Щоденно",
"chart.input": "Вхід",
"chart.output": "Вихід",
+1
View File
@@ -63,6 +63,7 @@ export const dict = {
"range.2W": "2 周",
"range.1M": "1 个月",
"range.2M": "2 个月",
"chart.weekly": "每周",
"chart.daily": "每日",
"chart.input": "输入",
"chart.output": "输出",
+1
View File
@@ -63,6 +63,7 @@ export const dict = {
"range.2W": "2 週",
"range.1M": "1 個月",
"range.2M": "2 個月",
"chart.weekly": "每週",
"chart.daily": "每日",
"chart.input": "輸入",
"chart.output": "輸出",
+47 -12
View File
@@ -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) {
<span>
<strong>{model.name}</strong>
<Show when={model.labName}>{(name) => <small>{name()}</small>}</Show>
<Show when={model.scores.some((score) => score.fallback)}>
<small data-slot="compare-radar-coverage">Hollow points use fallbacks · hover for details</small>
</Show>
</span>
</li>
)}
@@ -89,10 +98,16 @@ export function ComparisonRadar(props: ComparisonRadarProps) {
<polygon data-slot="compare-radar-area" points={radarSeriesPolygon(model.scores)} />
<For each={model.scores}>
{(score, index) => {
const point = () => radarPoint(index(), axes().length, score ?? 0)
const point = () => radarPoint(index(), axes().length, score.value)
return (
<>
<circle data-slot="compare-radar-point" cx={point().x} cy={point().y} r="0.95" />
<circle
data-slot="compare-radar-point"
data-fallback={score.fallback ? "true" : undefined}
cx={point().x}
cy={point().y}
r="0.95"
/>
<circle
data-slot="compare-radar-point-hit"
cx={point().x}
@@ -138,6 +153,13 @@ export function ComparisonRadar(props: ComparisonRadarProps) {
>
<strong>{axes()[activeAxis() ?? 0]?.label}</strong>
<p>{axes()[activeAxis() ?? 0]?.description}</p>
<For each={series()}>
{(model) => (
<p>
{model.name}: {formatRadarScore(model.scores[activeAxis() ?? 0])}
</p>
)}
</For>
</div>
</Show>
</div>
@@ -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` }
}
+24 -6
View File
@@ -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;
+52 -19
View File
@@ -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() {
<MarketShareSection data={stats().market} />
<GeoBreakdownSection data={stats().country} />
<ComparisonCardsSection
pairs={homeComparisonPairs(stats().leaderboard, stats().catalogLabs)}
pairs={homeComparisonPairs(stats().leaderboard.weekly, stats().catalogLabs)}
title="Model Comparisons"
description="Popular model pairs from the leaderboard."
variant="featured"
@@ -376,11 +376,13 @@ function formatUpdatedAtLabel(value: { date: string; time: string }) {
function TopModelsSection(props: {
data: UsagePoint[]
leaderboard: LeaderboardEntry[]
leaderboard: StatsHomePageData["leaderboard"]
catalogLabs: readonly string[]
}) {
const i18n = useI18n()
const [activeModel, setActiveModel] = createSignal<string>()
const [period, setPeriod] = createSignal<"weekly" | "daily">("weekly")
const leaderboard = createMemo(() => props.leaderboard[period()])
return (
<section id="top-models" data-section="top-models">
@@ -397,14 +399,41 @@ function TopModelsSection(props: {
>
<TopModelsChart data={props.data} range="2M" activeModel={activeModel()} onActiveModelChange={setActiveModel} />
</Show>
<div data-slot="leaderboard-period" role="group" aria-label={i18n.t("nav.leaderboard")}>
<div data-component="usage-filter" data-variant="range">
<button
type="button"
data-active={period() === "weekly" ? "true" : undefined}
aria-pressed={period() === "weekly"}
onClick={() => {
setPeriod("weekly")
setActiveModel(undefined)
}}
>
{i18n.t("chart.weekly")}
</button>
<button
type="button"
data-active={period() === "daily" ? "true" : undefined}
aria-pressed={period() === "daily"}
onClick={() => {
setPeriod("daily")
setActiveModel(undefined)
}}
>
{i18n.t("chart.daily")}
</button>
</div>
</div>
<Show
when={props.leaderboard.length > 0}
when={leaderboard().length > 0}
fallback={
<EmptyState title={i18n.t("home.noLeaderboardTitle")} description={i18n.t("home.noLeaderboardDescription")} />
}
>
<Leaderboard
data={props.leaderboard}
data={leaderboard()}
showChange={period() === "weekly"}
activeModel={activeModel()}
onActiveModelChange={setActiveModel}
catalogLabs={props.catalogLabs}
@@ -819,6 +848,7 @@ function formatUsers(value: number) {
function Leaderboard(props: {
data: LeaderboardEntry[]
showChange: boolean
activeModel: string | undefined
onActiveModelChange: (model: string | undefined) => void
catalogLabs: readonly string[]
@@ -833,6 +863,7 @@ function Leaderboard(props: {
{(entry) => (
<LeaderboardCard
entry={entry}
showChange={props.showChange}
size="featured"
active={props.activeModel === entry.model}
onActiveModelChange={props.onActiveModelChange}
@@ -847,6 +878,7 @@ function Leaderboard(props: {
{(entry) => (
<LeaderboardCard
entry={entry}
showChange={props.showChange}
size="compact"
active={props.activeModel === entry.model}
onActiveModelChange={props.onActiveModelChange}
@@ -860,6 +892,7 @@ function Leaderboard(props: {
{(entry) => (
<LeaderboardCard
entry={entry}
showChange={props.showChange}
size="featured"
active={props.activeModel === entry.model}
onActiveModelChange={props.onActiveModelChange}
@@ -874,6 +907,7 @@ function Leaderboard(props: {
function LeaderboardCard(props: {
entry: LeaderboardEntry
showChange: boolean
size: "featured" | "compact"
active: boolean
onActiveModelChange: (model: string | undefined) => 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 (
<a
data-component="leader-card"
@@ -900,13 +935,9 @@ function LeaderboardCard(props: {
onClick={() => props.onActiveModelChange(props.entry.model)}
>
<span data-slot="rank">{String(props.entry.rank).padStart(2, "0")}</span>
<Show when={hasProvider()}>
<ProviderIcon data-slot="leader-watermark" aria-hidden="true" id={getProviderIconId(props.entry.author)} />
</Show>
<ProviderIcon data-slot="leader-watermark" aria-hidden="true" id={icon()} />
<div data-slot="leader-body">
<Show when={hasProvider()}>
<ProviderIcon data-slot="leader-avatar" aria-hidden="true" id={getProviderIconId(props.entry.author)} />
</Show>
<ProviderIcon data-slot="leader-avatar" aria-hidden="true" id={icon()} />
<div data-slot="leader-copy">
<div>
<strong>{props.entry.model}</strong>
@@ -916,13 +947,15 @@ function LeaderboardCard(props: {
<Show when={hasProvider()} fallback={<span />}>
<span>{props.entry.author}</span>
</Show>
<span
data-slot="delta"
data-new={props.entry.change === null ? "true" : undefined}
data-negative={props.entry.change !== null && props.entry.change < 0 ? "true" : undefined}
>
{formatChange(props.entry.change, i18n)}
</span>
<Show when={props.showChange || props.entry.change === null}>
<span
data-slot="delta"
data-new={props.entry.change === null ? "true" : undefined}
data-negative={props.entry.change !== null && props.entry.change < 0 ? "true" : undefined}
>
{formatChange(props.entry.change, i18n)}
</span>
</Show>
</div>
</div>
</div>
+27 -6
View File
@@ -25,8 +25,8 @@ export type ModelCatalogEntry = {
limit?: { context?: number; output?: number }
modalities: { input: string[]; output: string[] }
openWeights: boolean
reasoning: boolean
toolCall: boolean
reasoning?: boolean
toolCall?: boolean
attachment: boolean
temperature: boolean
cost?: ModelCatalogCost
@@ -54,6 +54,7 @@ export type ModelCatalogLab = {
export type ModelCatalog = {
models: ModelCatalogEntry[]
aliases?: ModelCatalogEntry[]
labs: ModelCatalogLab[]
}
@@ -80,7 +81,8 @@ export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?
return (
catalog.models.find((entry) => entry.id.toLowerCase() === normalizedId) ??
catalog.models.find((entry) => (lab ? entry.lab === catalogLabSlug(lab) : true) && entry.slug === leaf) ??
catalog.models.find((entry) => entry.slug === leaf)
catalog.models.find((entry) => entry.slug === leaf) ??
catalog.aliases?.find((entry) => (lab ? entry.lab === catalogLabSlug(lab) : true) && entry.slug === leaf)
)
}
@@ -133,7 +135,7 @@ export function catalogSlug(value: string) {
.replace(/-{2,}/g, "-")
}
function buildModelCatalog(payload: unknown, pricingPayload?: unknown, labPayload?: unknown): ModelCatalog {
export function buildModelCatalog(payload: unknown, pricingPayload?: unknown, labPayload?: unknown): ModelCatalog {
const costs = readCatalogCosts(pricingPayload)
const labDescriptions = readCatalogLabDescriptions(payload, pricingPayload, labPayload)
const models = readCatalogModels(payload)
@@ -149,6 +151,25 @@ function buildModelCatalog(payload: unknown, pricingPayload?: unknown, labPayloa
.toSorted((a, b) => a.lab.localeCompare(b.lab) || displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate))
return {
models,
// Contributor is a serving tier of these Muse models, with its own pricing.
// Keep aliases out of the model population used to normalize benchmark scores.
aliases: ["meta/muse-spark-1.2", "meta/muse-spark-1.3"].flatMap((id) => {
const model = models.find((entry) => entry.id === id)
if (!model) return []
const alias = `${id}-contributor`
return [
{
...model,
id: alias,
slug: `${model.slug}-contributor`,
name: `${model.name} Contributor`,
cost:
costs.get(catalogIdKey(alias)) ??
costs.get(`${model.lab}/${model.slug}-contributor`) ??
costs.get(`${model.slug}-contributor`),
},
]
}),
labs: Object.values(
models.reduce<Record<string, ModelCatalogLab>>((result, model) => {
result[model.lab] = {
@@ -184,8 +205,8 @@ function readModelCatalogEntry(value: unknown): ModelCatalogEntry[] {
limit: readCatalogLimit(value.limit),
modalities: readCatalogModalities(value.modalities),
openWeights: booleanValue(value.open_weights),
reasoning: booleanValue(value.reasoning),
toolCall: booleanValue(value.tool_call),
reasoning: typeof value.reasoning === "boolean" ? value.reasoning : undefined,
toolCall: typeof value.tool_call === "boolean" ? value.tool_call : undefined,
attachment: booleanValue(value.attachment),
temperature: booleanValue(value.temperature),
cost: readCatalogCost(value.cost),
@@ -0,0 +1,74 @@
import { describe, expect, test } from "bun:test"
import { buildRadarAxes, resolveRadarScore } from "../src/routes/compare-radar"
import type { ModelCatalogEntry } from "../src/routes/model-catalog"
const model: ModelCatalogEntry = {
id: "meta/muse-spark-1.3",
lab: "meta",
slug: "muse-spark-1-3",
name: "Muse Spark 1.3",
modalities: { input: ["text", "image"], output: ["text"] },
reasoning: true,
toolCall: true,
openWeights: false,
attachment: true,
temperature: true,
weights: [],
benchmarks: [],
}
function scores(entry: ModelCatalogEntry | null, catalog = [model]) {
return Object.fromEntries(buildRadarAxes(catalog).map((axis) => [axis.label, resolveRadarScore(axis, entry)]))
}
describe("radar capability fallbacks", () => {
test("supported capabilities have visible baselines without benchmarks", () => {
const result = scores(model)
expect(result["Tool use"]).toEqual({ value: 50, fallback: "Tool calling supported; no comparable benchmark" })
expect(result.Reasoning).toEqual({ value: 50, fallback: "Reasoning supported; no comparable benchmark" })
expect(result.Coding).toEqual({ value: 50, fallback: "No data; neutral placeholder" })
})
test("explicitly unsupported capabilities remain zero", () => {
const result = scores({ ...model, reasoning: false, toolCall: false })
expect(result["Tool use"]).toEqual({ value: 0, fallback: "Tool calling not supported" })
expect(result.Reasoning).toEqual({ value: 0, fallback: "Reasoning not supported" })
})
test("unknown capabilities and unmatched models use neutral placeholders", () => {
const result = scores({ ...model, reasoning: undefined, toolCall: undefined })
expect(result["Tool use"]).toEqual({ value: 50, fallback: "No data; neutral placeholder" })
expect(result.Reasoning).toEqual({ value: 50, fallback: "No data; neutral placeholder" })
expect(Object.values(scores(null))).toEqual(Array(6).fill({ value: 50, fallback: "No data; neutral placeholder" }))
})
test("measured benchmark percentiles override fallbacks, including zero", () => {
const low = {
...model,
benchmarks: [
{ name: "Tau3", score: 20 },
{ name: "GPQA", score: 40 },
],
}
const high = {
...model,
id: "other/model",
benchmarks: [
{ name: "Tau3", score: 80 },
{ name: "GPQA", score: 90 },
],
}
expect(scores(low, [low, high])["Tool use"]).toEqual({ value: 0 })
expect(scores(low, [low, high]).Reasoning).toEqual({ value: 0 })
expect(scores(high, [low, high])["Tool use"]).toEqual({ value: 100 })
expect(scores(high, [low, high]).Reasoning).toEqual({ value: 100 })
})
test("a benchmark without comparison peers retains the capability baseline", () => {
const entry = { ...model, benchmarks: [{ name: "Tau3", score: 90 }] }
expect(scores(entry, [entry])["Tool use"]).toEqual({
value: 50,
fallback: "Tool calling supported; no comparable benchmark",
})
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.18.32",
"version": "1.18.33",
"private": true,
"type": "module",
"license": "MIT",
@@ -0,0 +1,50 @@
import { describe, expect, test } from "bun:test"
import { catalogIdentity } from "./catalog-identity"
describe("stats catalog identity", () => {
test("resolves OpenCode offerings to canonical labs", () => {
const identity = catalogIdentity({
models: { "meituan/longcat-2.5-preview": {} },
providers: {
opencode: { models: { "longcat-2.5-preview-free": { canonical_model_id: "meituan/longcat-2.5-preview" } } },
"opencode-go": {
models: { "longcat-2.5-preview-free": { canonical_model_id: "meituan/longcat-2.5-preview" } },
},
},
})
expect(identity.offerings.get("opencode/longcat-2.5-preview-free")).toBe("meituan")
expect(identity.offerings.get("opencode-go/longcat-2.5-preview-free")).toBe("meituan")
expect(identity.models.get("longcat-2.5-preview")).toBe("meituan")
})
test("does not guess a lab for a name shared by different canonical models", () => {
const identity = catalogIdentity({
models: { "lab-a/model": {}, "lab-b/model": {} },
providers: {
opencode: { models: { "model-free": { canonical_model_id: "lab-a/model" } } },
"opencode-go": { models: { model: { canonical_model_id: "lab-b/model" } } },
},
})
expect(identity.offerings.get("opencode/model-free")).toBe("lab-a")
expect(identity.offerings.get("opencode-go/model")).toBe("lab-b")
expect(identity.models.has("model")).toBe(false)
})
test("accepts provider IDs that are already canonical catalog IDs", () => {
const identity = catalogIdentity({
models: { "opencode/direct-model": {} },
providers: { opencode: { models: { "direct-model": {} } } },
})
expect(identity.offerings.get("opencode/direct-model")).toBe("opencode")
expect(identity.models.get("direct-model")).toBe("opencode")
})
test("rejects a catalog without published canonical identities", () => {
expect(() => catalogIdentity({ models: {}, providers: { opencode: { models: { model: {} } } } })).toThrow(
"Model catalog has no canonical OpenCode offerings",
)
})
})
@@ -0,0 +1,70 @@
import { statModel } from "./model-normalization"
const CATALOG_URL = "https://models.opencode.ai/catalog.json"
const STATS_PROVIDERS = ["opencode", "opencode-go"] as const
const cache: { value?: CatalogIdentity; expiresAt?: number } = {}
export type CatalogIdentity = {
offerings: ReadonlyMap<string, string>
models: ReadonlyMap<string, string>
}
export async function loadCatalogIdentity() {
if (cache.value && (cache.expiresAt ?? 0) > Date.now()) return { catalog: cache.value, stale: false }
return fetch(CATALOG_URL, { signal: AbortSignal.timeout(10_000) })
.then(async (response) => {
if (!response.ok) throw new Error(`Model catalog returned ${response.status}`)
return catalogIdentity(await response.json())
})
.then((value) => {
cache.value = value
cache.expiresAt = Date.now() + 5 * 60_000
return { catalog: value, stale: false }
})
.catch(() => ({ catalog: cache.value, stale: true }))
}
export function catalogIdentity(value: unknown): CatalogIdentity {
if (!record(value) || !record(value.models) || !record(value.providers)) throw new Error("Invalid model catalog")
const models = value.models
const providers = value.providers
const offerings = new Map<string, string>()
const candidates = new Map<string, Set<string>>()
STATS_PROVIDERS.forEach((providerID) => {
const provider = providers[providerID]
if (!record(provider) || !record(provider.models)) return
Object.entries(provider.models).forEach(([modelID, model]) => {
if (!record(model)) return
const canonicalID =
typeof model.canonical_model_id === "string"
? model.canonical_model_id
: modelID in models
? modelID
: `${providerID}/${modelID}` in models
? `${providerID}/${modelID}`
: undefined
if (!canonicalID || !(canonicalID in models)) return
const [lab, ...parts] = canonicalID.split("/")
if (!lab || parts.length === 0) return
const canonicalModel = parts.join("/")
offerings.set(`${providerID}/${modelID.toLowerCase()}`, lab)
;[modelID, canonicalModel].forEach((name) => {
const normalized = statModel(name, undefined)
candidates.set(normalized, (candidates.get(normalized) ?? new Set()).add(lab))
})
})
})
if (offerings.size === 0) throw new Error("Model catalog has no canonical OpenCode offerings")
return {
offerings,
models: new Map(
[...candidates.entries()].flatMap(([model, labs]) => (labs.size === 1 ? [[model, [...labs][0]!]] : [])),
),
}
}
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
+53 -2
View File
@@ -56,6 +56,7 @@ export declare namespace GeoStatRepo {
}) => Effect.Effect<GeoStatRow[], DatabaseError>
readonly upsert: (rows: GeoStatRow[]) => Effect.Effect<void, DatabaseError>
readonly deleteRetiredDimensions: (rows: GeoStatRow[]) => Effect.Effect<void, DatabaseError>
readonly deleteUnknownDimensions: (rows: GeoStatRow[]) => Effect.Effect<void, DatabaseError>
}
}
@@ -207,14 +208,64 @@ export class GeoStatRepo extends Context.Service<GeoStatRepo, GeoStatRepo.Servic
inArray(geoStat.dataset, scope.datasets),
inArray(geoStat.client, scope.clients),
inArray(geoStat.source, scope.sources),
or(inArray(geoStat.provider, RETIRED_STAT_PROVIDERS), inArray(geoStat.model, RETIRED_STAT_MODELS)),
or(
inArray(geoStat.provider, RETIRED_STAT_PROVIDERS),
inArray(geoStat.model, RETIRED_STAT_MODELS),
and(eq(geoStat.provider, "unknown"), eq(geoStat.model, "hy4-preview")),
),
),
),
catch: (cause) => DatabaseError.make({ cause }),
})
})
return GeoStatRepo.of({ listDaily, listByPeriod, upsert, deleteRetiredDimensions })
const deleteUnknownDimensions = Effect.fn("GeoStatRepo.deleteUnknownDimensions")(function* (rows: GeoStatRow[]) {
const scope = statRowScope(rows)
if (!scope) return
const replacements = new Set(rows.map((row) => [statPeriodKey(row), row.model, row.country].join("\u0000")))
const stale = yield* Effect.tryPromise({
try: () =>
db
.select({
id: geoStat.id,
grain: geoStat.grain,
period_key: geoStat.period_key,
dataset: geoStat.dataset,
tier: geoStat.tier,
client: geoStat.client,
source: geoStat.source,
model: geoStat.model,
country: geoStat.country,
})
.from(geoStat)
.where(
and(
eq(geoStat.provider, "unknown"),
inArray(geoStat.grain, scope.grains),
inArray(geoStat.period_key, scope.periodKeys),
inArray(geoStat.dataset, scope.datasets),
inArray(geoStat.client, scope.clients),
inArray(geoStat.source, scope.sources),
inArray(geoStat.model, [...new Set(rows.map((row) => row.model ?? "all"))]),
),
),
catch: (cause) => DatabaseError.make({ cause }),
})
const ids = stale
.filter((row) => replacements.has([statPeriodKey(row), row.model, row.country].join("\u0000")))
.map((row) => row.id)
yield* Effect.forEach(
chunks(ids, UPSERT_CHUNK_SIZE),
(chunk) =>
Effect.tryPromise({
try: () => db.delete(geoStat).where(and(eq(geoStat.provider, "unknown"), inArray(geoStat.id, chunk))),
catch: (cause) => DatabaseError.make({ cause }),
}),
{ discard: true },
)
})
return GeoStatRepo.of({ listDaily, listByPeriod, upsert, deleteRetiredDimensions, deleteUnknownDimensions })
}),
)
}
+75 -1
View File
@@ -1,10 +1,84 @@
import { describe, expect, test } from "bun:test"
import type { ModelStatMetric } from "./model"
import type { RetentionMetricRow } from "./home"
process.env.SST_RESOURCE_App = JSON.stringify({ name: "opencode", stage: "test" })
process.env.SST_RESOURCE_StatsDatabase = JSON.stringify({ url: "mysql://localhost/stats" })
const { buildRetentionEntries } = await import("./home")
const { buildRetentionEntries, buildStatsHomeData, normalizeStatRows } = await import("./home")
test("daily rankings use the latest day while weekly rankings retain seven days and their previous-period change", () => {
const rows = Array.from({ length: 14 }, (_, index) =>
[
{ model: "model-a", totalTokens: index === 13 ? 3_000_000_000 : 1_000_000_000 },
{ model: "model-b", totalTokens: index === 13 ? 2_000_000_000 : 4_000_000_000 },
].map(
(item) =>
({
periodKey: `2026-09-${String(index + 1).padStart(2, "0")}`,
updatedAt: new Date(Date.UTC(2026, 8, index + 1)),
tier: "Go",
provider: "test",
model: item.model,
sessions: 1,
uniqueUsers: 1,
inputTokens: item.totalTokens,
outputTokens: 0,
reasoningTokens: 0,
cacheReadTokens: 0,
totalTokens: item.totalTokens,
inputCostMicrocents: 0,
outputCostMicrocents: 0,
totalCostMicrocents: 0,
}) satisfies ModelStatMetric,
),
).flat()
const rankings = buildStatsHomeData(rows, [], []).leaderboard.Go
expect(rankings["1D"].map((item) => [item.model, item.tokens, item.change])).toEqual([
["model-a", 3, 200],
["model-b", 2, -50],
])
expect(rankings["1W"].map((item) => [item.model, item.tokens])).toEqual([
["model-b", 26],
["model-a", 9],
])
expect(rankings["2M"]).toEqual(rankings["1W"])
})
describe("model usage attribution", () => {
const row = {
periodKey: "2026-09-27",
updatedAt: new Date("2026-09-28T00:00:00.000Z"),
tier: "Go",
provider: "unknown",
model: "longcat-2.5-preview",
sessions: 1,
uniqueUsers: 1,
inputTokens: 50,
outputTokens: 50,
reasoningTokens: 0,
cacheReadTokens: 0,
totalTokens: 100,
inputCostMicrocents: 0,
outputCostMicrocents: 0,
totalCostMicrocents: 0,
}
test("shows historical LongCat usage under Meituan", () => {
expect(normalizeStatRows([row])).toMatchObject([{ provider: "meituan", model: "longcat-2.5-preview" }])
})
test("prefers recomputed rows over stale unknown dimensions", () => {
expect(normalizeStatRows([row, { ...row, provider: "meituan" }])).toMatchObject([
{ provider: "meituan", totalTokens: 100 },
])
})
test("keeps unknown usage when a different lab has the same model name", () => {
expect(normalizeStatRows([row, { ...row, provider: "another-lab" }])).toHaveLength(2)
})
})
describe("retention aggregates", () => {
test("pools the latest seven weekly cohorts and ranks models above the sample floor", () => {

Some files were not shown because too many files have changed in this diff Show More