Compare commits

...
Author SHA1 Message Date
opencode-agent[bot] 1216c55094 chore: generate 2026-08-26 11:20:31 +00:00
Jack ec25388937 docs: remove Ox Alpha Free (#45221) 2026-08-26 19:19:12 +08:00
opencode-agent[bot] c7134cbb01 chore: update nix node_modules hashes 2026-08-26 10:55:43 +00:00
Adam ba4d0ea8bf fix(console): validate auth redirects (#45027) 2026-08-26 05:41:23 -05:00
opencode-agent[bot] 1cc53890dc chore: update nix node_modules hashes 2026-08-26 07:47:41 +00:00
Frank ae2ea3c723 fix map inaccuracy 2026-08-26 03:40:35 -04:00
Frank 3f31551fad fix map inaccuracy 2026-08-26 03:31:48 -04:00
Frank 13c27598d3 Merge branch 'dev' of github.com:anomalyco/opencode into dev 2026-08-26 02:31:35 -04:00
Frank 2564a4f172 remove map 2026-08-26 02:31:32 -04:00
Ravitez Dondeti fd9bd448a2 docs: mention Exa and Parallel as web search backends (#38395) 2026-08-25 19:57:17 -05:00
opencode-agent[bot]andneriousy b72b50006b fix(core): recover legacy database migration history (#45061)
Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
2026-08-25 21:58:35 +02:00
Jack ac1c048e64 docs(go): add Grok 4.6 (#45042) 2026-08-26 01:27:48 +08:00
Dax 8615731d46 fix(console): rate limit checkout session creation (#45007) 2026-08-25 11:25:59 -04:00
OpeOginniandBrendan Allan a7444bf944 fix(ui): restore focus in stacked dialogs (#44928)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-08-25 19:05:49 +08:00
Frank 69aaa22793 zen: void invoice of cancelled subscription 2026-08-25 05:26:31 -04:00
opencode-agent[bot] 322e2b9dd5 chore: generate 2026-08-25 09:07:56 +00:00
Frank afd6f3b064 zen: display quota usage breakdown 2026-08-25 05:06:33 -04:00
Frank 1e86be2bc5 update inference headers 2026-08-25 04:22:20 -04:00
Nathan ThomassinandBrendan Allan a57230b80b fix(app): drop archived sessions from home list right away (#44905)
Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
2026-08-25 07:34:24 +00:00
Frank 6bb1a76e58 update inference headers 2026-08-25 03:30:20 -04:00
Frank bdcb6be649 update inference headers 2026-08-25 03:14:21 -04:00
opencode 2f36ffe35d sync release versions for v1.18.23 2026-08-25 06:30:46 +00:00
Frank 31c409a865 update inference headers 2026-08-25 01:54:38 -04:00
135 changed files with 1208 additions and 1604 deletions
+29 -51
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.22",
"version": "1.18.23",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -194,7 +194,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -221,7 +221,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -235,6 +235,7 @@
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"openai": "5.11.0",
@@ -243,7 +244,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -267,7 +268,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -287,7 +288,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.18.22",
"version": "1.18.23",
"bin": {
"opencode": "./bin/opencode",
},
@@ -381,7 +382,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"drizzle-orm": "catalog:",
@@ -435,7 +436,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -449,7 +450,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"effect": "catalog:",
},
@@ -461,7 +462,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -493,7 +494,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -509,7 +510,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -540,7 +541,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -559,7 +560,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.22",
"version": "1.18.23",
"bin": {
"opencode": "./bin/opencode",
},
@@ -690,7 +691,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -766,7 +767,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -781,7 +782,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -796,7 +797,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -836,7 +837,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -849,7 +850,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -858,32 +859,25 @@
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"d3-geo": "3.1.1",
"d3-scale": "4.0.2",
"effect": "catalog:",
"i18n-iso-countries": "7.14.0",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
"sst": "catalog:",
"topojson-client": "3.1.0",
"vite": "catalog:",
"world-atlas": "2.0.2",
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@types/bun": "catalog:",
"@types/d3-geo": "3.1.0",
"@types/d3-scale": "4.0.9",
"@types/geojson": "7946.0.16",
"@types/topojson-client": "3.1.5",
"@types/topojson-specification": "1.0.5",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:",
},
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -902,7 +896,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -944,7 +938,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -971,7 +965,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1022,7 +1016,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.22",
"version": "1.18.23",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -2843,8 +2837,6 @@
"@types/cross-spawn": ["@types/cross-spawn@6.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA=="],
"@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
"@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
"@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
@@ -2865,8 +2857,6 @@
"@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="],
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="],
@@ -2945,10 +2935,6 @@
"@types/ssri": ["@types/ssri@7.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw=="],
"@types/topojson-client": ["@types/topojson-client@3.1.5", "", { "dependencies": { "@types/geojson": "*", "@types/topojson-specification": "*" } }, "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw=="],
"@types/topojson-specification": ["@types/topojson-specification@1.0.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ=="],
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
"@types/tsscmp": ["@types/tsscmp@1.0.2", "", {}, "sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g=="],
@@ -3437,8 +3423,6 @@
"d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
"d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
"d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
"d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
@@ -5317,8 +5301,6 @@
"toolbeam-docs-theme": ["toolbeam-docs-theme@0.4.8", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-b+5ynEFp4Woe5a22hzNQm42lD23t13ZMihVxHbzjA50zdcM9aOSJTIjdJ0PDSd4/50HbBXcpHiQsz6rM4N88ww=="],
"topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
@@ -5561,8 +5543,6 @@
"workerd": ["workerd@1.20251118.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251118.0", "@cloudflare/workerd-darwin-arm64": "1.20251118.0", "@cloudflare/workerd-linux-64": "1.20251118.0", "@cloudflare/workerd-linux-arm64": "1.20251118.0", "@cloudflare/workerd-windows-64": "1.20251118.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Om5ns0Lyx/LKtYI04IV0bjIrkBgoFNg0p6urzr2asekJlfP18RqFzyqMFZKf0i9Gnjtz/JfAS/Ol6tjCe5JJsQ=="],
"world-atlas": ["world-atlas@2.0.2", "", {}, "sha512-IXfV0qwlKXpckz1FhwXVwKRjiIhOnWttOskm5CtxMsjgE/MXAYRHWJqgXOpM8IkcPBoXnyTU5lFHcYa5ChG0LQ=="],
"wrangler": ["wrangler@4.50.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.7.11", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20251118.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20251118.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20251118.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-+nuZuHZxDdKmAyXOSrHlciGshCoAPiy5dM+t6mEohWm7HpXvTHmWQGUf/na9jjWlWJHCJYOWzkA1P5HBJqrIEA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
@@ -6515,8 +6495,6 @@
"tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
"topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"tree-sitter-bash/node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="],
"tw-to-css/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-Be1I6OG6UitofhcGu2BeNzevmoQXc4Or5r/NPzwtft4=",
"aarch64-linux": "sha256-O+d+26CQIjZ08Rn8Qm3IytdDqIMbPdhzaGOZeUKAvIU=",
"aarch64-darwin": "sha256-ObS50y/oy6fM9wSGUL/wx6O0+fTWHC04mXJNd7w/2Z0=",
"x86_64-darwin": "sha256-eoR7ZSyH62Fq2ZaW2b2QqU2FC97rYxTMeEe+djT0nto="
"x86_64-linux": "sha256-fJ72uEK9rSoFL6eJk0Lwkc2TIMLZyQ7Iz83WrZE2duA=",
"aarch64-linux": "sha256-ElEwz5spFa8XFYSBiGjKlTKRFQCju/ZYDlb6h1FaKoI=",
"aarch64-darwin": "sha256-RmbrAlggOqxNFdhW+qj2tjRCpRf2NDLe68TikbGtCeA=",
"x86_64-darwin": "sha256-ZgYE0J+Dkz/kALK3kZ1jdFIZ5/BEkEaw0mXCqPon0iY="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.22",
"version": "1.18.23",
"description": "",
"type": "module",
"exports": {
@@ -1,8 +1,10 @@
import { describe, expect, test } from "bun:test"
import type { SessionV2Info } from "@opencode-ai/sdk/v2/client"
import { QueryClient } from "@tanstack/solid-query"
import type { Session, SessionV2Info } from "@opencode-ai/sdk/v2/client"
import {
applyHomeSessionEvent,
appendHomeSessionEvent,
createHomeSessionIndexCache,
HOME_V2_SESSION_PAGE_LIMIT,
loadHomeSessionIndex,
homeSessionIndexSessions,
@@ -151,4 +153,33 @@ describe("Home V2 session index", () => {
expect(homeSessionIndexRefresh("global.disposed", true).refetch).toBe(true)
expect(homeSessionIndexRefresh("session.next.moved", true).refetch).toBe(true)
})
test("removes a session from the loaded Home index", () => {
const queryClient = new QueryClient()
const cache = createHomeSessionIndexCache(queryClient, "server")
const sessions = [
{ id: "a", time: { created: 1, updated: 1 } },
{ id: "b", time: { created: 1, updated: 1 } },
] as Session[]
queryClient.setQueryData(cache.indexKey, { sessions, eventSequence: 0 })
cache.remove("a")
const index = queryClient.getQueryData<{ sessions: Session[] }>(cache.indexKey)
expect(index?.sessions.map((item) => item.id)).toEqual(["b"])
})
test("keeps the session out of the Home list when the index is not mounted", () => {
const queryClient = new QueryClient()
const cache = createHomeSessionIndexCache(queryClient, "server")
const sessions = [
{ id: "a", time: { created: 1, updated: 1 } },
{ id: "b", time: { created: 1, updated: 1 } },
] as Session[]
cache.remove("a")
expect(queryClient.getQueryData(cache.indexKey)).toBeUndefined()
expect(cache.sessions({ sessions, eventSequence: 0 }, undefined).map((item) => item.id)).toEqual(["b"])
})
})
@@ -85,6 +85,7 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
const indexKey = homeSessionIndexKey(server)
const eventsKey = homeSessionEventsKey(server)
let connected = false
const removed = new Set<string>()
return {
indexKey,
@@ -97,7 +98,8 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
queryClient.setQueryData<HomeSessionEvents>(eventsKey, (current) => trimHomeSessionEvents(current, sequence))
},
sessions(index: HomeSessionIndex | undefined, events: HomeSessionEvents | undefined) {
return homeSessionIndexSessions(index, events)
const sessions = homeSessionIndexSessions(index, events)
return removed.size === 0 ? sessions : sessions.filter((session) => !removed.has(session.id))
},
apply(event: HomeSessionEvent) {
if (!queryClient.getQueryState(indexKey)) return
@@ -116,6 +118,16 @@ export function createHomeSessionIndexCache(queryClient: QueryClient, server: st
}
queryClient.setQueryData<HomeSessionEvents>(eventsKey, { sequence: next.sequence, entries: [] })
},
remove(sessionID: string) {
removed.add(sessionID)
if (!queryClient.getQueryState(indexKey)) return
queryClient.setQueryData<HomeSessionIndex>(indexKey, (index) => {
if (!index) return index
const at = index.sessions.findIndex((session) => session.id === sessionID)
if (at === -1) return index
return { ...index, sessions: index.sessions.toSpliced(at, 1) }
})
},
refresh(event: Event["type"]) {
const result = homeSessionIndexRefresh(event, connected)
connected = result.connected
@@ -219,13 +219,15 @@ export function createHomeSessionsController(home: HomeController) {
directory: session.directory,
time: { archived: Date.now() },
}),
remove: () =>
remove: () => {
setStore(
produce((draft) => {
const match = Binary.search(draft.session, session.id, (item) => item.id)
if (match.found) draft.session.splice(match.index, 1)
}),
),
)
homeSessions().remove(session.id)
},
onError: (cause) =>
showToast({
title: language.t("common.requestFailed"),
@@ -3,6 +3,7 @@ import { produce } from "solid-js/store"
import { notifySessionTabsRemoved } from "@/components/titlebar-session-events"
import { useLanguage } from "@/context/language"
import { useSDK } from "@/context/sdk"
import { useServerSync } from "@/context/server-sync"
import { useSync } from "@/context/sync"
import { useTabs } from "@/context/tabs"
import { errorMessage } from "@/pages/layout/helpers"
@@ -15,6 +16,7 @@ export function useSessionArchive() {
const navigate = useNavigate()
const sdk = useSDK()
const sync = useSync()
const serverSync = useServerSync()
const tabs = useTabs()
const { params } = useSessionKey()
@@ -56,6 +58,7 @@ export function useSessionArchive() {
}),
)
sync().session.evict(sessionID)
serverSync().homeSessions.remove(sessionID)
navigateAfterRemoval(sessionID, session.parentID, nextSession?.id)
notifySessionTabsRemoved({ directory: sdk().directory, sessionIDs: [sessionID] })
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"type": "module",
"license": "MIT",
"scripts": {
+9 -2
View File
@@ -252,7 +252,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.banner.text": "Ox Alpha Free متاح على Go لفترة محدودة",
"go.meta.description": "يبلغ سعر Go ‏$10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body":
@@ -661,10 +660,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "بضع ثوان",
"workspace.lite.subscription.message": "أنت مشترك في OpenCode Go.",
"workspace.lite.subscription.manage": "إدارة الاشتراك",
"workspace.lite.subscription.rollingUsage": "الاستخدام المتجدد",
"workspace.lite.subscription.rollingUsage": "الاستخدام خلال 5 ساعات",
"workspace.lite.subscription.rollingQuota": "الحصة خلال 5 ساعات",
"workspace.lite.subscription.weeklyUsage": "الاستخدام الأسبوعي",
"workspace.lite.subscription.weeklyQuota": "الحصة الأسبوعية",
"workspace.lite.subscription.monthlyUsage": "الاستخدام الشهري",
"workspace.lite.subscription.monthlyQuota": "الحصة الشهرية",
"workspace.lite.subscription.resetsIn": "إعادة تعيين في",
"workspace.lite.subscription.showDetails": "إظهار التفاصيل",
"workspace.lite.subscription.hideDetails": "إخفاء التفاصيل",
"workspace.lite.subscription.model": "النموذج",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "الإجمالي",
"workspace.lite.subscription.useBalance": "استخدم رصيدك المتوفر بعد الوصول إلى حدود الاستخدام",
"workspace.lite.subscription.selectProvider":
'اختر "OpenCode Go" كمزود في إعدادات opencode الخاصة بك لاستخدام نماذج Go.',
+9 -2
View File
@@ -256,7 +256,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "seguintes exceções",
"go.title": "OpenCode Go | Modelos de codificação de baixo custo para todos",
"go.banner.text": "Ox Alpha Free está disponível no Go por tempo limitado",
"go.meta.description":
"O Go custa $10/mês, com limites generosos de uso e acesso confiável aos principais modelos de codificação.",
"go.hero.title": "Modelos de codificação de baixo custo para todos",
@@ -673,10 +672,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "alguns segundos",
"workspace.lite.subscription.message": "Você assina o OpenCode Go.",
"workspace.lite.subscription.manage": "Gerenciar Assinatura",
"workspace.lite.subscription.rollingUsage": "Uso Contínuo",
"workspace.lite.subscription.rollingUsage": "Uso de 5 horas",
"workspace.lite.subscription.rollingQuota": "Cota de 5 horas",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.weeklyQuota": "Cota Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensal",
"workspace.lite.subscription.monthlyQuota": "Cota Mensal",
"workspace.lite.subscription.resetsIn": "Reinicia em",
"workspace.lite.subscription.showDetails": "Mostrar detalhes",
"workspace.lite.subscription.hideDetails": "Ocultar detalhes",
"workspace.lite.subscription.model": "Modelo",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance": "Use seu saldo disponível após atingir os limites de uso",
"workspace.lite.subscription.selectProvider":
'Selecione "OpenCode Go" como provedor na sua configuração do opencode para usar os modelos Go.',
+9 -2
View File
@@ -254,7 +254,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "følgende undtagelser",
"go.title": "OpenCode Go | Kodningsmodeller til lav pris for alle",
"go.banner.text": "Ox Alpha Free er tilgængelig på Go i en begrænset periode",
"go.meta.description":
"Go koster $10/måned, med generøse brugsgrænser og pålidelig adgang til førende kodningsmodeller.",
"go.hero.title": "Kodningsmodeller til lav pris for alle",
@@ -669,10 +668,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "et par sekunder",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løbende forbrug",
"workspace.lite.subscription.rollingUsage": "5-timers forbrug",
"workspace.lite.subscription.rollingQuota": "5-timers kvote",
"workspace.lite.subscription.weeklyUsage": "Ugentligt forbrug",
"workspace.lite.subscription.weeklyQuota": "Ugentlig kvote",
"workspace.lite.subscription.monthlyUsage": "Månedligt forbrug",
"workspace.lite.subscription.monthlyQuota": "Månedlig kvote",
"workspace.lite.subscription.resetsIn": "Nulstiller i",
"workspace.lite.subscription.showDetails": "Vis detaljer",
"workspace.lite.subscription.hideDetails": "Skjul detaljer",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "I alt",
"workspace.lite.subscription.useBalance": "Brug din tilgængelige saldo, når du har nået forbrugsgrænserne",
"workspace.lite.subscription.selectProvider":
'Vælg "OpenCode Go" som udbyder i din opencode-konfiguration for at bruge Go-modeller.',
+9 -2
View File
@@ -256,7 +256,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "folgenden Ausnahmen",
"go.title": "OpenCode Go | Kostengünstige Coding-Modelle für alle",
"go.banner.text": "Ox Alpha Free ist für begrenzte Zeit auf Go verfügbar",
"go.meta.description":
"Go kostet $10/Monat, mit großzügigen Nutzungslimits und zuverlässigem Zugang zu führenden Coding-Modellen.",
"go.hero.title": "Kostengünstige Coding-Modelle für alle",
@@ -671,10 +670,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "einige Sekunden",
"workspace.lite.subscription.message": "Du hast OpenCode Go abonniert.",
"workspace.lite.subscription.manage": "Abo verwalten",
"workspace.lite.subscription.rollingUsage": "Fortlaufende Nutzung",
"workspace.lite.subscription.rollingUsage": "5-Stunden-Nutzung",
"workspace.lite.subscription.rollingQuota": "5-Stunden-Kontingent",
"workspace.lite.subscription.weeklyUsage": "Wöchentliche Nutzung",
"workspace.lite.subscription.weeklyQuota": "Wöchentliches Kontingent",
"workspace.lite.subscription.monthlyUsage": "Monatliche Nutzung",
"workspace.lite.subscription.monthlyQuota": "Monatliches Kontingent",
"workspace.lite.subscription.resetsIn": "Setzt zurück in",
"workspace.lite.subscription.showDetails": "Details anzeigen",
"workspace.lite.subscription.hideDetails": "Details ausblenden",
"workspace.lite.subscription.model": "Modell",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Gesamt",
"workspace.lite.subscription.useBalance": "Nutze dein verfügbares Guthaben, nachdem die Nutzungslimits erreicht sind",
"workspace.lite.subscription.selectProvider":
'Wähle "OpenCode Go" als Anbieter in deiner opencode-Konfiguration, um Go-Modelle zu verwenden.',
+9 -2
View File
@@ -253,7 +253,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "following exceptions",
"go.title": "OpenCode Go | Low cost coding models for everyone",
"go.banner.text": "Ox Alpha Free is available on Go for a limited time",
"go.meta.description": "Go costs $10/month, with generous usage limits and reliable access to leading coding models.",
"go.hero.title": "Low cost coding models for everyone",
"go.hero.body":
@@ -669,10 +668,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "a few seconds",
"workspace.lite.subscription.message": "You are subscribed to OpenCode Go.",
"workspace.lite.subscription.manage": "Manage Subscription",
"workspace.lite.subscription.rollingUsage": "Rolling Usage",
"workspace.lite.subscription.rollingUsage": "5-hour Usage",
"workspace.lite.subscription.rollingQuota": "5-hour Quota",
"workspace.lite.subscription.weeklyUsage": "Weekly Usage",
"workspace.lite.subscription.weeklyQuota": "Weekly Quota",
"workspace.lite.subscription.monthlyUsage": "Monthly Usage",
"workspace.lite.subscription.monthlyQuota": "Monthly Quota",
"workspace.lite.subscription.resetsIn": "Resets in",
"workspace.lite.subscription.showDetails": "Show details",
"workspace.lite.subscription.hideDetails": "Hide details",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits",
"workspace.lite.subscription.selectProvider":
'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.',
+9 -2
View File
@@ -257,7 +257,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "siguientes excepciones",
"go.title": "OpenCode Go | Modelos de programación de bajo coste para todos",
"go.banner.text": "Ox Alpha Free está disponible en Go por tiempo limitado",
"go.meta.description":
"Go cuesta 10 $/mes, con límites de uso generosos y acceso fiable a modelos de programación líderes.",
"go.hero.title": "Modelos de programación de bajo coste para todos",
@@ -674,10 +673,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "unos pocos segundos",
"workspace.lite.subscription.message": "Estás suscrito a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestionar Suscripción",
"workspace.lite.subscription.rollingUsage": "Uso Continuo",
"workspace.lite.subscription.rollingUsage": "Uso de 5 horas",
"workspace.lite.subscription.rollingQuota": "Cuota de 5 horas",
"workspace.lite.subscription.weeklyUsage": "Uso Semanal",
"workspace.lite.subscription.weeklyQuota": "Cuota Semanal",
"workspace.lite.subscription.monthlyUsage": "Uso Mensual",
"workspace.lite.subscription.monthlyQuota": "Cuota Mensual",
"workspace.lite.subscription.resetsIn": "Se reinicia en",
"workspace.lite.subscription.showDetails": "Mostrar detalles",
"workspace.lite.subscription.hideDetails": "Ocultar detalles",
"workspace.lite.subscription.model": "Modelo",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance": "Usa tu saldo disponible después de alcanzar los límites de uso",
"workspace.lite.subscription.selectProvider":
'Selecciona "OpenCode Go" como proveedor en tu configuración de opencode para usar los modelos Go.',
+9 -2
View File
@@ -258,7 +258,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "exceptions suivantes",
"go.title": "OpenCode Go | Modèles de code à faible coût pour tous",
"go.banner.text": "Ox Alpha Free est disponible sur Go pour une durée limitée",
"go.meta.description":
"Go coûte 10 $/mois, avec des limites d'utilisation généreuses et un accès fiable aux principaux modèles de codage.",
"go.hero.title": "Modèles de code à faible coût pour tous",
@@ -679,10 +678,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "quelques secondes",
"workspace.lite.subscription.message": "Vous êtes abonné à OpenCode Go.",
"workspace.lite.subscription.manage": "Gérer l'abonnement",
"workspace.lite.subscription.rollingUsage": "Utilisation glissante",
"workspace.lite.subscription.rollingUsage": "Utilisation sur 5 heures",
"workspace.lite.subscription.rollingQuota": "Quota sur 5 heures",
"workspace.lite.subscription.weeklyUsage": "Utilisation hebdomadaire",
"workspace.lite.subscription.weeklyQuota": "Quota hebdomadaire",
"workspace.lite.subscription.monthlyUsage": "Utilisation mensuelle",
"workspace.lite.subscription.monthlyQuota": "Quota mensuel",
"workspace.lite.subscription.resetsIn": "Réinitialisation dans",
"workspace.lite.subscription.showDetails": "Afficher les détails",
"workspace.lite.subscription.hideDetails": "Masquer les détails",
"workspace.lite.subscription.model": "Modèle",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Total",
"workspace.lite.subscription.useBalance":
"Utilisez votre solde disponible après avoir atteint les limites d'utilisation",
"workspace.lite.subscription.selectProvider":
+9 -2
View File
@@ -254,7 +254,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "seguenti eccezioni",
"go.title": "OpenCode Go | Modelli di coding a basso costo per tutti",
"go.banner.text": "Ox Alpha Free è disponibile su Go per un periodo limitato",
"go.meta.description":
"Go costa $10/mese, con limiti di utilizzo generosi e un accesso affidabile ai principali modelli di coding.",
"go.hero.title": "Modelli di coding a basso costo per tutti",
@@ -672,10 +671,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "pochi secondi",
"workspace.lite.subscription.message": "Sei abbonato a OpenCode Go.",
"workspace.lite.subscription.manage": "Gestisci Abbonamento",
"workspace.lite.subscription.rollingUsage": "Utilizzo Continuativo",
"workspace.lite.subscription.rollingUsage": "Utilizzo su 5 ore",
"workspace.lite.subscription.rollingQuota": "Quota su 5 ore",
"workspace.lite.subscription.weeklyUsage": "Utilizzo Settimanale",
"workspace.lite.subscription.weeklyQuota": "Quota Settimanale",
"workspace.lite.subscription.monthlyUsage": "Utilizzo Mensile",
"workspace.lite.subscription.monthlyQuota": "Quota Mensile",
"workspace.lite.subscription.resetsIn": "Si resetta tra",
"workspace.lite.subscription.showDetails": "Mostra dettagli",
"workspace.lite.subscription.hideDetails": "Nascondi dettagli",
"workspace.lite.subscription.model": "Modello",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Totale",
"workspace.lite.subscription.useBalance": "Usa il tuo saldo disponibile dopo aver raggiunto i limiti di utilizzo",
"workspace.lite.subscription.selectProvider":
'Seleziona "OpenCode Go" come provider nella tua configurazione opencode per utilizzare i modelli Go.',
+9 -2
View File
@@ -253,7 +253,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下の例外",
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
"go.banner.text": "Ox Alpha Freeは期間限定でGoで利用できます",
"go.meta.description":
"Goは月額$10で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
"go.hero.title": "すべての人のための低価格なコーディングモデル",
@@ -670,10 +669,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "数秒",
"workspace.lite.subscription.message": "あなたは OpenCode Go を購読しています。",
"workspace.lite.subscription.manage": "サブスクリプションの管理",
"workspace.lite.subscription.rollingUsage": "ローリング利用量",
"workspace.lite.subscription.rollingUsage": "5時間利用量",
"workspace.lite.subscription.rollingQuota": "5時間上限",
"workspace.lite.subscription.weeklyUsage": "週間利用量",
"workspace.lite.subscription.weeklyQuota": "週間上限",
"workspace.lite.subscription.monthlyUsage": "月間利用量",
"workspace.lite.subscription.monthlyQuota": "月間上限",
"workspace.lite.subscription.resetsIn": "リセットまで",
"workspace.lite.subscription.showDetails": "詳細を表示",
"workspace.lite.subscription.hideDetails": "詳細を非表示",
"workspace.lite.subscription.model": "モデル",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "合計",
"workspace.lite.subscription.useBalance": "利用限度額に達したら利用可能な残高を使用する",
"workspace.lite.subscription.selectProvider":
"Go モデルを使用するには、opencode の設定で「OpenCode Go」をプロバイダーとして選択してください。",
+9 -2
View File
@@ -250,7 +250,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "다음 예외",
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
"go.banner.text": "Ox Alpha Free가 한정된 기간 동안 Go에서 제공됩니다",
"go.meta.description": "Go는 월 $10이며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
"go.hero.title": "모두를 위한 저비용 코딩 모델",
"go.hero.body":
@@ -661,10 +660,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "몇 초",
"workspace.lite.subscription.message": "현재 OpenCode Go를 구독 중입니다.",
"workspace.lite.subscription.manage": "구독 관리",
"workspace.lite.subscription.rollingUsage": "롤링 사용량",
"workspace.lite.subscription.rollingUsage": "5시간 사용량",
"workspace.lite.subscription.rollingQuota": "5시간 할당량",
"workspace.lite.subscription.weeklyUsage": "주간 사용량",
"workspace.lite.subscription.weeklyQuota": "주간 할당량",
"workspace.lite.subscription.monthlyUsage": "월간 사용량",
"workspace.lite.subscription.monthlyQuota": "월간 할당량",
"workspace.lite.subscription.resetsIn": "초기화까지 남은 시간:",
"workspace.lite.subscription.showDetails": "상세 정보 보기",
"workspace.lite.subscription.hideDetails": "상세 정보 숨기기",
"workspace.lite.subscription.model": "모델",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "합계",
"workspace.lite.subscription.useBalance": "사용 한도 도달 후에는 보유 잔액 사용",
"workspace.lite.subscription.selectProvider":
'Go 모델을 사용하려면 opencode 설정에서 "OpenCode Go"를 공급자로 선택하세요.',
+9 -2
View File
@@ -254,7 +254,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "følgende unntak",
"go.title": "OpenCode Go | Rimelige kodemodeller for alle",
"go.banner.text": "Ox Alpha Free er tilgjengelig på Go i en begrenset periode",
"go.meta.description":
"Go koster $10/måned, med sjenerøse bruksgrenser og pålitelig tilgang til ledende kodemodeller.",
"go.hero.title": "Rimelige kodemodeller for alle",
@@ -670,10 +669,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "noen få sekunder",
"workspace.lite.subscription.message": "Du abonnerer på OpenCode Go.",
"workspace.lite.subscription.manage": "Administrer abonnement",
"workspace.lite.subscription.rollingUsage": "Løpende bruk",
"workspace.lite.subscription.rollingUsage": "5-timers bruk",
"workspace.lite.subscription.rollingQuota": "5-timers kvote",
"workspace.lite.subscription.weeklyUsage": "Ukentlig bruk",
"workspace.lite.subscription.weeklyQuota": "Ukentlig kvote",
"workspace.lite.subscription.monthlyUsage": "Månedlig bruk",
"workspace.lite.subscription.monthlyQuota": "Månedlig kvote",
"workspace.lite.subscription.resetsIn": "Nullstilles om",
"workspace.lite.subscription.showDetails": "Vis detaljer",
"workspace.lite.subscription.hideDetails": "Skjul detaljer",
"workspace.lite.subscription.model": "Modell",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Totalt",
"workspace.lite.subscription.useBalance": "Bruk din tilgjengelige saldo etter å ha nådd bruksgrensene",
"workspace.lite.subscription.selectProvider":
'Velg "OpenCode Go" som leverandør i opencode-konfigurasjonen din for å bruke Go-modeller.',
+9 -2
View File
@@ -255,7 +255,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "następującymi wyjątkami",
"go.title": "OpenCode Go | Niskokosztowe modele do kodowania dla każdego",
"go.banner.text": "Ox Alpha Free jest dostępny w Go przez ograniczony czas",
"go.meta.description":
"Go kosztuje $10/miesiąc, oferując hojne limity użycia i niezawodny dostęp do wiodących modeli do kodowania.",
"go.hero.title": "Niskokosztowe modele do kodowania dla każdego",
@@ -671,10 +670,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "kilka sekund",
"workspace.lite.subscription.message": "Subskrybujesz OpenCode Go.",
"workspace.lite.subscription.manage": "Zarządzaj subskrypcją",
"workspace.lite.subscription.rollingUsage": "Użycie kroczące",
"workspace.lite.subscription.rollingUsage": "Użycie w ciągu 5 godzin",
"workspace.lite.subscription.rollingQuota": "Limit 5-godzinny",
"workspace.lite.subscription.weeklyUsage": "Użycie tygodniowe",
"workspace.lite.subscription.weeklyQuota": "Limit tygodniowy",
"workspace.lite.subscription.monthlyUsage": "Użycie miesięczne",
"workspace.lite.subscription.monthlyQuota": "Limit miesięczny",
"workspace.lite.subscription.resetsIn": "Resetuje się za",
"workspace.lite.subscription.showDetails": "Pokaż szczegóły",
"workspace.lite.subscription.hideDetails": "Ukryj szczegóły",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Łącznie",
"workspace.lite.subscription.useBalance": "Użyj dostępnego salda po osiągnięciu limitów użycia",
"workspace.lite.subscription.selectProvider":
'Wybierz "OpenCode Go" jako dostawcę w konfiguracji opencode, aby używać modeli Go.',
+9 -2
View File
@@ -258,7 +258,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "следующими исключениями",
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
"go.banner.text": "Ox Alpha Free доступна в Go в течение ограниченного времени",
"go.meta.description":
"Go стоит $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
"go.hero.title": "Недорогие модели для кодинга для всех",
@@ -678,10 +677,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "несколько секунд",
"workspace.lite.subscription.message": "Вы подписаны на OpenCode Go.",
"workspace.lite.subscription.manage": "Управление подпиской",
"workspace.lite.subscription.rollingUsage": "Скользящее использование",
"workspace.lite.subscription.rollingUsage": "Использование за 5 часов",
"workspace.lite.subscription.rollingQuota": "Квота на 5 часов",
"workspace.lite.subscription.weeklyUsage": "Недельное использование",
"workspace.lite.subscription.weeklyQuota": "Недельная квота",
"workspace.lite.subscription.monthlyUsage": "Ежемесячное использование",
"workspace.lite.subscription.monthlyQuota": "Ежемесячная квота",
"workspace.lite.subscription.resetsIn": "Сброс через",
"workspace.lite.subscription.showDetails": "Показать подробности",
"workspace.lite.subscription.hideDetails": "Скрыть подробности",
"workspace.lite.subscription.model": "Модель",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Итого",
"workspace.lite.subscription.useBalance": "Использовать доступный баланс после достижения лимитов",
"workspace.lite.subscription.selectProvider":
'Выберите "OpenCode Go" в качестве провайдера в настройках opencode для использования моделей Go.',
+9 -2
View File
@@ -253,7 +253,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.banner.text": "Ox Alpha Free พร้อมใช้งานบน Go ในช่วงเวลาจำกัด",
"go.meta.description":
"Go มีราคา $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
@@ -667,10 +666,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "ไม่กี่วินาที",
"workspace.lite.subscription.message": "คุณได้สมัครสมาชิก OpenCode Go แล้ว",
"workspace.lite.subscription.manage": "จัดการการสมัครสมาชิก",
"workspace.lite.subscription.rollingUsage": "การใช้งานแบบหมุนเวียน",
"workspace.lite.subscription.rollingUsage": "การใช้งานใน 5 ชั่วโมง",
"workspace.lite.subscription.rollingQuota": "โควตา 5 ชั่วโมง",
"workspace.lite.subscription.weeklyUsage": "การใช้งานรายสัปดาห์",
"workspace.lite.subscription.weeklyQuota": "โควตารายสัปดาห์",
"workspace.lite.subscription.monthlyUsage": "การใช้งานรายเดือน",
"workspace.lite.subscription.monthlyQuota": "โควตารายเดือน",
"workspace.lite.subscription.resetsIn": "รีเซ็ตใน",
"workspace.lite.subscription.showDetails": "แสดงรายละเอียด",
"workspace.lite.subscription.hideDetails": "ซ่อนรายละเอียด",
"workspace.lite.subscription.model": "โมเดล",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "รวม",
"workspace.lite.subscription.useBalance": "ใช้ยอดคงเหลือของคุณหลังจากถึงขีดจำกัดการใช้งาน",
"workspace.lite.subscription.selectProvider":
'เลือก "OpenCode Go" เป็นผู้ให้บริการในการตั้งค่า opencode ของคุณเพื่อใช้โมเดล Go',
+9 -2
View File
@@ -256,7 +256,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "aşağıdaki istisnalar",
"go.title": "OpenCode Go | Herkes için düşük maliyetli kodlama modelleri",
"go.banner.text": "Ox Alpha Free sınırlı bir süre için Go'da kullanılabilir",
"go.meta.description":
"Go ayda 10$'dır; cömert kullanım limitleri ve önde gelen kodlama modellerine güvenilir erişim sunar.",
"go.hero.title": "Herkes için düşük maliyetli kodlama modelleri",
@@ -673,10 +672,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "birkaç saniye",
"workspace.lite.subscription.message": "OpenCode Go abonesisiniz.",
"workspace.lite.subscription.manage": "Aboneliği Yönet",
"workspace.lite.subscription.rollingUsage": "Devam Eden Kullanım",
"workspace.lite.subscription.rollingUsage": "5 Saatlik Kullanım",
"workspace.lite.subscription.rollingQuota": "5 Saatlik Kota",
"workspace.lite.subscription.weeklyUsage": "Haftalık Kullanım",
"workspace.lite.subscription.weeklyQuota": "Haftalık Kota",
"workspace.lite.subscription.monthlyUsage": "Aylık Kullanım",
"workspace.lite.subscription.monthlyQuota": "Aylık Kota",
"workspace.lite.subscription.resetsIn": "Sıfırlama süresi",
"workspace.lite.subscription.showDetails": "Ayrıntıları göster",
"workspace.lite.subscription.hideDetails": "Ayrıntıları gizle",
"workspace.lite.subscription.model": "Model",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Toplam",
"workspace.lite.subscription.useBalance": "Kullanım limitlerine ulaştıktan sonra mevcut bakiyenizi kullanın",
"workspace.lite.subscription.selectProvider":
'Go modellerini kullanmak için opencode yapılandırmanızda "OpenCode Go"\'yu sağlayıcı olarak seçin.',
+9 -2
View File
@@ -255,7 +255,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "такими винятками",
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
"go.banner.text": "Ox Alpha Free доступна в Go протягом обмеженого часу",
"go.meta.description":
"Go коштує $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
"go.hero.title": "Недорогі моделі кодування для всіх",
@@ -669,10 +668,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "кілька секунд",
"workspace.lite.subscription.message": "Ви підписані на OpenCode Go.",
"workspace.lite.subscription.manage": "Керувати підпискою",
"workspace.lite.subscription.rollingUsage": "Ковзне використання",
"workspace.lite.subscription.rollingUsage": "Використання за 5 годин",
"workspace.lite.subscription.rollingQuota": "Квота на 5 годин",
"workspace.lite.subscription.weeklyUsage": "Тижневе використання",
"workspace.lite.subscription.weeklyQuota": "Тижнева квота",
"workspace.lite.subscription.monthlyUsage": "Місячне використання",
"workspace.lite.subscription.monthlyQuota": "Місячна квота",
"workspace.lite.subscription.resetsIn": "Скидається через",
"workspace.lite.subscription.showDetails": "Показати подробиці",
"workspace.lite.subscription.hideDetails": "Приховати подробиці",
"workspace.lite.subscription.model": "Модель",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "Усього",
"workspace.lite.subscription.useBalance": "Використовуйте доступний баланс після досягнення лімітів",
"workspace.lite.subscription.selectProvider": 'Виберіть "OpenCode Go" як провайдера в конфігурації opencode.',
"workspace.lite.providers.title": "Провайдери",
+9 -2
View File
@@ -244,7 +244,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下例外情况除外",
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
"go.banner.text": "Ox Alpha Free 限时加入 Go",
"go.meta.description": "Go 每月 $10,提供充裕的使用限额,并可可靠访问领先的编程模型。",
"go.hero.title": "人人可用的低成本编程模型",
"go.hero.body":
@@ -642,10 +641,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "几秒钟",
"workspace.lite.subscription.message": "您已订阅 OpenCode Go。",
"workspace.lite.subscription.manage": "管理订阅",
"workspace.lite.subscription.rollingUsage": "滚动用量",
"workspace.lite.subscription.rollingUsage": "5 小时用量",
"workspace.lite.subscription.rollingQuota": "5 小时配额",
"workspace.lite.subscription.weeklyUsage": "每周用量",
"workspace.lite.subscription.weeklyQuota": "每周配额",
"workspace.lite.subscription.monthlyUsage": "每月用量",
"workspace.lite.subscription.monthlyQuota": "每月配额",
"workspace.lite.subscription.resetsIn": "重置于",
"workspace.lite.subscription.showDetails": "显示详情",
"workspace.lite.subscription.hideDetails": "隐藏详情",
"workspace.lite.subscription.model": "模型",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "总计",
"workspace.lite.subscription.useBalance": "达到使用限额后使用您的可用余额",
"workspace.lite.subscription.selectProvider":
"在你的 opencode 配置中选择「OpenCode Go」作为提供商,即可使用 Go 模型。",
+9 -2
View File
@@ -244,7 +244,6 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下例外情況",
"go.title": "OpenCode Go | 低成本全民編碼模型",
"go.banner.text": "Ox Alpha Free 限時加入 Go",
"go.meta.description": "Go 每月 $10,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
"go.hero.title": "低成本全民編碼模型",
"go.hero.body":
@@ -642,10 +641,18 @@ export const dict = {
"workspace.lite.time.fewSeconds": "幾秒",
"workspace.lite.subscription.message": "您已訂閱 OpenCode Go。",
"workspace.lite.subscription.manage": "管理訂閱",
"workspace.lite.subscription.rollingUsage": "滾動使用量",
"workspace.lite.subscription.rollingUsage": "5 小時使用量",
"workspace.lite.subscription.rollingQuota": "5 小時配額",
"workspace.lite.subscription.weeklyUsage": "每週使用量",
"workspace.lite.subscription.weeklyQuota": "每週配額",
"workspace.lite.subscription.monthlyUsage": "每月使用量",
"workspace.lite.subscription.monthlyQuota": "每月配額",
"workspace.lite.subscription.resetsIn": "重置時間:",
"workspace.lite.subscription.showDetails": "顯示詳情",
"workspace.lite.subscription.hideDetails": "隱藏詳情",
"workspace.lite.subscription.model": "模型",
"workspace.lite.subscription.contribution": "%",
"workspace.lite.subscription.total": "總計",
"workspace.lite.subscription.useBalance": "達到使用限制後使用您的可用餘額",
"workspace.lite.subscription.selectProvider":
"在您的 opencode 設定中選擇「OpenCode Go」作為提供商,即可使用 Go 模型。",
@@ -0,0 +1,60 @@
export type LiteUsageBreakdownSource = {
model: string
name: string
cost: number
quotaCost: number
multiplier?: number
estimated: boolean
}
export type LiteUsageBreakdownItem = {
model: string
name: string
cost?: number
multiplier?: number
quotaCost: number
contributionPercent: number
estimated: boolean
}
export function buildLiteUsageBreakdown(input: { usage: number; limit: number; sources: LiteUsageBreakdownSource[] }) {
const rows: LiteUsageBreakdownItem[] = input.sources
.filter((item) => item.cost !== 0 || item.quotaCost !== 0)
.sort((a, b) => b.quotaCost - a.quotaCost)
.map((item) => ({
...item,
contributionPercent: 0,
}))
const usagePercent = getUsagePercent(input.usage, input.limit)
const target = Math.max(0, Math.round(usagePercent * 10))
const totalQuota = rows.reduce((total, item) => total + Math.max(0, item.quotaCost), 0)
const units = rows.map((item) => {
const exact = totalQuota === 0 ? 0 : (Math.max(0, item.quotaCost) / totalQuota) * target
const value = Math.floor(exact)
return { item, exact, value }
})
const remaining = target - units.reduce((total, item) => total + item.value, 0)
const ranked = units.toSorted((a, b) => b.exact - b.value - (a.exact - a.value))
Array.from({ length: ranked.length === 0 ? 0 : remaining }).forEach((_, index) => {
ranked[index % ranked.length].value += 1
})
units.forEach((unit) => (unit.item.contributionPercent = unit.value / 10))
return {
usage: input.usage,
limit: input.limit,
usagePercent,
rows,
}
}
export function getModelQuotaLimit(limit: number, multiplier?: number) {
if (multiplier === undefined || multiplier <= 0) return
return limit / multiplier
}
export function getUsagePercent(amount: number, limit: number) {
if (limit === 0) return 0
return Math.round((amount / limit) * 1000) / 10
}
@@ -103,9 +103,6 @@ export default function Black() {
<button type="button" onClick={() => cancel()} data-slot="cancel">
{i18n.t("common.cancel")}
</button>
<a href={`/black/subscribe/${plan().id}`} data-slot="continue">
{i18n.t("black.action.continue")}
</a>
</div>
</div>
</div>
@@ -1,489 +0,0 @@
import { A, createAsync, query, redirect, useParams } from "@solidjs/router"
import { Title } from "@solidjs/meta"
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"
import { type Stripe, type PaymentMethod, loadStripe } from "@stripe/stripe-js"
import { Elements, PaymentElement, useStripe, useElements, AddressElement } from "solid-stripe"
import { PlanID, plans } from "../common"
import { getActor, useAuthSession } from "~/context/auth"
import { withActor } from "~/context/auth.withActor"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { createList } from "solid-list"
import { Modal } from "~/component/modal"
import { BillingTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
import { formError } from "~/lib/form-error"
import { Resource } from "@opencode-ai/console-resource"
const getEnabled = query(async () => {
"use server"
return Resource.App.stage !== "production"
}, "black.subscribe.enabled")
const plansMap = Object.fromEntries(plans.map((p) => [p.id, p])) as Record<PlanID, (typeof plans)[number]>
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY!)
const getWorkspaces = query(async (plan: string) => {
"use server"
const actor = await getActor()
if (actor.type === "public") throw redirect("/auth/authorize?continue=/black/subscribe/" + plan)
return withActor(async () => {
return Database.use((tx) =>
tx
.select({
id: WorkspaceTable.id,
name: WorkspaceTable.name,
slug: WorkspaceTable.slug,
billing: {
customerID: BillingTable.customerID,
paymentMethodID: BillingTable.paymentMethodID,
paymentMethodType: BillingTable.paymentMethodType,
paymentMethodLast4: BillingTable.paymentMethodLast4,
subscriptionID: BillingTable.subscriptionID,
timeSubscriptionBooked: BillingTable.timeSubscriptionBooked,
},
})
.from(UserTable)
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
.innerJoin(BillingTable, eq(WorkspaceTable.id, BillingTable.workspaceID))
.where(
and(
eq(UserTable.accountID, Actor.account()),
isNull(WorkspaceTable.timeDeleted),
isNull(UserTable.timeDeleted),
),
),
)
})
}, "black.subscribe.workspaces")
const createSetupIntent = async (input: { plan: string; workspaceID: string }) => {
"use server"
const { plan, workspaceID } = input
if (!plan || !["20", "100", "200"].includes(plan)) return { error: formError.invalidPlan }
if (!workspaceID) return { error: formError.workspaceRequired }
return withActor(async () => {
const session = await useAuthSession()
const account = session.data.account?.[session.data.current ?? ""]
const email = account?.email
const customer = await Database.use((tx) =>
tx
.select({
customerID: BillingTable.customerID,
subscriptionID: BillingTable.subscriptionID,
})
.from(BillingTable)
.where(eq(BillingTable.workspaceID, workspaceID))
.then((rows) => rows[0]),
)
if (customer?.subscriptionID) {
return { error: formError.alreadySubscribed }
}
let customerID = customer?.customerID
if (!customerID) {
const customer = await Billing.stripe().customers.create({
email,
metadata: {
workspaceID,
},
})
customerID = customer.id
await Database.use((tx) =>
tx
.update(BillingTable)
.set({
customerID,
})
.where(eq(BillingTable.workspaceID, workspaceID)),
)
}
const intent = await Billing.stripe().setupIntents.create({
customer: customerID,
payment_method_types: ["card"],
metadata: {
workspaceID,
},
})
return { clientSecret: intent.client_secret ?? undefined }
}, workspaceID)
}
const bookSubscription = async (input: {
workspaceID: string
plan: PlanID
paymentMethodID: string
paymentMethodType: string
paymentMethodLast4?: string
}) => {
"use server"
return withActor(
() =>
Database.use((tx) =>
tx
.update(BillingTable)
.set({
paymentMethodID: input.paymentMethodID,
paymentMethodType: input.paymentMethodType,
paymentMethodLast4: input.paymentMethodLast4,
subscriptionPlan: input.plan,
timeSubscriptionBooked: new Date(),
})
.where(eq(BillingTable.workspaceID, input.workspaceID)),
),
input.workspaceID,
)
}
interface SuccessData {
plan: string
paymentMethodType: string
paymentMethodLast4?: string
}
function Failure(props: { message: string }) {
const i18n = useI18n()
return (
<div data-slot="failure">
<p data-slot="message">
{i18n.t("black.subscribe.failurePrefix")} {props.message}
</p>
</div>
)
}
function Success(props: SuccessData) {
const i18n = useI18n()
return (
<div data-slot="success">
<p data-slot="title">{i18n.t("black.subscribe.success.title")}</p>
<dl data-slot="details">
<div>
<dt>{i18n.t("black.subscribe.success.subscriptionPlan")}</dt>
<dd>{i18n.t("black.subscribe.success.planName", { plan: props.plan })}</dd>
</div>
<div>
<dt>{i18n.t("black.subscribe.success.amount")}</dt>
<dd>{i18n.t("black.subscribe.success.amountValue", { plan: props.plan })}</dd>
</div>
<div>
<dt>{i18n.t("black.subscribe.success.paymentMethod")}</dt>
<dd>
<Show when={props.paymentMethodLast4} fallback={<span>{props.paymentMethodType}</span>}>
<span>
{props.paymentMethodType} - {props.paymentMethodLast4}
</span>
</Show>
</dd>
</div>
<div>
<dt>{i18n.t("black.subscribe.success.dateJoined")}</dt>
<dd>{new Date().toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}</dd>
</div>
</dl>
<p data-slot="charge-notice">{i18n.t("black.subscribe.success.chargeNotice")}</p>
</div>
)
}
function IntentForm(props: { plan: PlanID; workspaceID: string; onSuccess: (data: SuccessData) => void }) {
const i18n = useI18n()
const stripe = useStripe()
const elements = useElements()
const [error, setError] = createSignal<string | undefined>(undefined)
const [loading, setLoading] = createSignal(false)
const handleSubmit = async (e: Event) => {
e.preventDefault()
if (!stripe() || !elements()) return
setLoading(true)
setError(undefined)
const result = await elements()!.submit()
if (result.error) {
setError(result.error.message ?? i18n.t("black.subscribe.error.generic"))
setLoading(false)
return
}
const { error: confirmError, setupIntent } = await stripe()!.confirmSetup({
elements: elements()!,
confirmParams: {
expand: ["payment_method"],
payment_method_data: {
allow_redisplay: "always",
},
},
redirect: "if_required",
})
if (confirmError) {
setError(confirmError.message ?? i18n.t("black.subscribe.error.generic"))
setLoading(false)
return
}
if (setupIntent?.status === "succeeded") {
const pm = setupIntent.payment_method as PaymentMethod
await bookSubscription({
workspaceID: props.workspaceID,
plan: props.plan,
paymentMethodID: pm.id,
paymentMethodType: pm.type,
paymentMethodLast4: pm.card?.last4,
})
props.onSuccess({
plan: props.plan,
paymentMethodType: pm.type,
paymentMethodLast4: pm.card?.last4,
})
}
setLoading(false)
}
return (
<form onSubmit={handleSubmit} data-slot="checkout-form">
<PaymentElement />
<AddressElement options={{ mode: "billing" }} />
<Show when={error()}>
<p data-slot="error">{error()}</p>
</Show>
<button type="submit" disabled={loading() || !stripe() || !elements()} data-slot="submit-button">
{loading() ? i18n.t("black.subscribe.processing") : i18n.t("black.subscribe.submit", { plan: props.plan })}
</button>
<p data-slot="charge-notice">{i18n.t("black.subscribe.form.chargeNotice")}</p>
</form>
)
}
export default function BlackSubscribe() {
const params = useParams()
const i18n = useI18n()
const language = useLanguage()
const enabled = createAsync(() => getEnabled())
const planData = plansMap[(params.plan as PlanID) ?? "20"] ?? plansMap["20"]
const plan = planData.id
const workspaces = createAsync(() => getWorkspaces(plan))
const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | undefined>(undefined)
const [success, setSuccess] = createSignal<SuccessData | undefined>(undefined)
const [failure, setFailure] = createSignal<string | undefined>(undefined)
const [clientSecret, setClientSecret] = createSignal<string | undefined>(undefined)
const [stripe, setStripe] = createSignal<Stripe | undefined>(undefined)
const formatError = (error: string) => {
if (error === formError.invalidPlan) return i18n.t("black.subscribe.error.invalidPlan")
if (error === formError.workspaceRequired) return i18n.t("black.subscribe.error.workspaceRequired")
if (error === formError.alreadySubscribed) return i18n.t("black.subscribe.error.alreadySubscribed")
if (error === "Invalid plan") return i18n.t("black.subscribe.error.invalidPlan")
if (error === "Workspace ID is required") return i18n.t("black.subscribe.error.workspaceRequired")
if (error === "This workspace already has a subscription") return i18n.t("black.subscribe.error.alreadySubscribed")
return error
}
// Resolve stripe promise once
createEffect(() => {
void stripePromise.then((s) => {
if (s) setStripe(s)
})
})
// Auto-select if only one workspace
createEffect(() => {
const ws = workspaces()
if (ws?.length === 1 && !selectedWorkspace()) {
setSelectedWorkspace(ws[0].id)
}
})
// Fetch setup intent when workspace is selected (unless workspace already has payment method)
createEffect(async () => {
const id = selectedWorkspace()
if (!id) return
const ws = workspaces()?.find((w) => w.id === id)
if (ws?.billing?.subscriptionID) {
setFailure(i18n.t("black.subscribe.error.alreadySubscribed"))
return
}
if (ws?.billing?.paymentMethodID) {
if (!ws?.billing?.timeSubscriptionBooked) {
await bookSubscription({
workspaceID: id,
plan: planData.id,
paymentMethodID: ws.billing.paymentMethodID!,
paymentMethodType: ws.billing.paymentMethodType!,
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
})
}
setSuccess({
plan: planData.id,
paymentMethodType: ws.billing.paymentMethodType!,
paymentMethodLast4: ws.billing.paymentMethodLast4 ?? undefined,
})
return
}
const result = await createSetupIntent({ plan, workspaceID: id })
if (result.error) {
setFailure(formatError(result.error))
} else if ("clientSecret" in result) {
setClientSecret(result.clientSecret)
}
})
// Keyboard navigation for workspace picker
const { active, setActive, onKeyDown } = createList({
items: () => workspaces()?.map((w) => w.id) ?? [],
initialActive: null,
})
const handleSelectWorkspace = (id: string) => {
setSelectedWorkspace(id)
}
let listRef: HTMLUListElement | undefined
// Show workspace picker if multiple workspaces and none selected
const showWorkspacePicker = () => {
const ws = workspaces()
return ws && ws.length > 1 && !selectedWorkspace()
}
return (
<Show when={enabled()}>
<Title>{i18n.t("black.subscribe.title")}</Title>
<section data-slot="subscribe-form">
<div data-slot="form-card">
<Switch>
<Match when={success()}>{(data) => <Success {...data()} />}</Match>
<Match when={failure()}>{(data) => <Failure message={data()} />}</Match>
<Match when={true}>
<>
<div data-slot="plan-header">
<p data-slot="title">{i18n.t("black.subscribe.title")}</p>
<p data-slot="price">
<span data-slot="amount">${planData.id}</span>{" "}
<span data-slot="period">{i18n.t("black.price.perMonth")}</span>
<Show when={planData.multiplier}>
{(multiplier) => <span data-slot="multiplier">{i18n.t(multiplier())}</span>}
</Show>
</p>
</div>
<div data-slot="divider" />
<p data-slot="section-title">{i18n.t("black.subscribe.paymentMethod")}</p>
<Show
when={clientSecret() && selectedWorkspace() && stripe()}
fallback={
<div data-slot="loading">
<p>
{selectedWorkspace()
? i18n.t("black.subscribe.loadingPaymentForm")
: i18n.t("black.subscribe.selectWorkspaceToContinue")}
</p>
</div>
}
>
<Elements
stripe={stripe()!}
options={{
clientSecret: clientSecret()!,
appearance: {
theme: "night",
variables: {
colorPrimary: "#ffffff",
colorBackground: "#1a1a1a",
colorText: "#ffffff",
colorTextSecondary: "#999999",
colorDanger: "#ff6b6b",
fontFamily: "JetBrains Mono, monospace",
borderRadius: "4px",
spacingUnit: "4px",
},
rules: {
".Input": {
backgroundColor: "#1a1a1a",
border: "1px solid rgba(255, 255, 255, 0.17)",
color: "#ffffff",
},
".Input:focus": {
borderColor: "rgba(255, 255, 255, 0.35)",
boxShadow: "none",
},
".Label": {
color: "rgba(255, 255, 255, 0.59)",
fontSize: "14px",
marginBottom: "8px",
},
},
},
}}
>
<IntentForm plan={plan} workspaceID={selectedWorkspace()!} onSuccess={setSuccess} />
</Elements>
</Show>
</>
</Match>
</Switch>
</div>
{/* Workspace picker modal */}
<Modal
open={showWorkspacePicker() ?? false}
onClose={() => {}}
title={i18n.t("black.workspace.selectPlan")}
variant="black"
>
<div data-component="black-workspace-picker-modal" data-slot="workspace-picker">
<ul
ref={listRef}
data-slot="workspace-list"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" && active()) {
handleSelectWorkspace(active()!)
} else {
onKeyDown(e)
}
}}
>
<For each={workspaces()}>
{(workspace) => (
<li
data-slot="workspace-item"
data-active={active() === workspace.id}
onMouseEnter={() => setActive(workspace.id)}
onClick={() => handleSelectWorkspace(workspace.id)}
>
<span data-slot="selected-icon">[*]</span>
<span>{workspace.name || workspace.slug}</span>
</li>
)}
</For>
</ul>
</div>
</Modal>
<p data-slot="fine-print">
{i18n.t("black.finePrint.beforeTerms")} ·{" "}
<A href={language.route("/legal/terms-of-service")}>{i18n.t("black.finePrint.terms")}</A>
</p>
</section>
</Show>
)
}
@@ -327,37 +327,6 @@ body {
}
}
[data-component="desktop-app-banner"] {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 32px;
[data-slot="badge"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
font-weight: 500;
padding: 4px 8px;
line-height: 1;
flex-shrink: 0;
}
[data-slot="content"] {
display: flex;
align-items: center;
gap: 1ch;
}
[data-slot="text"] {
color: var(--color-text-strong);
line-height: 1.4;
@media (max-width: 30.625rem) {
display: none;
}
}
}
[data-slot="hero-copy"] {
img {
margin-bottom: 24px;
+3 -10
View File
@@ -23,7 +23,7 @@ const checkLoggedIn = query(async () => {
}, "checkLoggedIn.get")
const models = [
{ name: "Grok 4.5", 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 5.6 Luna", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention30" },
{ 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" },
@@ -70,8 +70,8 @@ function LimitsGraph(props: { href: string }) {
const baseline = 100
const graph = [
{ id: "kimi-k3", name: "Kimi K3", req: 110, d: "50ms" },
{ id: "grok-4.5", name: "Grok 4.5", req: 120, d: "75ms" },
{ id: "qwen3.8-max", name: "Qwen3.8 Max", req: 160, d: "90ms" },
{ id: "grok-4.6", name: "Grok 4.6", req: 169, d: "75ms" },
{ id: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 2050, d: "290ms" },
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
@@ -81,7 +81,6 @@ function LimitsGraph(props: { href: string }) {
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
{ id: "hy3", name: "Hy3", req: 34400, baseReq: 4300, d: "320ms" },
{ id: "muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", req: 45300, edge: true, d: "360ms" },
{ id: "ox-alpha-free", name: "Ox Alpha Free", req: Infinity, infinite: true, edge: true, d: "400ms" },
]
const w = 1040
@@ -270,12 +269,6 @@ export default function Home() {
<div data-component="content">
<section data-component="hero">
<div data-component="desktop-app-banner">
<span data-slot="badge">{i18n.t("home.banner.badge")}</span>
<div data-slot="content">
<span data-slot="text">{i18n.t("go.banner.text")}</span>
</div>
</div>
<div data-slot="hero-copy">
<img data-slot="zen logo light" src={goLogoLight} alt="" />
<img data-slot="zen logo dark" src={goLogoDark} alt="" />
@@ -511,7 +504,7 @@ export default function Home() {
</div>
<div data-slot="faq-retention-notes">
<p>
<strong>Grok 4.5:</strong> {i18n.t("go.faq.a5.grokRetention")}{" "}
<strong>Grok 4.6:</strong> {i18n.t("go.faq.a5.grokRetention")}{" "}
<a href="https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr">
{i18n.t("go.faq.a5.learnMore")}
</a>
@@ -205,6 +205,13 @@ export async function POST(input: APIEvent) {
} else if (productID === BlackData.productID()) {
await Billing.unsubscribeBlack({ subscriptionID })
}
const latestInvoice = body.data.object.latest_invoice
const invoiceID = typeof latestInvoice === "string" ? latestInvoice : latestInvoice?.id
if (invoiceID) {
const invoice = await Billing.stripe().invoices.retrieve(invoiceID)
if (invoice.status === "open") await Billing.stripe().invoices.voidInvoice(invoiceID)
}
}
if (body.type === "invoice.payment_succeeded") {
if (
@@ -19,6 +19,7 @@
[data-slot="usage-item"] {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: var(--space-2);
@@ -60,6 +61,119 @@
color: var(--color-text-muted);
}
[data-slot="usage-details"] {
margin-top: var(--space-1);
}
[data-slot="usage-details-trigger"] {
display: inline-flex;
align-items: center;
gap: var(--space-2);
width: fit-content;
padding: 0;
border: 0;
background-color: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
cursor: pointer;
&:hover:not(:disabled) {
background-color: transparent;
border-color: transparent;
}
svg {
transition: transform 0.2s ease;
}
&[aria-expanded="true"] svg {
transform: rotate(180deg);
}
[data-slot="hide-details"] {
display: none;
}
&[aria-expanded="true"] [data-slot="show-details"] {
display: none;
}
&[aria-expanded="true"] [data-slot="hide-details"] {
display: inline;
}
}
[data-slot="usage-details-content"] {
width: 100%;
margin-top: var(--space-3);
}
[data-slot="usage-details-loading"] {
width: 100%;
margin-top: var(--space-3);
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
[data-slot="usage-details-empty"] {
margin: 0;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
}
[data-slot="usage-details-table"] {
overflow-x: auto;
border: 1px solid var(--color-border-muted);
border-radius: var(--border-radius-sm);
table {
width: 100%;
min-width: 28rem;
table-layout: fixed;
border-collapse: collapse;
font-size: var(--font-size-sm);
white-space: nowrap;
}
th,
td {
width: 25%;
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--color-border-muted);
text-align: end;
font-variant-numeric: tabular-nums;
}
th {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
}
th:first-child,
td:first-child {
text-align: start;
}
td:first-child {
max-width: 13rem;
overflow: hidden;
color: var(--color-text);
text-overflow: ellipsis;
}
tbody tr:last-child td {
border-bottom: 0;
}
[data-slot="usage-total"] td {
border-top: 1px solid var(--color-border);
color: var(--color-text);
font-weight: 600;
}
}
[data-slot="setting-row"] {
display: flex;
align-items: center;
@@ -1,15 +1,19 @@
import { action, useParams, useAction, useSubmission, json, query, createAsync } from "@solidjs/router"
import { createStore } from "solid-js/store"
import { createMemo, For, Show } from "solid-js"
import { createMemo, createSignal, For, Show } from "solid-js"
import { Modal } from "~/component/modal"
import { Billing } from "@opencode-ai/console-core/billing.js"
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, LiteTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { Database, eq, and, gte, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, LiteTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { Actor } from "@opencode-ai/console-core/actor.js"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { Subscription } from "@opencode-ai/console-core/subscription.js"
import { LiteData } from "@opencode-ai/console-core/lite.js"
import { ZenData } from "@opencode-ai/console-core/model.js"
import { getMonthlyBounds, getWeekBounds } from "@opencode-ai/console-core/util/date.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { withActor } from "~/context/auth.withActor"
import { queryBillingInfo } from "../../common"
import styles from "./lite-section.module.css"
@@ -20,8 +24,12 @@ import { formatResetTime, liteResetTimeKeys } from "~/lib/format-reset-time"
import { createReferralFromCookie } from "~/lib/referral-invite"
import { getRequestEvent } from "solid-js/web"
import { countryFromRequest } from "~/lib/request-country"
import { checkCheckoutRateLimit } from "~/routes/zen/util/redis"
import { IconAlipay, IconUpi } from "~/component/icon"
import { IconAlipay, IconChevron, IconUpi } from "~/component/icon"
import { buildLiteUsageBreakdown, getModelQuotaLimit, getUsagePercent } from "~/lib/lite-usage"
type LiteUsageWindow = "rolling" | "weekly" | "monthly"
export const queryLiteSubscription = query(async (workspaceID: string) => {
"use server"
@@ -51,6 +59,33 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
const limits = LiteData.getLimits()
const mine = row.userID === Actor.userID()
const now = new Date()
const rollingCutoff = new Date(now.getTime() - limits.rollingWindow * 3600 * 1000)
const week = getWeekBounds(now)
const month = getMonthlyBounds(now, row.timeCreated)
const rollingActive = !!row.timeRollingUpdated && row.timeRollingUpdated >= rollingCutoff
const weeklyActive = !!row.timeWeeklyUpdated && row.timeWeeklyUpdated >= week.start
const monthlyActive = !!row.timeMonthlyUpdated && row.timeMonthlyUpdated >= month.start
const rollingLimit = centsToMicroCents(limits.rollingLimit * 100)
const weeklyLimit = centsToMicroCents(limits.weeklyLimit * 100)
const monthlyLimit = centsToMicroCents(limits.monthlyLimit * 100)
const rollingUsage = Subscription.analyzeRollingUsage({
limit: limits.rollingLimit,
window: limits.rollingWindow,
usage: row.rollingUsage ?? 0,
timeUpdated: row.timeRollingUpdated ?? now,
})
const weeklyUsage = Subscription.analyzeWeeklyUsage({
limit: limits.weeklyLimit,
usage: row.weeklyUsage ?? 0,
timeUpdated: row.timeWeeklyUpdated ?? now,
})
const monthlyUsage = Subscription.analyzeMonthlyUsage({
limit: limits.monthlyLimit,
usage: row.monthlyUsage ?? 0,
timeUpdated: row.timeMonthlyUpdated ?? now,
timeSubscribed: row.timeCreated,
})
return {
mine,
@@ -58,27 +93,126 @@ export const queryLiteSubscription = query(async (workspaceID: string) => {
allowTraining: row.allowTraining ?? false,
region:
row.region ?? (await Workspace.setDefaultRegion({ country: countryFromRequest(getRequestEvent()?.request) })),
rollingUsage: Subscription.analyzeRollingUsage({
limit: limits.rollingLimit,
window: limits.rollingWindow,
usage: row.rollingUsage ?? 0,
timeUpdated: row.timeRollingUpdated ?? new Date(),
}),
weeklyUsage: Subscription.analyzeWeeklyUsage({
limit: limits.weeklyLimit,
usage: row.weeklyUsage ?? 0,
timeUpdated: row.timeWeeklyUpdated ?? new Date(),
}),
monthlyUsage: Subscription.analyzeMonthlyUsage({
limit: limits.monthlyLimit,
usage: row.monthlyUsage ?? 0,
timeUpdated: row.timeMonthlyUpdated ?? new Date(),
timeSubscribed: row.timeCreated,
}),
rollingUsage: {
...rollingUsage,
usage: rollingActive ? (row.rollingUsage ?? 0) : 0,
limit: rollingLimit,
usagePercent: getUsagePercent(rollingActive ? (row.rollingUsage ?? 0) : 0, rollingLimit),
},
weeklyUsage: {
...weeklyUsage,
usage: weeklyActive ? (row.weeklyUsage ?? 0) : 0,
limit: weeklyLimit,
usagePercent: getUsagePercent(weeklyActive ? (row.weeklyUsage ?? 0) : 0, weeklyLimit),
},
monthlyUsage: {
...monthlyUsage,
usage: monthlyActive ? (row.monthlyUsage ?? 0) : 0,
limit: monthlyLimit,
usagePercent: getUsagePercent(monthlyActive ? (row.monthlyUsage ?? 0) : 0, monthlyLimit),
},
}
}, workspaceID)
}, "lite.subscription.get")
export const queryLiteUsageDetails = query(async (workspaceID: string, window: LiteUsageWindow) => {
"use server"
return withActor(async () => {
if (window !== "rolling" && window !== "weekly" && window !== "monthly") return null
const row = await Database.use((tx) =>
tx
.select({
userID: LiteTable.userID,
rollingUsage: LiteTable.rollingUsage,
weeklyUsage: LiteTable.weeklyUsage,
monthlyUsage: LiteTable.monthlyUsage,
timeRollingUpdated: LiteTable.timeRollingUpdated,
timeWeeklyUpdated: LiteTable.timeWeeklyUpdated,
timeMonthlyUpdated: LiteTable.timeMonthlyUpdated,
timeCreated: LiteTable.timeCreated,
})
.from(LiteTable)
.where(and(eq(LiteTable.workspaceID, Actor.workspace()), isNull(LiteTable.timeDeleted)))
.then((result) => result[0]),
)
if (!row || row.userID !== Actor.userID()) return null
const limits = LiteData.getLimits()
const now = new Date()
const detail = (() => {
if (window === "rolling") {
const active =
!!row.timeRollingUpdated &&
row.timeRollingUpdated >= new Date(now.getTime() - limits.rollingWindow * 3600 * 1000)
return {
start: active ? row.timeRollingUpdated! : now,
usage: active ? (row.rollingUsage ?? 0) : 0,
limit: centsToMicroCents(limits.rollingLimit * 100),
}
}
if (window === "weekly") {
const start = getWeekBounds(now).start
return {
start,
usage: row.timeWeeklyUpdated && row.timeWeeklyUpdated >= start ? (row.weeklyUsage ?? 0) : 0,
limit: centsToMicroCents(limits.weeklyLimit * 100),
}
}
const start = getMonthlyBounds(now, row.timeCreated).start
return {
start,
usage: row.timeMonthlyUpdated && row.timeMonthlyUpdated >= start ? (row.monthlyUsage ?? 0) : 0,
limit: centsToMicroCents(limits.monthlyLimit * 100),
}
})()
const modelData = Object.fromEntries(
Object.entries(ZenData.list("lite").models).map(([id, value]) => {
const models = Array.isArray(value) ? value : [value]
const multipliers = new Set(models.map((model) => model.costMultiplier))
return [id, { name: models[0].name, multiplier: multipliers.size === 1 ? models[0].costMultiplier : undefined }]
}),
)
const usageRows = await Database.use((tx) =>
tx
.select({
model: UsageTable.model,
multiplier: sql<string | null>`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`,
cost: sql<string>`SUM(${UsageTable.cost})`,
quotaCost: sql<string>`SUM(CASE WHEN JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier') IS NOT NULL THEN ROUND(${UsageTable.cost} * CAST(JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier')) AS DECIMAL(20, 8))) ELSE 0 END)`,
})
.from(UsageTable)
.innerJoin(KeyTable, and(eq(KeyTable.id, UsageTable.keyID), eq(KeyTable.workspaceID, UsageTable.workspaceID)))
.where(
and(
eq(UsageTable.workspaceID, Actor.workspace()),
eq(KeyTable.userID, row.userID),
gte(UsageTable.timeCreated, detail.start),
sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.plan')) = 'lite'`,
),
)
.groupBy(UsageTable.model, sql`JSON_UNQUOTE(JSON_EXTRACT(${UsageTable.enrichment}, '$.costMultiplier'))`),
)
return buildLiteUsageBreakdown({
usage: detail.usage,
limit: detail.limit,
sources: usageRows.map((usage) => {
const cost = Number(usage.cost)
const info = modelData[usage.model]
const multiplier = usage.multiplier === null ? info?.multiplier : Number(usage.multiplier)
return {
model: usage.model,
name: info?.name ?? usage.model,
cost,
quotaCost: usage.multiplier === null ? Math.round(cost * (multiplier ?? 1)) : Number(usage.quotaCost),
multiplier,
estimated: usage.multiplier === null,
}
}),
})
}, workspaceID)
}, "lite.subscription.usage")
type LiteSubscription = Awaited<ReturnType<typeof queryLiteSubscription>>
const createLiteCheckoutUrl = action(
@@ -86,6 +220,7 @@ const createLiteCheckoutUrl = action(
"use server"
return json(
await withActor(async () => {
await checkCheckoutRateLimit(Actor.account())
const data = await Billing.generateLiteCheckoutUrl({ successUrl, cancelUrl, method })
await createReferralFromCookie()
return { error: undefined, data }
@@ -174,7 +309,16 @@ const setGoAllowTraining = action(async (form: FormData) => {
)
}, "go.allowTraining.set")
function LiteUsageItem(props: { label: string; usage: { usagePercent: number; resetInSec: number } }) {
type LiteUsage = NonNullable<LiteSubscription>["rollingUsage"]
type LiteUsageDetailsData = NonNullable<Awaited<ReturnType<typeof queryLiteUsageDetails>>>
function LiteUsageItem(props: {
id: LiteUsageWindow
label: string
usage: LiteUsage
open: boolean
onToggle: () => void
}) {
const i18n = useI18n()
return (
@@ -183,17 +327,178 @@ function LiteUsageItem(props: { label: string; usage: { usagePercent: number; re
<span data-slot="usage-label">{props.label}</span>
<span data-slot="usage-value">{props.usage.usagePercent}%</span>
</div>
<div data-slot="progress">
<div data-slot="progress-bar" style={{ width: `${props.usage.usagePercent}%` }} />
<div
data-slot="progress"
role="progressbar"
aria-label={props.label}
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={Math.min(100, props.usage.usagePercent)}
>
<div data-slot="progress-bar" style={{ width: `${Math.min(100, props.usage.usagePercent)}%` }} />
</div>
<span data-slot="reset-time">
{i18n.t("workspace.lite.subscription.resetsIn")}{" "}
{formatResetTime(props.usage.resetInSec, i18n, liteResetTimeKeys)}
</span>
<Show when={props.usage.usage > 0}>
<div data-slot="usage-details">
<button
type="button"
data-slot="usage-details-trigger"
aria-expanded={props.open}
aria-controls={`usage-details-${props.id}`}
onClick={props.onToggle}
>
<span data-slot="show-details">{i18n.t("workspace.lite.subscription.showDetails")}</span>
<span data-slot="hide-details">{i18n.t("workspace.lite.subscription.hideDetails")}</span>
<IconChevron />
</button>
</div>
</Show>
</div>
)
}
function LiteUsageDetails(props: {
id: LiteUsageWindow
label: string
quotaLabel: string
usage: LiteUsageDetailsData
}) {
const i18n = useI18n()
const language = useLanguage()
const money = (amount: number) =>
new Intl.NumberFormat(language.tag(language.locale()), {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 4,
}).format(amount / 100_000_000)
const totalPercentage = () =>
Number(props.usage.rows.reduce((total, row) => total + row.contributionPercent, 0).toFixed(1))
return (
<div id={`usage-details-${props.id}`} data-slot="usage-details-content" role="region" aria-label={props.label}>
<div data-slot="usage-details-table">
<table>
<thead>
<tr>
<th>{i18n.t("workspace.lite.subscription.model")}</th>
<th>{props.label}</th>
<th>{props.quotaLabel}</th>
<th>{i18n.t("workspace.lite.subscription.contribution")}</th>
</tr>
</thead>
<tbody>
<For each={props.usage.rows}>
{(row) => {
const quota = getModelQuotaLimit(props.usage.limit, row.multiplier)
return (
<tr>
<td>
<bdi dir="auto">{row.name}</bdi>
</td>
<td>{row.cost === undefined ? "-" : money(row.cost)}</td>
<td>{quota === undefined ? "-" : money(quota)}</td>
<td>{row.contributionPercent}%</td>
</tr>
)
}}
</For>
<tr data-slot="usage-total">
<td colSpan={3}>{i18n.t("workspace.lite.subscription.total")}</td>
<td>{totalPercentage()}%</td>
</tr>
</tbody>
</table>
</div>
</div>
)
}
function LiteUsageGroup(props: { lite: NonNullable<LiteSubscription> }) {
const params = useParams()
const i18n = useI18n()
const [open, setOpen] = createSignal<LiteUsageWindow>()
const [store, setStore] = createStore({
details: {} as Partial<Record<LiteUsageWindow, LiteUsageDetailsData | null>>,
loading: undefined as LiteUsageWindow | undefined,
})
const items = () =>
[
{
id: "rolling",
label: i18n.t("workspace.lite.subscription.rollingUsage"),
quotaLabel: i18n.t("workspace.lite.subscription.rollingQuota"),
usage: props.lite.rollingUsage,
},
{
id: "weekly",
label: i18n.t("workspace.lite.subscription.weeklyUsage"),
quotaLabel: i18n.t("workspace.lite.subscription.weeklyQuota"),
usage: props.lite.weeklyUsage,
},
{
id: "monthly",
label: i18n.t("workspace.lite.subscription.monthlyUsage"),
quotaLabel: i18n.t("workspace.lite.subscription.monthlyQuota"),
usage: props.lite.monthlyUsage,
},
] as const
const selected = createMemo(() => items().find((item) => item.id === open()))
async function toggle(id: LiteUsageWindow) {
if (open() === id) {
setOpen()
return
}
setOpen(id)
if (store.details[id] !== undefined) return
setStore("loading", id)
const details = await queryLiteUsageDetails(params.id!, id).catch(() => null)
setStore("details", id, details)
setStore("loading", (current) => (current === id ? undefined : current))
}
return (
<>
<div data-slot="usage">
<For each={items()}>
{(item) => (
<LiteUsageItem
id={item.id}
label={item.label}
usage={item.usage}
open={open() === item.id}
onToggle={() => toggle(item.id)}
/>
)}
</For>
</div>
<Show when={selected()}>
{(item) => {
const details = () => store.details[item().id]
return (
<Show
when={details()}
fallback={
<Show when={store.loading === item().id}>
<div data-slot="usage-details-loading">{i18n.t("workspace.lite.loading")}</div>
</Show>
}
>
{(usage) => (
<LiteUsageDetails id={item().id} label={item().label} quotaLabel={item().quotaLabel} usage={usage()} />
)}
</Show>
)
}}
</Show>
</>
)
}
export function LiteSection(props: { lite: LiteSubscription | undefined }) {
const params = useParams()
const i18n = useI18n()
@@ -261,11 +566,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
</a>
.
</div>
<div data-slot="usage">
<LiteUsageItem label={i18n.t("workspace.lite.subscription.rollingUsage")} usage={sub().rollingUsage} />
<LiteUsageItem label={i18n.t("workspace.lite.subscription.weeklyUsage")} usage={sub().weeklyUsage} />
<LiteUsageItem label={i18n.t("workspace.lite.subscription.monthlyUsage")} usage={sub().monthlyUsage} />
</div>
<LiteUsageGroup lite={sub()} />
<form action={setLiteUseBalance} method="post" data-slot="setting-row">
<p>{i18n.t("workspace.lite.subscription.useBalance")}</p>
<input type="hidden" name="workspaceID" value={params.id} />
@@ -339,7 +640,7 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
</p>
<h3 data-slot="promo-models-title">{i18n.t("workspace.lite.promo.modelsTitle")}</h3>
<ul data-slot="promo-models">
<li>Grok 4.5</li>
<li>Grok 4.6</li>
<li>GPT 5.6 Luna</li>
<li>GLM-5.3</li>
<li>GLM-5.2</li>
@@ -361,7 +662,6 @@ export function LiteSection(props: { lite: LiteSubscription | undefined }) {
<li>MiMo-V2.5</li>
<li>MiMo-V2.5-Pro</li>
<li>Hy3</li>
<li>Ox Alpha Free</li>
</ul>
<p data-slot="promo-description">{i18n.t("workspace.lite.promo.footer")}</p>
<div data-slot="subscribe-actions">
@@ -6,6 +6,7 @@ import { Billing } from "@opencode-ai/console-core/billing.js"
import { and, Database, desc, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { checkCheckoutRateLimit } from "~/routes/zen/util/redis"
export function formatDateForTable(date: Date) {
const options: Intl.DateTimeFormatOptions = {
@@ -77,7 +78,8 @@ export const createCheckoutUrl = action(
return json(
await withActor(
() =>
Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl })
checkCheckoutRateLimit(Actor.account())
.then(() => Billing.generateCheckoutUrl({ amount, successUrl, cancelUrl }))
.then((data) => ({ error: undefined, data }))
.catch((e) => ({
error: e.message as string,
@@ -234,12 +234,18 @@ export async function handler(
}
headers.set(k, v)
})
if (isNewInference) {
headers.set("x-zen-model", model)
}
headers.delete("host")
headers.delete("content-length")
headers.delete("x-opencode-request")
if (!isNewInference) headers.delete("x-opencode-session")
headers.delete("x-opencode-project")
headers.delete("x-opencode-client")
if (!isNewInference) {
headers.delete("x-opencode-session")
headers.delete("x-opencode-project")
headers.delete("x-opencode-client")
headers.delete("x-opencode-request")
headers.delete("x-zen-model")
}
return headers
})(),
body: reqBody,
@@ -1108,7 +1114,7 @@ export async function handler(
enrichment: (() => {
if (billingSource === "subscription") return { plan: "sub" }
if (billingSource === "byok") return { plan: "byok" }
if (billingSource === "lite") return { plan: "lite" }
if (billingSource === "lite") return { plan: "lite", costMultiplier: modelInfo.costMultiplier }
return undefined
})(),
}),
@@ -16,3 +16,11 @@ export function getRedis() {
export function buildRateLimitKey(kind: string, identifier: string, interval?: string) {
return `${Resource.App.stage}:ratelimit:${kind}:${identifier}${interval ? `:${interval}` : ""}`
}
export async function checkCheckoutRateLimit(accountID: string) {
const redis = getRedis()
const key = buildRateLimitKey("checkout", accountID)
const count = await redis.incr(key)
if (count === 1) await redis.expire(key, 60 * 60)
if (count > 5) throw new Error("Too many payment attempts. Please try again later.")
}
@@ -0,0 +1,75 @@
import { describe, expect, test } from "bun:test"
import { buildLiteUsageBreakdown, getModelQuotaLimit } from "../src/lib/lite-usage"
describe("Go usage breakdown", () => {
test("derives the model quota from the window limit and multiplier", () => {
expect(getModelQuotaLimit(30, 1)).toBe(30)
expect(getModelQuotaLimit(30, 2)).toBe(15)
expect(getModelQuotaLimit(30, 4)).toBe(7.5)
})
test("groups model quota usage into the percentage of the limit", () => {
const result = buildLiteUsageBreakdown({
usage: 416,
limit: 1_200,
sources: [
{ model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false },
{ model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: false },
],
})
expect(result.usagePercent).toBe(34.7)
expect(result.rows[0]).toMatchObject({ name: "GLM", multiplier: 1.5, contributionPercent: 25 })
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
})
test("distributes credits across the model contributions", () => {
const result = buildLiteUsageBreakdown({
usage: 366,
limit: 1_200,
sources: [
{ model: "glm", name: "GLM", cost: 200, quotaCost: 300, multiplier: 1.5, estimated: false },
{ model: "kimi", name: "Kimi", cost: 116, quotaCost: 116, multiplier: 1, estimated: true },
],
})
expect(result.rows).toHaveLength(2)
expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true)
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
})
test("does not synthesize a row when request history is unavailable", () => {
const result = buildLiteUsageBreakdown({ usage: 120, limit: 1_200, sources: [] })
expect(result.rows).toEqual([])
})
test("allocates rounded percentages without making positive rows negative", () => {
const sources = Array.from({ length: 20 }, (_, index) => ({
model: `model-${index}`,
name: `Model ${index}`,
cost: 4,
quotaCost: 4,
multiplier: 1,
estimated: false,
}))
const result = buildLiteUsageBreakdown({ usage: 80, limit: 10_000, sources })
expect(result.rows.every((row) => row.contributionPercent >= 0)).toBe(true)
expect(result.rows.reduce((total, row) => total + row.contributionPercent, 0)).toBeCloseTo(result.usagePercent)
})
test("keeps multiplier changes for the same model as separate rows", () => {
const result = buildLiteUsageBreakdown({
usage: 500,
limit: 1_000,
sources: [
{ model: "glm", name: "GLM", cost: 100, quotaCost: 100, multiplier: 1, estimated: false },
{ model: "glm", name: "GLM", cost: 200, quotaCost: 400, multiplier: 2, estimated: false },
],
})
expect(result.rows.map((row) => row.multiplier)).toEqual([2, 1])
expect(result.rows.map((row) => row.contributionPercent)).toEqual([40, 10])
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.18.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
@@ -129,6 +129,7 @@ export const UsageTable = mysqlTable(
sessionID: varchar("session_id", { length: 30 }),
enrichment: json("enrichment").$type<{
plan: "sub" | "byok" | "lite"
costMultiplier?: number
}>(),
},
(table) => [...workspaceIndexes(table), index("usage_time_created").on(table.workspaceID, table.timeCreated)],
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.18.22",
"version": "1.18.23",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -11,6 +11,7 @@
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@tsconfig/node22": "22.0.2",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"openai": "5.11.0",
"typescript": "catalog:",
@@ -0,0 +1,18 @@
import { describe, expect, test } from "bun:test"
import { isAllowedAuthorizationRedirect } from "./auth-redirect"
describe("authorization redirect validation", () => {
test("allows registered OpenCode callbacks", () => {
expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai/auth/callback")).toBe(true)
expect(isAllowedAuthorizationRedirect("app", "https://dev.opencode.ai/auth/callback")).toBe(true)
expect(isAllowedAuthorizationRedirect("app", "http://localhost:3000/auth/callback")).toBe(true)
expect(isAllowedAuthorizationRedirect("app", "http://127.0.0.1:3000/auth/callback")).toBe(true)
})
test("rejects unregistered clients and external redirects", () => {
expect(isAllowedAuthorizationRedirect("other", "https://opencode.ai/auth/callback")).toBe(false)
expect(isAllowedAuthorizationRedirect("app", "https://evil.example/callback")).toBe(false)
expect(isAllowedAuthorizationRedirect("app", "https://opencode.ai.evil.example/callback")).toBe(false)
expect(isAllowedAuthorizationRedirect("app", "javascript:alert(1)")).toBe(false)
})
})
@@ -0,0 +1,18 @@
export const isAllowedAuthorizationRedirect = (clientID: string, redirectURI: string) => {
if (clientID !== "app") return false
const redirect = (() => {
try {
return new URL(redirectURI)
} catch {
return undefined
}
})()
if (redirect === undefined) return false
if (redirect.hostname === "localhost" || redirect.hostname === "127.0.0.1") {
return redirect.protocol === "http:" || redirect.protocol === "https:"
}
return (
redirect.protocol === "https:" &&
(redirect.hostname === "opencode.ai" || redirect.hostname.endsWith(".opencode.ai"))
)
}
+13
View File
@@ -17,6 +17,7 @@ import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.j
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { AuthTable } from "@opencode-ai/console-core/schema/auth.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { isAllowedAuthorizationRedirect } from "./auth-redirect.js"
type Env = {
AuthStorage: KVNamespace
@@ -41,6 +42,17 @@ const MY_THEME: Theme = {
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const requestURL = new URL(request.url)
if (requestURL.pathname === "/authorize") {
const redirectURI = requestURL.searchParams.get("redirect_uri")
if (
redirectURI !== null &&
!isAllowedAuthorizationRedirect(requestURL.searchParams.get("client_id") ?? "", redirectURI)
) {
return new Response("Unauthorized client", { status: 400 })
}
}
const result = await issuer({
theme: MY_THEME,
providers: {
@@ -102,6 +114,7 @@ export default {
namespace: env.AuthStorage,
}),
subjects,
allow: ({ clientID, redirectURI }) => Promise.resolve(isAllowedAuthorizationRedirect(clientID, redirectURI)),
async success(ctx, response) {
console.log(response)
+1 -1
View File
@@ -6,6 +6,6 @@
"moduleResolution": "bundler",
"jsx": "preserve",
"jsxImportSource": "react",
"types": ["@cloudflare/workers-types", "node"]
"types": ["@cloudflare/workers-types", "bun", "node"]
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.18.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.22",
"version": "1.18.23",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
+33 -6
View File
@@ -54,12 +54,39 @@ export function applyOnly(db: Database, input: Migration[]) {
if (
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${"__drizzle_migrations"}`)
) {
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
SELECT name, ${Date.now()}
FROM ${sql.identifier("__drizzle_migrations")}
WHERE name IS NOT NULL
`)
const named = (yield* db.all<{ name: string }>(
sql`SELECT name FROM pragma_table_info('__drizzle_migrations')`,
)).some((column) => column.name === "name")
if (named) {
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
SELECT name, ${Date.now()}
FROM ${sql.identifier("__drizzle_migrations")}
WHERE name IS NOT NULL
`)
}
if (!named) {
const entries = yield* db.all<{ created_at: number; prefix: string | null }>(sql`
SELECT created_at, strftime('%Y%m%d%H%M%S', created_at / 1000, 'unixepoch') AS prefix
FROM ${sql.identifier("__drizzle_migrations")}
WHERE created_at IS NOT NULL
`)
for (const entry of entries) {
const migration = input.find((item) => item.id.startsWith(`${entry.prefix}_`))
if (!migration) {
return yield* Effect.die(
new Error(`Legacy migration timestamp ${entry.created_at} does not match any known migration`),
)
}
yield* db.run(sql`
INSERT OR IGNORE INTO ${sql.identifier("migration")} (id, time_completed)
VALUES (${migration.id}, ${Date.now()})
`)
}
}
completed = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM ${sql.identifier("migration")}`)).map((row) => row.id),
)
@@ -5,6 +5,9 @@ export default {
id: "20260410174513_workspace-name",
up(tx) {
return Effect.gen(function* () {
const columns = yield* tx.all<{ name: string }>(`PRAGMA table_info(\`workspace\`)`)
const name = columns.some((column) => column.name === "name") ? "`name`" : "''"
yield* tx.run(`PRAGMA foreign_keys=OFF;`)
yield* tx.run(`
CREATE TABLE \`__new_workspace\` (
@@ -19,7 +22,7 @@ export default {
);
`)
yield* tx.run(
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
`INSERT INTO \`__new_workspace\`(\`id\`, \`type\`, \`branch\`, \`name\`, \`directory\`, \`extra\`, \`project_id\`) SELECT \`id\`, \`type\`, \`branch\`, ${name}, \`directory\`, \`extra\`, \`project_id\` FROM \`workspace\`;`,
)
yield* tx.run(`DROP TABLE \`workspace\`;`)
yield* tx.run(`ALTER TABLE \`__new_workspace\` RENAME TO \`workspace\`;`)
@@ -8,6 +8,7 @@ import { Effect, Layer } from "effect"
import { eq, inArray, sql } from "drizzle-orm"
import { DatabaseMigration } from "@opencode-ai/core/database/migration"
import { migrations } from "@opencode-ai/core/database/migration.gen"
import workspaceNameMigration from "@opencode-ai/core/database/migration/20260410174513_workspace-name"
import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage"
import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths"
import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order"
@@ -38,6 +39,68 @@ const run = <A, E>(effect: Effect.Effect<A, E, SqlClientService>) =>
const makeDb = EffectDrizzleSqlite.makeWithDefaults()
describe("DatabaseMigration", () => {
test("defaults missing workspace names while preserving legacy workspace data", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`
CREATE TABLE workspace (
id text PRIMARY KEY,
type text NOT NULL,
branch text,
directory text,
extra text,
project_id text NOT NULL
)
`)
yield* db.run(sql`
INSERT INTO workspace (id, type, branch, directory, extra, project_id)
VALUES ('wrk_legacy', 'remote', 'main', '/repo', '{}', 'proj_legacy')
`)
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
expect(yield* db.get(sql`SELECT id, name, branch, directory, extra FROM workspace`)).toEqual({
id: "wrk_legacy",
name: "",
branch: "main",
directory: "/repo",
extra: "{}",
})
}),
)
})
test("imports unnamed legacy Drizzle journal entries by their actual migration timestamps", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
yield* db.run(sql`
INSERT INTO __drizzle_migrations (hash, created_at)
VALUES ('', ${Date.UTC(2026, 3, 10, 17, 45, 13)})
`)
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260410174513_workspace-name" }])
}),
)
})
test("rejects unknown legacy Drizzle journal timestamps instead of guessing completed migrations", async () => {
await expect(
run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE __drizzle_migrations (id integer PRIMARY KEY, hash text, created_at integer)`)
yield* db.run(sql`INSERT INTO __drizzle_migrations (hash, created_at) VALUES ('', 1234567890000)`)
yield* DatabaseMigration.applyOnly(db, [workspaceNameMigration])
}),
),
).rejects.toThrow("does not match any known migration")
})
test("serializes concurrent embedded initialization for one database path", async () => {
await using tmp = await tmpdir()
const filename = path.join(tmp.path, "embedded.sqlite")
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.18.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.18.22",
"version": "1.18.23",
"$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.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.22",
"version": "1.18.23",
"name": "opencode",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.18.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"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.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.22",
"version": "1.18.23",
"type": "module",
"license": "MIT",
"scripts": {
+2 -9
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-app",
"version": "1.18.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
@@ -19,25 +19,18 @@
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
"@solidjs/start": "catalog:",
"d3-geo": "3.1.1",
"d3-scale": "4.0.2",
"effect": "catalog:",
"i18n-iso-countries": "7.14.0",
"nitro": "3.0.1-alpha.1",
"solid-js": "catalog:",
"sst": "catalog:",
"topojson-client": "3.1.0",
"vite": "catalog:",
"world-atlas": "2.0.2"
"vite": "catalog:"
},
"devDependencies": {
"@cloudflare/workers-types": "catalog:",
"@types/bun": "catalog:",
"@types/d3-geo": "3.1.0",
"@types/d3-scale": "4.0.9",
"@types/geojson": "7946.0.16",
"@types/topojson-client": "3.1.5",
"@types/topojson-specification": "1.0.5",
"@typescript/native-preview": "catalog:",
"typescript": "catalog:"
},
+1 -6
View File
@@ -126,12 +126,9 @@ const en = {
"home.noMarketDescription": "No model rows matched this range.",
"home.marketChart": "Market share by model author",
"home.noData": "No data",
"home.geoTitle": "Geo Breakdown",
"home.geoDescription": "Tokens used by country.",
"home.geoTitle": "Geographic Breakdown",
"home.noGeoTitle": "No geo data",
"home.noGeoDescription": "No geo rows matched this range.",
"home.worldMap": "World map of token usage by country",
"home.geoMapTitle": "Geo Breakdown map",
"home.unknown": "Unknown",
"home.tokenCostTitle": "Token Cost",
"home.tokenCostDescription": "Price per 1M tokens.",
@@ -236,10 +233,8 @@ const en = {
"model.averageTokensSession": "Average tokens / session",
"model.cacheRatio": "Cache Ratio",
"model.inputTokens": "input tokens",
"model.geoDescription": "OpenCode model tokens used by country.",
"model.noGeoTitle": "No geo data",
"model.noGeoDescription": "No OpenCode geo rows matched this model.",
"model.worldMap": "World map of model token usage by country",
"model.peersDescription": "Nearby models by recent OpenCode token volume.",
"model.noPeersTitle": "No peers",
"model.noPeersDescription": "Peer rankings appear after usage lands.",
-5
View File
@@ -108,11 +108,8 @@ export const dict = {
"home.marketChart": "حصة السوق حسب مؤلف النموذج",
"home.noData": "لا توجد بيانات",
"home.geoTitle": "التوزيع الجغرافي",
"home.geoDescription": "الرموز المستخدمة حسب البلد.",
"home.noGeoTitle": "لا توجد بيانات جغرافية",
"home.noGeoDescription": "لم تطابق أي صفوف جغرافية هذا النطاق.",
"home.worldMap": "خريطة عالمية لاستخدام الرموز حسب البلد",
"home.geoMapTitle": "خريطة التوزيع الجغرافي",
"home.unknown": "غير معروف",
"home.tokenCostTitle": "تكلفة الرموز",
"home.tokenCostDescription": "السعر لكل مليون رمز.",
@@ -216,10 +213,8 @@ export const dict = {
"model.tokensSession": "الرموز / الجلسة",
"model.cacheRatio": "نسبة التخزين المؤقت",
"model.inputTokens": "رموز الإدخال",
"model.geoDescription": "رموز نموذج OpenCode المستخدمة حسب البلد.",
"model.noGeoTitle": "لا توجد بيانات جغرافية",
"model.noGeoDescription": "لم تطابق أي صفوف جغرافية في OpenCode هذا النموذج.",
"model.worldMap": "خريطة عالمية لاستخدام رموز النموذج حسب البلد",
"model.peersDescription": "نماذج قريبة حسب حجم رموز OpenCode الأخير.",
"model.noPeersTitle": "لا توجد نماذج مشابهة",
"model.noPeersDescription": "تظهر ترتيبات النماذج المشابهة بعد وصول الاستخدام.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Participação de mercado por autor do modelo",
"home.noData": "Sem dados",
"home.geoTitle": "Distribuição geográfica",
"home.geoDescription": "Tokens usados por país.",
"home.noGeoTitle": "Sem dados geográficos",
"home.noGeoDescription": "Nenhuma linha geográfica correspondeu a este intervalo.",
"home.worldMap": "Mapa-múndi do uso de tokens por país",
"home.geoMapTitle": "Mapa da distribuição geográfica",
"home.unknown": "Desconhecido",
"home.tokenCostTitle": "Custo de tokens",
"home.tokenCostDescription": "Preço por 1 milhão de tokens.",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "Tokens / sessão",
"model.cacheRatio": "Taxa de cache",
"model.inputTokens": "tokens de entrada",
"model.geoDescription": "Tokens do modelo OpenCode usados por país.",
"model.noGeoTitle": "Sem dados geográficos",
"model.noGeoDescription": "Nenhuma linha geográfica do OpenCode correspondeu a este modelo.",
"model.worldMap": "Mapa-múndi do uso de tokens do modelo por país",
"model.peersDescription": "Modelos próximos por volume recente de tokens do OpenCode.",
"model.noPeersTitle": "Sem pares",
"model.noPeersDescription": "Os rankings de pares aparecem depois que o uso chega.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Markedsandel efter modelforfatter",
"home.noData": "Ingen data",
"home.geoTitle": "Geografisk opdeling",
"home.geoDescription": "Tokens brugt efter land.",
"home.noGeoTitle": "Ingen geodata",
"home.noGeoDescription": "Ingen georækker matchede dette interval.",
"home.worldMap": "Verdenskort over tokenbrug efter land",
"home.geoMapTitle": "Kort over geografisk opdeling",
"home.unknown": "Ukendt",
"home.tokenCostTitle": "Tokenomkostning",
"home.tokenCostDescription": "Pris pr. 1 mio. tokens.",
@@ -217,10 +214,8 @@ export const dict = {
"model.tokensSession": "Tokens / session",
"model.cacheRatio": "Cacheandel",
"model.inputTokens": "inputtokens",
"model.geoDescription": "OpenCode-modeltokens brugt efter land.",
"model.noGeoTitle": "Ingen geodata",
"model.noGeoDescription": "Ingen OpenCode-georækker matchede denne model.",
"model.worldMap": "Verdenskort over modeltokenbrug efter land",
"model.peersDescription": "Nærliggende modeller efter seneste OpenCode-tokenvolumen.",
"model.noPeersTitle": "Ingen lignende modeller",
"model.noPeersDescription": "Ranglister over lignende modeller vises, når brug lander.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Marktanteil nach Modellautor",
"home.noData": "Keine Daten",
"home.geoTitle": "Geografische Aufschlüsselung",
"home.geoDescription": "Nach Land verwendete Tokens.",
"home.noGeoTitle": "Keine Geodaten",
"home.noGeoDescription": "Keine Geozeilen passten zu diesem Zeitraum.",
"home.worldMap": "Weltkarte der Tokennutzung nach Land",
"home.geoMapTitle": "Karte der geografischen Aufschlüsselung",
"home.unknown": "Unbekannt",
"home.tokenCostTitle": "Tokenkosten",
"home.tokenCostDescription": "Preis pro 1 Mio. Tokens.",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "Tokens / Sitzung",
"model.cacheRatio": "Cache-Anteil",
"model.inputTokens": "Eingabetokens",
"model.geoDescription": "OpenCode-Modelltokens nach Land.",
"model.noGeoTitle": "Keine Geodaten",
"model.noGeoDescription": "Keine OpenCode-Geozeilen passten zu diesem Modell.",
"model.worldMap": "Weltkarte der Modelltokennutzung nach Land",
"model.peersDescription": "Nahe Modelle nach aktuellem OpenCode-Tokenvolumen.",
"model.noPeersTitle": "Keine Vergleichsmodelle",
"model.noPeersDescription": "Vergleichsrankings erscheinen, nachdem Nutzung eingegangen ist.",
-5
View File
@@ -108,11 +108,8 @@ export const dict = {
"home.marketChart": "Cuota de mercado por autor del modelo",
"home.noData": "Sin datos",
"home.geoTitle": "Desglose geográfico",
"home.geoDescription": "Tokens usados por país.",
"home.noGeoTitle": "Sin datos geográficos",
"home.noGeoDescription": "Ninguna fila geográfica coincidió con este rango.",
"home.worldMap": "Mapa mundial del uso de tokens por país",
"home.geoMapTitle": "Mapa de desglose geográfico",
"home.unknown": "Desconocido",
"home.tokenCostTitle": "Coste de tokens",
"home.tokenCostDescription": "Precio por 1 M de tokens.",
@@ -218,10 +215,8 @@ export const dict = {
"model.tokensSession": "Tokens / sesión",
"model.cacheRatio": "Ratio de caché",
"model.inputTokens": "tokens de entrada",
"model.geoDescription": "Tokens del modelo de OpenCode usados por país.",
"model.noGeoTitle": "Sin datos geográficos",
"model.noGeoDescription": "Ninguna fila geográfica de OpenCode coincidió con este modelo.",
"model.worldMap": "Mapa mundial del uso de tokens del modelo por país",
"model.peersDescription": "Modelos cercanos por volumen reciente de tokens de OpenCode.",
"model.noPeersTitle": "Sin modelos similares",
"model.noPeersDescription": "Las clasificaciones de modelos similares aparecen después de que llegue uso.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Part de marché par auteur de modèle",
"home.noData": "Aucune donnée",
"home.geoTitle": "Répartition géographique",
"home.geoDescription": "Tokens utilisés par pays.",
"home.noGeoTitle": "Aucune donnée géographique",
"home.noGeoDescription": "Aucune ligne géographique ne correspondait à cette période.",
"home.worldMap": "Carte mondiale de l'utilisation des tokens par pays",
"home.geoMapTitle": "Carte de répartition géographique",
"home.unknown": "Inconnu",
"home.tokenCostTitle": "Coût des tokens",
"home.tokenCostDescription": "Prix par million de tokens.",
@@ -220,10 +217,8 @@ export const dict = {
"model.tokensSession": "Tokens / session",
"model.cacheRatio": "Taux de cache",
"model.inputTokens": "tokens d'entrée",
"model.geoDescription": "Tokens du modèle OpenCode utilisés par pays.",
"model.noGeoTitle": "Aucune donnée géographique",
"model.noGeoDescription": "Aucune ligne géographique OpenCode ne correspondait à ce modèle.",
"model.worldMap": "Carte mondiale de l'utilisation des tokens du modèle par pays",
"model.peersDescription": "Modèles proches par volume récent de tokens OpenCode.",
"model.noPeersTitle": "Aucun modèle proche",
"model.noPeersDescription": "Les classements de modèles proches apparaissent après l'arrivée de l'utilisation.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Quota di mercato per autore del modello",
"home.noData": "Nessun dato",
"home.geoTitle": "Ripartizione geografica",
"home.geoDescription": "Token usati per paese.",
"home.noGeoTitle": "Nessun dato geografico",
"home.noGeoDescription": "Nessuna riga geografica corrispondeva a questo intervallo.",
"home.worldMap": "Mappa mondiale dell'utilizzo dei token per paese",
"home.geoMapTitle": "Mappa della ripartizione geografica",
"home.unknown": "Sconosciuto",
"home.tokenCostTitle": "Costo token",
"home.tokenCostDescription": "Prezzo per 1 M di token.",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "Token / sessione",
"model.cacheRatio": "Rapporto cache",
"model.inputTokens": "token di input",
"model.geoDescription": "Token del modello OpenCode usati per paese.",
"model.noGeoTitle": "Nessun dato geografico",
"model.noGeoDescription": "Nessuna riga geografica OpenCode corrispondeva a questo modello.",
"model.worldMap": "Mappa mondiale dell'utilizzo dei token del modello per paese",
"model.peersDescription": "Modelli vicini per volume recente di token OpenCode.",
"model.noPeersTitle": "Nessun modello simile",
"model.noPeersDescription": "Le classifiche dei modelli simili appaiono dopo l'arrivo dell'utilizzo.",
-5
View File
@@ -111,11 +111,8 @@ export const dict = {
"home.marketChart": "モデル作者別マーケットシェア",
"home.noData": "データなし",
"home.geoTitle": "地域別内訳",
"home.geoDescription": "国別のトークン使用量。",
"home.noGeoTitle": "地域データがありません",
"home.noGeoDescription": "この期間に一致する地域行はありません。",
"home.worldMap": "国別トークン使用量の世界地図",
"home.geoMapTitle": "地域別内訳マップ",
"home.unknown": "不明",
"home.tokenCostTitle": "トークンコスト",
"home.tokenCostDescription": "100万トークンあたりの価格。",
@@ -220,10 +217,8 @@ export const dict = {
"model.tokensSession": "トークン / セッション",
"model.cacheRatio": "キャッシュ比率",
"model.inputTokens": "入力トークン",
"model.geoDescription": "国別のOpenCodeモデルのトークン使用量。",
"model.noGeoTitle": "地域データがありません",
"model.noGeoDescription": "このモデルに一致するOpenCode地域行はありません。",
"model.worldMap": "国別モデル別トークン使用量の世界地図",
"model.peersDescription": "最近のOpenCodeトークン量が近いモデル。",
"model.noPeersTitle": "類似モデルがありません",
"model.noPeersDescription": "使用量が届くと類似モデルのランキングが表示されます。",
-5
View File
@@ -111,11 +111,8 @@ export const dict = {
"home.marketChart": "모델 작성자별 시장 점유율",
"home.noData": "데이터 없음",
"home.geoTitle": "지역별 분포",
"home.geoDescription": "국가별 사용 토큰입니다.",
"home.noGeoTitle": "지역 데이터 없음",
"home.noGeoDescription": "이 범위에 맞는 지역 행이 없습니다.",
"home.worldMap": "국가별 토큰 사용량 세계 지도",
"home.geoMapTitle": "지역별 분포 지도",
"home.unknown": "알 수 없음",
"home.tokenCostTitle": "토큰 비용",
"home.tokenCostDescription": "100만 토큰당 가격입니다.",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "토큰 / 세션",
"model.cacheRatio": "캐시 비율",
"model.inputTokens": "입력 토큰",
"model.geoDescription": "국가별 OpenCode 모델 토큰 사용량입니다.",
"model.noGeoTitle": "지역 데이터 없음",
"model.noGeoDescription": "이 모델과 일치하는 OpenCode 지역 행이 없습니다.",
"model.worldMap": "국가별 모델 토큰 사용량 세계 지도",
"model.peersDescription": "최근 OpenCode 토큰 볼륨이 가까운 모델입니다.",
"model.noPeersTitle": "비슷한 모델 없음",
"model.noPeersDescription": "사용량이 들어오면 비슷한 모델 순위가 표시됩니다.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Markedsandel etter modellforfatter",
"home.noData": "Ingen data",
"home.geoTitle": "Geografisk fordeling",
"home.geoDescription": "Tokens brukt etter land.",
"home.noGeoTitle": "Ingen geodata",
"home.noGeoDescription": "Ingen georader matchet dette intervallet.",
"home.worldMap": "Verdenskart over tokenbruk etter land",
"home.geoMapTitle": "Kart over geografisk fordeling",
"home.unknown": "Ukjent",
"home.tokenCostTitle": "Tokenkostnad",
"home.tokenCostDescription": "Pris per 1 mill. tokens.",
@@ -218,10 +215,8 @@ export const dict = {
"model.tokensSession": "Tokens / økt",
"model.cacheRatio": "Cacheandel",
"model.inputTokens": "inndata-tokens",
"model.geoDescription": "OpenCode-modelltokens brukt etter land.",
"model.noGeoTitle": "Ingen geodata",
"model.noGeoDescription": "Ingen OpenCode-georader matchet denne modellen.",
"model.worldMap": "Verdenskart over modelltokenbruk etter land",
"model.peersDescription": "Nærliggende modeller etter nylig OpenCode-tokenvolum.",
"model.noPeersTitle": "Ingen lignende modeller",
"model.noPeersDescription": "Rangeringer for lignende modeller vises etter at bruk lander.",
-5
View File
@@ -108,11 +108,8 @@ export const dict = {
"home.marketChart": "Udział w rynku według autora modelu",
"home.noData": "Brak danych",
"home.geoTitle": "Podział geograficzny",
"home.geoDescription": "Tokeny użyte według kraju.",
"home.noGeoTitle": "Brak danych geograficznych",
"home.noGeoDescription": "Żadne wiersze geograficzne nie pasowały do tego zakresu.",
"home.worldMap": "Mapa świata użycia tokenów według kraju",
"home.geoMapTitle": "Mapa podziału geograficznego",
"home.unknown": "Nieznane",
"home.tokenCostTitle": "Koszt tokenów",
"home.tokenCostDescription": "Cena za 1 mln tokenów.",
@@ -217,10 +214,8 @@ export const dict = {
"model.tokensSession": "Tokeny / sesja",
"model.cacheRatio": "Współczynnik cache",
"model.inputTokens": "tokeny wejściowe",
"model.geoDescription": "Tokeny modelu OpenCode użyte według kraju.",
"model.noGeoTitle": "Brak danych geograficznych",
"model.noGeoDescription": "Żadne wiersze geograficzne OpenCode nie pasowały do tego modelu.",
"model.worldMap": "Mapa świata użycia tokenów modelu według kraju",
"model.peersDescription": "Pobliskie modele według ostatniego wolumenu tokenów OpenCode.",
"model.noPeersTitle": "Brak podobnych modeli",
"model.noPeersDescription": "Rankingi podobnych modeli pojawią się po nadejściu użycia.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Доля рынка по автору модели",
"home.noData": "Нет данных",
"home.geoTitle": "Географический разрез",
"home.geoDescription": "Токены, использованные по странам.",
"home.noGeoTitle": "Нет геоданных",
"home.noGeoDescription": "Нет географических строк для этого диапазона.",
"home.worldMap": "Карта мира использования токенов по странам",
"home.geoMapTitle": "Карта географического разреза",
"home.unknown": "Неизвестно",
"home.tokenCostTitle": "Стоимость токенов",
"home.tokenCostDescription": "Цена за 1 млн токенов.",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "Токены / сеанс",
"model.cacheRatio": "Доля кэша",
"model.inputTokens": "входные токены",
"model.geoDescription": "Токены модели OpenCode, использованные по странам.",
"model.noGeoTitle": "Нет геоданных",
"model.noGeoDescription": "Нет географических строк OpenCode для этой модели.",
"model.worldMap": "Карта мира использования токенов модели по странам",
"model.peersDescription": "Близкие модели по недавнему объему токенов OpenCode.",
"model.noPeersTitle": "Нет похожих моделей",
"model.noPeersDescription": "Рейтинги похожих моделей появятся после использования.",
-5
View File
@@ -110,11 +110,8 @@ export const dict = {
"home.marketChart": "ส่วนแบ่งตลาดตามผู้สร้างโมเดล",
"home.noData": "ไม่มีข้อมูล",
"home.geoTitle": "แยกตามภูมิศาสตร์",
"home.geoDescription": "token ที่ใช้แยกตามประเทศ",
"home.noGeoTitle": "ไม่มีข้อมูลภูมิศาสตร์",
"home.noGeoDescription": "ไม่มีแถวภูมิศาสตร์ที่ตรงกับช่วงเวลานี้",
"home.worldMap": "แผนที่โลกของการใช้ token แยกตามประเทศ",
"home.geoMapTitle": "แผนที่แยกตามภูมิศาสตร์",
"home.unknown": "ไม่ทราบ",
"home.tokenCostTitle": "ต้นทุน Token",
"home.tokenCostDescription": "ราคาต่อ 1 ล้าน token",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "Token / เซสชัน",
"model.cacheRatio": "อัตราแคช",
"model.inputTokens": "input token",
"model.geoDescription": "token ของโมเดล OpenCode ที่ใช้แยกตามประเทศ",
"model.noGeoTitle": "ไม่มีข้อมูลภูมิศาสตร์",
"model.noGeoDescription": "ไม่มีแถวภูมิศาสตร์ของ OpenCode ที่ตรงกับโมเดลนี้",
"model.worldMap": "แผนที่โลกของการใช้ token ของโมเดลแยกตามประเทศ",
"model.peersDescription": "โมเดลใกล้เคียงตามปริมาณ token ล่าสุดของ OpenCode",
"model.noPeersTitle": "ไม่มีโมเดลใกล้เคียง",
"model.noPeersDescription": "อันดับโมเดลใกล้เคียงจะแสดงหลังจากมีการใช้งานเข้ามา",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Model yazarına göre pazar payı",
"home.noData": "Veri yok",
"home.geoTitle": "Coğrafi Dağılım",
"home.geoDescription": "Ülkeye göre kullanılan tokenlar.",
"home.noGeoTitle": "Coğrafi veri yok",
"home.noGeoDescription": "Bu aralıkla eşleşen coğrafi satır yok.",
"home.worldMap": "Ülkeye göre token kullanımının dünya haritası",
"home.geoMapTitle": "Coğrafi Dağılım haritası",
"home.unknown": "Bilinmiyor",
"home.tokenCostTitle": "Token Maliyeti",
"home.tokenCostDescription": "1 milyon token başına fiyat.",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "Token / Oturum",
"model.cacheRatio": "Önbellek Oranı",
"model.inputTokens": "giriş tokenları",
"model.geoDescription": "Ülkeye göre kullanılan OpenCode model tokenları.",
"model.noGeoTitle": "Coğrafi veri yok",
"model.noGeoDescription": "Bu modelle eşleşen OpenCode coğrafi satırı yok.",
"model.worldMap": "Ülkeye göre model token kullanımının dünya haritası",
"model.peersDescription": "Son OpenCode token hacmine göre yakındaki modeller.",
"model.noPeersTitle": "Benzer yok",
"model.noPeersDescription": "Benzer model sıralamaları kullanım geldikten sonra görünür.",
-5
View File
@@ -109,11 +109,8 @@ export const dict = {
"home.marketChart": "Частка ринку за автором моделі",
"home.noData": "Немає даних",
"home.geoTitle": "Географічний розріз",
"home.geoDescription": "Токени, використані за країнами.",
"home.noGeoTitle": "Немає геоданих",
"home.noGeoDescription": "Жодні географічні рядки не відповідали цьому діапазону.",
"home.worldMap": "Карта світу використання токенів за країнами",
"home.geoMapTitle": "Карта географічного розрізу",
"home.unknown": "Невідомо",
"home.tokenCostTitle": "Вартість токенів",
"home.tokenCostDescription": "Ціна за 1 млн токенів.",
@@ -219,10 +216,8 @@ export const dict = {
"model.tokensSession": "Токени / сеанс",
"model.cacheRatio": "Частка кешу",
"model.inputTokens": "вхідні токени",
"model.geoDescription": "Токени моделі OpenCode, використані за країнами.",
"model.noGeoTitle": "Немає геоданих",
"model.noGeoDescription": "Жодні географічні рядки OpenCode не відповідали цій моделі.",
"model.worldMap": "Карта світу використання токенів моделі за країнами",
"model.peersDescription": "Близькі моделі за нещодавнім обсягом токенів OpenCode.",
"model.noPeersTitle": "Немає схожих моделей",
"model.noPeersDescription": "Рейтинги схожих моделей з'являться після використання.",
-5
View File
@@ -110,11 +110,8 @@ export const dict = {
"home.marketChart": "按模型作者显示的市场份额",
"home.noData": "无数据",
"home.geoTitle": "地理分布",
"home.geoDescription": "按国家/地区统计的 token 使用量。",
"home.noGeoTitle": "无地理数据",
"home.noGeoDescription": "没有符合该时间范围的地理行。",
"home.worldMap": "按国家/地区显示 token 使用量的世界地图",
"home.geoMapTitle": "地理分布地图",
"home.unknown": "未知",
"home.tokenCostTitle": "Token 成本",
"home.tokenCostDescription": "每 100 万 token 的价格。",
@@ -218,10 +215,8 @@ export const dict = {
"model.tokensSession": "Token / 会话",
"model.cacheRatio": "缓存比例",
"model.inputTokens": "输入 token",
"model.geoDescription": "按国家/地区统计的 OpenCode 模型 token 使用量。",
"model.noGeoTitle": "无地理数据",
"model.noGeoDescription": "没有符合此模型的 OpenCode 地理行。",
"model.worldMap": "按国家/地区显示模型 token 使用量的世界地图",
"model.peersDescription": "按近期 OpenCode token 用量排列的相近模型。",
"model.noPeersTitle": "无同类模型",
"model.noPeersDescription": "使用量到达后会显示同类模型排名。",
-5
View File
@@ -110,11 +110,8 @@ export const dict = {
"home.marketChart": "按模型作者顯示的市場佔有率",
"home.noData": "無數據",
"home.geoTitle": "地理分布",
"home.geoDescription": "按國家/地區統計的 token 使用量。",
"home.noGeoTitle": "無地理數據",
"home.noGeoDescription": "沒有符合該時間範圍的地理列。",
"home.worldMap": "按國家/地區顯示 token 使用量的世界地圖",
"home.geoMapTitle": "地理分布地圖",
"home.unknown": "未知",
"home.tokenCostTitle": "Token 成本",
"home.tokenCostDescription": "每 100 萬 token 的價格。",
@@ -218,10 +215,8 @@ export const dict = {
"model.tokensSession": "Token / 工作階段",
"model.cacheRatio": "快取比例",
"model.inputTokens": "輸入 token",
"model.geoDescription": "按國家/地區統計的 OpenCode 模型 token 使用量。",
"model.noGeoTitle": "無地理數據",
"model.noGeoDescription": "沒有符合此模型的 OpenCode 地理列。",
"model.worldMap": "按國家/地區顯示模型 token 使用量的世界地圖",
"model.peersDescription": "按近期 OpenCode token 用量排列的相近模型。",
"model.noPeersTitle": "無同類模型",
"model.noPeersDescription": "使用量到達後會顯示同類模型排名。",
+2 -124
View File
@@ -17,7 +17,6 @@ import { useI18n } from "../../context/i18n"
import { useLanguage } from "../../context/language"
import { localizedUrl } from "../../lib/language"
import { findModelCatalogEntry, formatCatalogLabName, loadModelCatalog, type ModelCatalogEntry } from "../model-catalog"
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map"
import { SectionHeading } from "../section-heading"
import { runStatsEffect } from "../../stats-runtime"
import { setStatsPageCacheHeaders } from "../stats-cache"
@@ -892,21 +891,10 @@ function ModelEfficiencySection(props: { data: StatsModelPageData | null; catalo
function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) {
const i18n = useI18n()
const language = useLanguage()
const [activeCountry, setActiveCountry] = createSignal<string>()
const data = createMemo(() => props.data)
const countryById = createMemo(
() =>
new Map(
data().flatMap((country) => {
const id = countryNumericId(country.country)
return id ? [[id, country] as const] : []
}),
),
)
const maxTokens = createMemo(() => Math.max(0, ...data().map((country) => country.tokens)) || 1)
const topCountries = createMemo(() => data().slice(0, 15))
const active = createMemo(() => data().find((country) => country.country === activeCountry()) ?? data()[0])
return (
<section
@@ -917,36 +905,12 @@ function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) {
setActiveCountry(undefined)
}}
>
<SectionTitle
href="#geo-breakdown"
title={i18n.t("nav.geoBreakdown")}
description={i18n.t("model.geoDescription")}
/>
<SectionTitle href="#geo-breakdown" title={i18n.t("home.geoTitle")} />
<Show
when={data().length > 0}
fallback={<ModelEmptyState title={i18n.t("model.noGeoTitle")} description={i18n.t("model.noGeoDescription")} />}
>
<div data-component="geo-breakdown">
<div data-slot="geo-map-panel">
<GeoWorldMap
countryById={countryById()}
activeCountry={activeCountry()}
maxTokens={maxTokens()}
onActiveCountryChange={setActiveCountry}
/>
<Show when={active()}>
{(country) => (
<div data-slot="geo-active-country">
<span>#{String(country().rank).padStart(2, "0")}</span>
<strong>{formatCountryName(country().country, language.tag(language.locale()), i18n)}</strong>
<p>
<b>{formatGeoTokens(country().tokens)}</b>
<em>{formatGeoShare(country().share)}</em>
</p>
</div>
)}
</Show>
</div>
<GeoCountryList
data={topCountries()}
activeCountry={activeCountry()}
@@ -959,92 +923,6 @@ function ModelGeoBreakdownSection(props: { data: CountryEntry[] }) {
)
}
function GeoWorldMap(props: {
countryById: Map<string, CountryEntry>
activeCountry: string | undefined
maxTokens: number
onActiveCountryChange: (country: string | undefined) => void
}) {
const i18n = useI18n()
const opacityScale = createMemo(() => scaleSqrt().domain([0, props.maxTokens]).range([0.26, 0.96]).clamp(true))
const countryOpacity = (country: CountryEntry | undefined) => {
if (!country || country.tokens <= 0) return 0
const opacity = opacityScale()(country.tokens)
if (props.activeCountry === country.country) return 1
if (!props.activeCountry) return opacity
return Math.max(0.18, opacity * 0.36)
}
return (
<svg
data-component="geo-world-map"
viewBox={`0 0 ${geoMapWidth} ${geoMapHeight}`}
role="img"
aria-label={i18n.t("model.worldMap")}
>
<title>{i18n.t("home.geoMapTitle")}</title>
<g data-slot="geo-countries">
<For each={worldCountryPaths}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<path
d={country.path}
data-country-id={country.id}
data-has-data={entry() ? "true" : undefined}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
)
}}
</For>
</g>
<g data-slot="geo-country-markers">
<For each={worldCountryMarkers}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<Show when={entry()}>
<circle
cx={country.marker.x}
cy={country.marker.y}
data-country-id={country.id}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
</Show>
)
}}
</For>
</g>
<path data-slot="geo-borders" d={worldBorderPath} aria-hidden="true" />
</svg>
)
}
function GeoCountryList(props: {
data: CountryEntry[]
activeCountry: string | undefined
@@ -1137,7 +1015,7 @@ function PeerRow(props: { peer: ModelPeerEntry; active: boolean }) {
)
}
function SectionTitle(props: { href: string; title: string; description: string }) {
function SectionTitle(props: { href: string; title: string; description?: string }) {
return <SectionHeading href={props.href} title={props.title} description={props.description} />
}
-120
View File
@@ -1,120 +0,0 @@
import { geoEquirectangular, geoPath } from "d3-geo"
import { feature, mesh } from "topojson-client"
import countriesTopologySource from "world-atlas/countries-110m.json?raw"
import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson"
import type { GeometryCollection, Topology } from "topojson-specification"
export const geoMapWidth = 960
export const geoMapHeight = 430
type WorldCountryProperties = GeoJsonProperties & { name?: string }
type WorldTopology = Topology<{ countries: GeometryCollection<WorldCountryProperties> }>
const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology
const worldCountryGeometries: GeometryCollection<WorldCountryProperties> = {
...worldTopology.objects.countries,
geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"),
}
const worldCountries = feature<WorldCountryProperties>(worldTopology, worldCountryGeometries) as FeatureCollection<
GeometryObject,
WorldCountryProperties
>
const worldProjection = geoEquirectangular().fitExtent(
[
[10, 12],
[geoMapWidth - 10, geoMapHeight - 12],
],
worldCountries,
)
const worldPath = geoPath(worldProjection)
export const worldCountryPaths = worldCountries.features.map((country) => ({
id: String(country.id ?? "").padStart(3, "0"),
path: worldPath(country) ?? "",
}))
export const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? ""
function geoCountryMarker(country: (typeof worldCountries.features)[number]) {
const bounds = worldPath.bounds(country)
const [x, y] = worldPath.centroid(country)
if (!Number.isFinite(x) || !Number.isFinite(y)) return undefined
if (bounds[1][0] - bounds[0][0] >= 3 && bounds[1][1] - bounds[0][1] >= 3) return undefined
return { x, y }
}
// The 110m topology omits small regions. Geographic centroids keep those countries interactive without shipping 50m paths.
const fallbackCountryMarkerCoordinates = [
["016", -170.7179, -14.3046],
["020", 1.5606, 42.542],
["028", -61.7945, 17.2762],
["048", 50.5425, 26.0417],
["052", -59.5602, 13.1811],
["060", -64.7558, 32.3131],
["086", 72.4453, -7.3312],
["092", -64.4704, 18.5276],
["132", -23.9576, 15.9551],
["136", -80.9129, 19.43],
["174", 43.6844, -11.879],
["184", -159.7871, -21.2195],
["212", -61.3576, 15.4394],
["234", -6.8808, 62.0527],
["239", -36.4863, -54.4641],
["248", 19.9528, 60.2153],
["258", -144.8045, -14.7283],
["296", -167.9217, 0.893],
["308", -61.6818, 12.1174],
["316", 144.767, 13.4406],
["334", 73.52, -53.0872],
["336", 12.4343, 41.9021],
["344", 114.1143, 22.3983],
["438", 9.5357, 47.1367],
["446", 113.509, 22.2231],
["462", 73.4573, 3.7316],
["470", 14.405, 35.9215],
["480", 57.5714, -20.2779],
["492", 7.4073, 43.7526],
["500", -62.1856, 16.7404],
["520", 166.9326, -0.5189],
["531", -68.9721, 12.1957],
["533", -69.9827, 12.521],
["534", -63.0572, 18.0509],
["570", -169.8704, -19.0489],
["574", 167.9497, -29.0516],
["580", 145.6193, 15.8288],
["583", 153.2966, 7.5361],
["584", 170.3313, 7.015],
["585", 134.4056, 7.286],
["612", -128.3167, -24.3649],
["652", -62.841, 17.8988],
["654", -9.7009, -12.3548],
["659", -62.6873, 17.2647],
["660", -63.066, 18.2243],
["662", -60.9696, 13.8946],
["663", -63.0599, 18.0888],
["666", -56.3037, 46.9187],
["670", -61.2008, 13.2251],
["674", 12.4594, 43.9415],
["678", 6.7235, 0.4434],
["690", 55.476, -4.6601],
["702", 103.817, 1.359],
["776", -174.7998, -20.4161],
["796", -71.9734, 21.8312],
["831", -2.5726, 49.4678],
["832", -2.1272, 49.2181],
["833", -4.5388, 54.224],
["850", -64.8028, 17.9555],
["876", -177.3469, -13.8898],
["882", -172.1649, -13.7536],
] as const
export const worldCountryMarkers = [
...worldCountries.features.flatMap((country) => {
const marker = geoCountryMarker(country)
return marker ? [{ id: String(country.id ?? "").padStart(3, "0"), marker }] : []
}),
...fallbackCountryMarkerCoordinates.flatMap(([id, longitude, latitude]) => {
const marker = worldProjection([longitude, latitude])
return marker ? [{ id, marker: { x: marker[0], y: marker[1] } }] : []
}),
]
-122
View File
@@ -2264,121 +2264,6 @@ body {
align-items: start;
}
[data-page="stats"] [data-slot="geo-map-panel"] {
position: relative;
min-width: 0;
overflow: hidden;
background: var(--stats-layer);
border: 1px solid var(--stats-line);
}
[data-page="stats"] [data-component="geo-world-map"] {
display: block;
width: 100%;
height: auto;
}
[data-page="stats"] [data-slot="geo-countries"] path {
fill: var(--stats-layer-2);
stroke: var(--stats-bg);
stroke-width: 0.45px;
transition:
fill 140ms ease,
opacity 140ms ease;
}
[data-page="stats"] [data-slot="geo-countries"] path[data-has-data="true"] {
fill: var(--stats-accent);
opacity: var(--geo-country-opacity);
cursor: pointer;
}
[data-page="stats"] [data-slot="geo-countries"] path[data-active="true"] {
fill: color-mix(in srgb, var(--stats-accent) 70%, var(--stats-text));
opacity: var(--geo-country-opacity);
}
[data-page="stats"] [data-slot="geo-country-markers"] circle {
fill: var(--stats-accent);
stroke: var(--stats-bg);
stroke-width: 1.1px;
opacity: var(--geo-country-opacity);
cursor: pointer;
transition:
fill 140ms ease,
opacity 140ms ease,
r 140ms ease;
}
[data-page="stats"] [data-slot="geo-country-markers"] circle[data-active="true"] {
fill: color-mix(in srgb, var(--stats-accent) 70%, var(--stats-text));
opacity: var(--geo-country-opacity);
}
[data-page="stats"] [data-slot="geo-borders"] {
fill: none;
stroke: var(--stats-line-strong);
stroke-linejoin: round;
stroke-width: 0.6px;
pointer-events: none;
}
[data-page="stats"] [data-slot="geo-active-country"] {
position: absolute;
bottom: 16px;
left: 16px;
display: grid;
gap: 8px;
min-width: 168px;
max-width: calc(100% - 32px);
box-sizing: border-box;
padding: 12px;
background: color-mix(in srgb, var(--stats-bg) 92%, transparent);
box-shadow:
0 0 0 0.5px var(--stats-line-strong),
0 6px 16px #0000000d,
0 2px 6px #0000000f;
}
[data-page="stats"] [data-slot="geo-active-country"] span,
[data-page="stats"] [data-slot="geo-active-country"] em {
color: var(--stats-faint);
font-style: normal;
}
[data-page="stats"] [data-slot="geo-active-country"] span {
font-size: 10px;
font-weight: 600;
line-height: 1;
}
[data-page="stats"] [data-slot="geo-active-country"] strong {
min-width: 0;
overflow: hidden;
color: var(--stats-text);
font-size: 16px;
font-weight: 600;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-page="stats"] [data-slot="geo-active-country"] p {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
color: var(--stats-muted);
font-size: 11px;
font-weight: 500;
line-height: 1;
}
[data-page="stats"] [data-slot="geo-active-country"] b {
color: var(--stats-accent-text);
font-weight: 600;
}
[data-page="stats"] [data-component="geo-country-list"] {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 212px), 1fr));
@@ -8386,13 +8271,6 @@ body {
height: 400px;
}
[data-page="stats"] [data-slot="geo-active-country"] {
position: static;
min-width: 0;
max-width: none;
margin: 0 12px 12px;
}
[data-page="stats"] [data-component="geo-country-list"] button {
grid-template-columns: 26px 8px minmax(0, 1fr) auto;
}
+2 -122
View File
@@ -21,7 +21,6 @@ import { useI18n } from "../context/i18n"
import { useLanguage } from "../context/language"
import { localizedUrl } from "../lib/language"
import { findModelCatalogEntry, loadModelCatalog, type ModelCatalog } from "./model-catalog"
import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "./geo-map"
import { SectionHeading } from "./section-heading"
import { setStatsPageCacheHeaders } from "./stats-cache"
import { ComparisonCardsSection, uniqueComparisonPairs, type ComparisonModelRef } from "./compare-cards"
@@ -317,7 +316,7 @@ function ChartSection(props: {
)
}
function SectionTitle(props: { id: string; title: string; description: string }) {
function SectionTitle(props: { id: string; title: string; description?: string }) {
return <SectionHeading href={`#${props.id}`} title={props.title} description={props.description} />
}
@@ -1074,20 +1073,9 @@ function MarketShareList(props: {
function GeoBreakdownSection(props: { data: CountryEntry[] }) {
const i18n = useI18n()
const language = useLanguage()
const [activeCountry, setActiveCountry] = createSignal<string>()
const countryById = createMemo(
() =>
new Map(
props.data.flatMap((country) => {
const id = countryNumericId(country.country)
return id ? [[id, country] as const] : []
}),
),
)
const maxTokens = createMemo(() => Math.max(0, ...props.data.map((country) => country.tokens)) || 1)
const topCountries = createMemo(() => props.data.slice(0, 15))
const active = createMemo(() => props.data.find((country) => country.country === activeCountry()) ?? props.data[0])
return (
<section
@@ -1099,34 +1087,12 @@ function GeoBreakdownSection(props: { data: CountryEntry[] }) {
}}
>
<SectionBridge label={i18n.t("nav.marketShare").toUpperCase()} href="#market-share" />
<SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} description={i18n.t("home.geoDescription")} />
<SectionTitle id="geo-breakdown" title={i18n.t("home.geoTitle")} />
<Show
when={props.data.length > 0}
fallback={<EmptyState title={i18n.t("home.noGeoTitle")} description={i18n.t("home.noGeoDescription")} />}
>
<div data-component="geo-breakdown">
<div data-slot="geo-map-panel">
<GeoWorldMap
countryById={countryById()}
activeCountry={activeCountry()}
maxTokens={maxTokens()}
onActiveCountryChange={setActiveCountry}
/>
<Show when={active()}>
{(country) => (
<div data-slot="geo-active-country">
<span>#{String(country().rank).padStart(2, "0")}</span>
<strong>
{formatCountryName(country().country, language.tag(language.locale()), i18n.t("home.unknown"))}
</strong>
<p>
<b>{formatGeoTokens(country().tokens)}</b>
<em>{formatGeoShare(country().share)}</em>
</p>
</div>
)}
</Show>
</div>
<GeoCountryList
data={topCountries()}
activeCountry={activeCountry()}
@@ -1139,92 +1105,6 @@ function GeoBreakdownSection(props: { data: CountryEntry[] }) {
)
}
function GeoWorldMap(props: {
countryById: Map<string, CountryEntry>
activeCountry: string | undefined
maxTokens: number
onActiveCountryChange: (country: string | undefined) => void
}) {
const i18n = useI18n()
const opacityScale = createMemo(() => scaleSqrt().domain([0, props.maxTokens]).range([0.26, 0.96]).clamp(true))
const countryOpacity = (country: CountryEntry | undefined) => {
if (!country || country.tokens <= 0) return 0
const opacity = opacityScale()(country.tokens)
if (props.activeCountry === country.country) return 1
if (!props.activeCountry) return opacity
return Math.max(0.18, opacity * 0.36)
}
return (
<svg
data-component="geo-world-map"
viewBox={`0 0 ${geoMapWidth} ${geoMapHeight}`}
role="img"
aria-label={i18n.t("home.worldMap")}
>
<title>{i18n.t("home.geoMapTitle")}</title>
<g data-slot="geo-countries">
<For each={worldCountryPaths}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<path
d={country.path}
data-country-id={country.id}
data-has-data={entry() ? "true" : undefined}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
)
}}
</For>
</g>
<g data-slot="geo-country-markers">
<For each={worldCountryMarkers}>
{(country) => {
const entry = () => props.countryById.get(country.id)
return (
<Show when={entry()}>
<circle
cx={country.marker.x}
cy={country.marker.y}
data-country-id={country.id}
r={entry()?.country === props.activeCountry ? 3.4 : 2.4}
data-active={entry()?.country === props.activeCountry ? "true" : undefined}
style={{ "--geo-country-opacity": String(countryOpacity(entry())) } as JSX.CSSProperties}
aria-hidden="true"
onPointerEnter={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
onClick={() => {
const item = entry()
if (!item) return
props.onActiveCountryChange(item.country)
}}
/>
</Show>
)
}}
</For>
</g>
<path data-slot="geo-borders" d={worldBorderPath} aria-hidden="true" />
</svg>
)
}
function GeoCountryList(props: {
data: CountryEntry[]
activeCountry: string | undefined
@@ -1,7 +1,7 @@
export function SectionHeading(props: {
href: string
title: string
description: string
description?: string
as?: "h2" | "p"
slot?: string
}) {
@@ -12,10 +12,16 @@ export function SectionHeading(props: {
<span data-slot="heading-anchor" aria-hidden="true">
#
</span>
{props.title}.
{props.title}
{props.description ? "." : ""}
</a>
</strong>{" "}
<span>{props.description}</span>
</strong>
{props.description && (
<>
{" "}
<span>{props.description}</span>
</>
)}
</>
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.18.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-server",
"version": "1.18.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/tui",
"version": "1.18.22",
"version": "1.18.23",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.18.22",
"version": "1.18.23",
"type": "module",
"license": "MIT",
"repository": {
+2 -2
View File
@@ -88,10 +88,10 @@ function init() {
setClosing = setClosingSignal
return (
<Kobalte
modal
modal={stack().at(-1)?.id === id}
open={!closing()}
onOpenChange={(open: boolean) => {
if (open) return
if (open || stack().at(-1)?.id !== id) return
close(id)
}}
>
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.18.22",
"version": "1.18.23",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+8 -14
View File
@@ -49,7 +49,7 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
تشمل قائمة النماذج الحالية:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -71,7 +71,6 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (لفترة محدودة)
قد تتغير قائمة النماذج مع استمرارنا في اختبار نماذج جديدة وإضافتها.
@@ -91,7 +90,7 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
| Model | الطلبات لكل 5 ساعات | الطلبات في الأسبوع | الطلبات في الشهر |
| ---------------------------- | ------------------- | ------------------ | ---------------- |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
@@ -113,11 +112,10 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| DeepSeek V4 Flash Vision Exp | 3,800 | 9,450 | 18,900 |
| Hy3 | 4,300 | 10,750 | 21,500 |
| Ox Alpha Free | - | - | - |
تستند التقديرات إلى أنماط الطلبات المرصودة:
- Grok 4.51,100 input، و71,500 cached، و220 output tokens لكل طلب
- Grok 4.6390 input، و32,500 cached، و120 output tokens لكل طلب
- GLM-5.3/5.2/5.1 — 700 input، و52,000 cached، و150 output tokens لكل طلب
- GPT 5.6 Luna — 1,000 توكن إدخال، و50,000 توكن مخزّن مؤقتًا، و220 توكن إخراج لكل طلب
- Kimi K3 — 1,050 input، و76,500 cached، و300 output tokens لكل طلب
@@ -141,7 +139,8 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
| النموذج | الإدخال | الإخراج | القراءة المخزنة | الكتابة المخزنة | الاستخدام |
| --------------------------------------- | ------- | ------- | --------------- | --------------- | --------- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | $15 |
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | $15 |
| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 |
| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
@@ -170,14 +169,11 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
| DeepSeek V4 Flash Vision Exp (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash Vision Exp (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
| Ox Alpha Free | - | - | - | - | - |
**DeepSeek V4 Flash / V4 Flash Vision Exp / V4 Pro:** ساعات Peak هي 01:00-04:00 و06:00-10:00 UTC من الاثنين إلى الجمعة؛ وجميع الساعات الأخرى، بما في ذلك عطلات نهاية الأسبوع، Off-Peak. [اعرف المزيد](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** يتم تحويل الصور إلى رموز بناءً على أبعادها، وتُحتسب كرموز إدخال إلى جانب رموز النص. [اعرف المزيد](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** مجاني لفترة محدودة.
يمكنك تتبّع استخدامك الحالي في **<a href={console}>console</a>**.
:::tip
@@ -212,7 +208,7 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
| Model | Model ID | Endpoint | AI SDK Package |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| GLM-5.3 | glm-5.3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -235,7 +231,6 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ox Alpha Free | ox-alpha-free | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
يستخدم [model id](/docs/config/#models) في إعدادات OpenCode لديك التنسيق `opencode-go/<model-id>`. على سبيل المثال، بالنسبة إلى Kimi K3، ستستخدم `opencode-go/kimi-k3` في إعداداتك.
@@ -255,7 +250,7 @@ https://opencode.ai/zen/go/v1/models
| النموذج | تدريب النموذج | الاحتفاظ بالبيانات |
| ---------------------------- | ------------- | ------------------ |
| Grok 4.5 | غير مستخدَمة | 30 يومًا |
| Grok 4.6 | غير مستخدَمة | 30 يومًا |
| GPT 5.6 Luna | غير مستخدَمة | 30 يومًا |
| GLM-5.3 | غير مستخدَمة | 0 أيام |
| GLM-5.2 | غير مستخدَمة | 0 أيام |
@@ -277,9 +272,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | غير مستخدَمة | 0 أيام |
| DeepSeek V4 Flash Vision Exp | غير مستخدَمة | 0 أيام |
| Hy3 | غير مستخدَمة | 0 أيام |
| Ox Alpha Free | غير مستخدَمة | 0 أيام |
- **Grok 4.5:** تعطّل ZDR ميزات API مهمة تعتمد على البيانات المخزنة، بما في ذلك Responses API ذات الحالة، وFiles and Collections، وBatch API. [اعرف المزيد](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** تعطّل ZDR ميزات API مهمة تعتمد على البيانات المخزنة، بما في ذلك Responses API ذات الحالة، وFiles and Collections، وBatch API. [اعرف المزيد](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** تُنشأ سجلات مراقبة إساءة الاستخدام لكل استخدام لميزات API، ويُحتفظ بها لمدة تصل إلى 30 يومًا. [اعرف المزيد](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** أسعار توكنات مخفّضة للغاية مقابل منح الإذن باستخدام مطالباتك وإكمالات النموذج لتدريب نماذج Meta المستقبلية. يقتصر التوفر على المناطق التي تسمح بها [سياسة الاستخدام الجغرافي](https://ai.developer.meta.com/legal/geographic-use-policy) الخاصة بـ Meta. [اعرف المزيد](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** تُجدَّد اتفاقية ZDR شهريًا. الاتفاقية الحالية سارية حتى 31 أغسطس 2026.
-3
View File
@@ -112,7 +112,6 @@ OpenCode Zen هي بوابة AI تتيح لك الوصول إلى هذه الن
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ox Alpha Free | x-preview-f-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Hy3 Free | hy3-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -140,7 +139,6 @@ https://opencode.ai/zen/v1/models
| النموذج | الإدخال | الإخراج | القراءة المخزنة | الكتابة المخزنة |
| --------------------------------- | ------- | ------- | --------------- | --------------- |
| Big Pickle | Free | Free | Free | - |
| Ox Alpha Free | Free | Free | Free | - |
| MiMo-V2.5 Free | Free | Free | Free | - |
| Hy3 Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
@@ -231,7 +229,6 @@ https://opencode.ai/zen/v1/models
- Nemotron 3 Ultra Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Nemotron 3.5 Lightning Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Big Pickle نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
- Ox Alpha Free نموذج خفي ومتاح مجانا على OpenCode لفترة محدودة. يتبع مزوده سياسة عدم الاحتفاظ بالبيانات ولا يستخدم بياناتك لتدريب النماذج.
- Muse Spark 1.2 Contributor Free متاح على OpenCode لفترة محدودة. يستخدم الفريق هذه الفترة لجمع الملاحظات وتحسين النموذج.
<a href={email}>تواصل معنا</a> إذا كانت لديك أي أسئلة.
+8 -14
View File
@@ -59,7 +59,7 @@ Samo jedan član po radnom prostoru (workspace) može se pretplatiti na OpenCode
Trenutna lista modela uključuje:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +81,6 @@ Trenutna lista modela uključuje:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (ograničeno vrijeme)
Lista modela se može mijenjati dok testiramo i dodajemo nove.
@@ -101,7 +100,7 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori
| Model | zahtjeva na 5 sati | zahtjeva sedmično | zahtjeva mjesečno |
| ---------------------------- | ------------------ | ----------------- | ----------------- |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
@@ -123,11 +122,10 @@ Tabela ispod pruža procijenjeni broj zahtjeva na osnovu tipičnih obrazaca kori
| DeepSeek V4 Flash | 7,600 | 18,900 | 37,800 |
| DeepSeek V4 Flash Vision Exp | 3,800 | 9,450 | 18,900 |
| Hy3 | 4,300 | 10,750 | 21,500 |
| Ox Alpha Free | - | - | - |
Procjene se zasnivaju na zapaženim obrascima zahtjeva:
- Grok 4.51,100 ulaznih, 71,500 keširanih, 220 izlaznih tokena po zahtjevu
- Grok 4.6390 ulaznih, 32,500 keširanih, 120 izlaznih tokena po zahtjevu
- GLM-5.3/5.2/5.1 — 700 ulaznih (input), 52,000 keširanih, 150 izlaznih (output) tokena po zahtjevu
- GPT 5.6 Luna — 1,000 ulaznih, 50,000 keširanih, 220 izlaznih tokena po zahtjevu
- Kimi K3 — 1,050 ulaznih, 76,500 keširanih, 300 izlaznih tokena po zahtjevu
@@ -151,7 +149,8 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj
| Model | Input | Output | Cached Read | Cached Write | Potrošnja |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | --------- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| Grok 4.6 (≤ 200K tokens) | $2.00 | $6.00 | $0.50 | - | $15 |
| Grok 4.6 (> 200K tokens) | $4.00 | $12.00 | $1.00 | - | $15 |
| GPT 5.6 Luna (≤ 272K tokens) | $0.20 | $1.20 | $0.02 | $0.25 | $15 |
| GPT 5.6 Luna (> 272K tokens) | $0.40 | $1.80 | $0.04 | $0.50 | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
@@ -180,14 +179,11 @@ Procjene se također zasnivaju na sljedećim cijenama po 1M tokena i mjesečnoj
| DeepSeek V4 Flash Vision Exp (Off-Peak) | $0.22 | $0.66 | $0.007 | - | $15 |
| DeepSeek V4 Flash Vision Exp (Peak) | $0.44 | $1.32 | $0.014 | - | $15 |
| Hy3 | $0.14 | $0.58 | $0.035 | - | $60 |
| Ox Alpha Free | - | - | - | - | - |
**DeepSeek V4 Flash / V4 Flash Vision Exp / V4 Pro:** Peak sati su 01:00-04:00 i 06:00-10:00 UTC od ponedjeljka do petka; svi ostali sati, uključujući vikende, su Off-Peak. [Saznajte više](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Slike se pretvaraju u tokene na osnovu svojih dimenzija i naplaćuju kao ulazni tokeni zajedno s tekstualnim tokenima. [Saznajte više](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Besplatan ograničeno vrijeme.
Svoju trenutnu potrošnju možete pratiti u **<a href={console}>konzoli</a>**.
:::tip
@@ -224,7 +220,7 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa.
| Model | Model ID | Endpoint | AI SDK Paket |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| Grok 4.6 | grok-4.6 | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| GPT 5.6 Luna | gpt-5.6-luna | `https://opencode.ai/zen/go/v1/responses` | `@ai-sdk/openai` |
| GLM-5.3 | glm-5.3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM-5.2 | glm-5.2 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +243,6 @@ Također možete pristupiti Go modelima putem sljedećih API endpointa.
| Qwen3.7 Plus | qwen3.7-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Qwen3.6 Plus | qwen3.6-plus | `https://opencode.ai/zen/go/v1/messages` | `@ai-sdk/anthropic` |
| Hy3 | hy3 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ox Alpha Free | ox-alpha-free | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
[Model id](/docs/config/#models) u vašoj OpenCode konfiguraciji
koristi format `opencode-go/<model-id>`. Na primjer, za Kimi K3, koristili biste
@@ -269,7 +264,7 @@ https://opencode.ai/zen/go/v1/models
| Model | Treniranje modela | Zadržavanje podataka |
| ---------------------------- | ----------------- | -------------------- |
| Grok 4.5 | Ne koristi se | 30 dana |
| Grok 4.6 | Ne koristi se | 30 dana |
| GPT 5.6 Luna | Ne koristi se | 30 dana |
| GLM-5.3 | Ne koristi se | 0 dana |
| GLM-5.2 | Ne koristi se | 0 dana |
@@ -291,9 +286,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Ne koristi se | 0 dana |
| DeepSeek V4 Flash Vision Exp | Ne koristi se | 0 dana |
| Hy3 | Ne koristi se | 0 dana |
| Ox Alpha Free | Ne koristi se | 0 dana |
- **Grok 4.5:** ZDR onemogućava važne API funkcije koje zavise od pohranjenih podataka, uključujući Responses API s očuvanjem stanja, Files and Collections i Batch API. [Saznajte više](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR onemogućava važne API funkcije koje zavise od pohranjenih podataka, uključujući Responses API s očuvanjem stanja, Files and Collections i Batch API. [Saznajte više](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Zapisi o nadzoru zloupotrebe generišu se za svako korištenje API funkcija i čuvaju do 30 dana. [Saznajte više](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Znatno snižene cijene tokena u zamjenu za dopuštenje da se vaši promptovi i odgovori modela koriste za treniranje budućih Meta modela. Dostupnost je ograničena na regije dopuštene [Pravilima geografskog korištenja](https://ai.developer.meta.com/legal/geographic-use-policy) kompanije Meta. [Saznajte više](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** ZDR sporazum obnavlja se mjesečno. Trenutni sporazum važi do 31. augusta 2026.
-3
View File
@@ -117,7 +117,6 @@ Našim modelima možete pristupiti i preko sljedećih API endpointa.
| Kimi K2.7 Code | kimi-k2.7-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K3 | kimi-k3 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Ox Alpha Free | x-preview-f-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiMo-V2.5 Free | mimo-v2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Hy3 Free | hy3-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Nemotron 3 Ultra Free | nemotron-3-ultra-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -147,7 +146,6 @@ Podržavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
| Model | Input | Output | Cached Read | Cached Write |
| --------------------------------- | ------ | ------- | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| Ox Alpha Free | Free | Free | Free | - |
| MiMo-V2.5 Free | Free | Free | Free | - |
| Hy3 Free | Free | Free | Free | - |
| Nemotron 3 Ultra Free | Free | Free | Free | - |
@@ -238,7 +236,6 @@ Besplatni modeli:
- Nemotron 3 Ultra Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- Nemotron 3.5 Lightning Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- Big Pickle je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
- Ox Alpha Free je stealth model koji je besplatan na OpenCode ograničeno vrijeme. Pružalac usluge slijedi politiku nultog zadržavanja i ne koristi vaše podatke za treniranje modela.
- Muse Spark 1.2 Contributor Free je dostupan na OpenCode ograničeno vrijeme. Tim koristi ovo vrijeme da prikupi povratne informacije i poboljša model.
<a href={email}>Kontaktirajte nas</a> ako imate bilo kakvih pitanja.

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