Compare commits

..
Author SHA1 Message Date
Adam c2eacd72af fix(console): secure server action redirects (#45374) 2026-08-26 15:03:16 -05:00
Adam c5ef753d28 fix(stats): align retention columns 2026-08-26 11:14:49 -05:00
Adam 530535c6ea fix(stats): reduce retention query scan 2026-08-26 10:38:12 -05:00
Adam 023620b57e chore: add sst unlock workflow 2026-08-26 09:39:34 -05:00
Adam 902e67eba9 feat(stats): add weekly retention 2026-08-26 09:02:23 -05:00
Jack 830aaf2059 docs(go): add GLM-5.3-Flash (#45269) 2026-08-26 21:49:06 +08:00
Adam a0f36c9df7 feat(stats): add retention metrics 2026-08-26 07:39:44 -05:00
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
111 changed files with 1447 additions and 2511 deletions
+52
View File
@@ -0,0 +1,52 @@
name: unlock
on:
workflow_dispatch:
inputs:
stage:
description: SST stage to unlock
required: true
type: choice
options:
- dev
- production
concurrency: deploy-${{ inputs.stage }}
permissions:
contents: read
id-token: write
jobs:
unlock:
runs-on: ubuntu-latest
environment: ${{ inputs.stage }}
steps:
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
- uses: ./.github/actions/setup-bun
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
- uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
with:
role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_ARN }}
role-session-name: opencode-${{ github.run_id }}
aws-region: us-east-1
- run: bun sst unlock --stage=${{ inputs.stage }}
env:
GITHUB_TOKEN: ${{ github.token }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
PLANETSCALE_SERVICE_TOKEN_NAME: ${{ secrets.PLANETSCALE_SERVICE_TOKEN_NAME }}
PLANETSCALE_SERVICE_TOKEN: ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}
STRIPE_SECRET_KEY: ${{ inputs.stage == 'production' && secrets.STRIPE_SECRET_KEY_PROD || secrets.STRIPE_SECRET_KEY_DEV }}
HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.WEB_SENTRY_PROJECT }}
SENTRY_RELEASE: unlock@${{ github.sha }}
VITE_SENTRY_DSN: ${{ vars.WEB_SENTRY_DSN }}
VITE_SENTRY_RELEASE: unlock@${{ github.sha }}
+1 -23
View File
@@ -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",
@@ -858,25 +859,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",
},
"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:",
},
@@ -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
@@ -252,7 +252,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "الاستثناءات التالية",
"go.title": "OpenCode Go | نماذج برمجة منخفضة التكلفة للجميع",
"go.banner.text": "Ox Alpha Free متاح على Go لفترة محدودة",
"go.banner.text": "يحصل GLM-5.3-Flash على حدود استخدام مضاعفة لفترة محدودة",
"go.meta.description": "يبلغ سعر Go ‏$10/شهر، مع حدود استخدام سخية ووصول موثوق إلى نماذج البرمجة الرائدة.",
"go.hero.title": "نماذج برمجة منخفضة التكلفة للجميع",
"go.hero.body":
+1 -1
View File
@@ -256,7 +256,7 @@ 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.banner.text": "GLM-5.3-Flash tem limites de uso 2x maiores 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",
+1 -1
View File
@@ -254,7 +254,7 @@ 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.banner.text": "GLM-5.3-Flash får fordoblet brugsgrænse 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",
+1 -1
View File
@@ -256,7 +256,7 @@ 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.banner.text": "GLM-5.3-Flash erhält für begrenzte Zeit 2x Nutzungslimits",
"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",
+1 -1
View File
@@ -253,7 +253,7 @@ 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.banner.text": "GLM-5.3-Flash gets 2× usage limits 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":
+1 -1
View File
@@ -257,7 +257,7 @@ 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.banner.text": "GLM-5.3-Flash tiene límites de uso 2x mayores 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",
+1 -1
View File
@@ -258,7 +258,7 @@ 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.banner.text": "GLM-5.3-Flash bénéficie de limites dutilisation 2x supérieures 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",
+1 -1
View File
@@ -254,7 +254,7 @@ 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.banner.text": "GLM-5.3-Flash offre limiti di utilizzo 2x superiori 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",
+1 -1
View File
@@ -253,7 +253,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下の例外",
"go.title": "OpenCode Go | すべての人のための低価格なコーディングモデル",
"go.banner.text": "Ox Alpha Freeは期間限定でGoで利用できます",
"go.banner.text": "GLM-5.3-Flashの利用上限が期間限定で2倍に",
"go.meta.description":
"Goは月額$10で、主要なコーディングモデルへのゆとりある利用上限と安定したアクセスを提供します。",
"go.hero.title": "すべての人のための低価格なコーディングモデル",
+1 -1
View File
@@ -250,7 +250,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "다음 예외",
"go.title": "OpenCode Go | 모두를 위한 저비용 코딩 모델",
"go.banner.text": "Ox Alpha Free가 한정된 기간 동안 Go에서 제공됩니다",
"go.banner.text": "GLM-5.3-Flash 사용 한도가 한시적으로 2배 확대됩니다",
"go.meta.description": "Go는 월 $10이며, 넉넉한 사용 한도와 주요 코딩 모델에 대한 안정적인 액세스를 제공합니다.",
"go.hero.title": "모두를 위한 저비용 코딩 모델",
"go.hero.body":
+1 -1
View File
@@ -254,7 +254,7 @@ 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.banner.text": "GLM-5.3-Flash får 2x bruksgrense 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",
+1 -1
View File
@@ -255,7 +255,7 @@ 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.banner.text": "GLM-5.3-Flash oferuje 2x wyższe limity użycia 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",
+1 -1
View File
@@ -258,7 +258,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "следующими исключениями",
"go.title": "OpenCode Go | Недорогие модели для кодинга для всех",
"go.banner.text": "Ox Alpha Free доступна в Go в течение ограниченного времени",
"go.banner.text": "GLM-5.3-Flash получает 2x лимиты использования на ограниченное время",
"go.meta.description":
"Go стоит $10/месяц и предлагает щедрые лимиты использования и надежный доступ к ведущим моделям для кодинга.",
"go.hero.title": "Недорогие модели для кодинга для всех",
+1 -1
View File
@@ -253,7 +253,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "ข้อยกเว้นดังนี้",
"go.title": "OpenCode Go | โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
"go.banner.text": "Ox Alpha Free พร้อมใช้งานบน Go ในช่วงเวลาจำกัด",
"go.banner.text": "GLM-5.3-Flash เพิ่มโควตาการใช้งานเป็น 2 เท่าในช่วงเวลาจำกัด",
"go.meta.description":
"Go มีราคา $10/เดือน พร้อมขีดจำกัดการใช้งานที่เอื้อเฟื้อและการเข้าถึงโมเดลเขียนโค้ดชั้นนำอย่างเชื่อถือได้",
"go.hero.title": "โมเดลเขียนโค้ดราคาประหยัดสำหรับทุกคน",
+1 -1
View File
@@ -256,7 +256,7 @@ 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.banner.text": "GLM-5.3-Flash sınırlı bir süre için 2x kullanım limiti sunuyor",
"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",
+1 -1
View File
@@ -255,7 +255,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "такими винятками",
"go.title": "OpenCode Go | Недорогі моделі кодування для всіх",
"go.banner.text": "Ox Alpha Free доступна в Go протягом обмеженого часу",
"go.banner.text": "GLM-5.3-Flash отримує 2x ліміти використання протягом обмеженого часу",
"go.meta.description":
"Go коштує $10/місяць, зі щедрими лімітами використання та надійним доступом до провідних моделей для кодування.",
"go.hero.title": "Недорогі моделі кодування для всіх",
+1 -1
View File
@@ -244,7 +244,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下例外情况除外",
"go.title": "OpenCode Go | 人人可用的低成本编程模型",
"go.banner.text": "Ox Alpha Free 限时加入 Go",
"go.banner.text": "GLM-5.3-Flash 限时享受 2 倍使用额度",
"go.meta.description": "Go 每月 $10,提供充裕的使用限额,并可可靠访问领先的编程模型。",
"go.hero.title": "人人可用的低成本编程模型",
"go.hero.body":
+1 -1
View File
@@ -244,7 +244,7 @@ export const dict = {
"zen.privacy.exceptionsLink": "以下例外情況",
"go.title": "OpenCode Go | 低成本全民編碼模型",
"go.banner.text": "Ox Alpha Free 限時加入 Go",
"go.banner.text": "GLM-5.3-Flash 限時享有 2 倍使用額度",
"go.meta.description": "Go 每月 $10,提供充裕的使用限額,並可穩定存取領先的編碼模型。",
"go.hero.title": "低成本全民編碼模型",
"go.hero.body":
@@ -0,0 +1,11 @@
export function sanitizeServerActionRequest(request: Request) {
const requestUrl = new URL(request.url)
if (requestUrl.pathname !== "/_server") return request
const referer = request.headers.get("referer")
if (referer && URL.canParse(referer) && new URL(referer).origin === requestUrl.origin) return request
const sanitized = new Request(request)
sanitized.headers.set("referer", requestUrl.origin)
return sanitized
}
+3
View File
@@ -1,9 +1,12 @@
import { createMiddleware } from "@solidjs/start/middleware"
import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language"
import { normalizeReferralCode, referralCookie } from "~/lib/referral-invite"
import { sanitizeServerActionRequest } from "~/lib/server-action"
export default createMiddleware({
onRequest(event) {
event.request = sanitizeServerActionRequest(event.request)
const url = new URL(event.request.url)
const locale = fromPathname(url.pathname)
if (locale) {
+7 -7
View File
@@ -23,8 +23,9 @@ 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-Flash", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "GLM-5.3", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "GLM-5.2", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
{ name: "GLM-5.1", training: "go.faq.a5.notUsed", retention: "go.faq.a5.retention0" },
@@ -70,18 +71,17 @@ 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: "glm-5.2", name: "GLM-5.2", req: 880, d: "100ms" },
{ id: "grok-4.6", name: "Grok 4.6", req: 169, d: "75ms" },
{ id: "gpt-5.6-luna", name: "GPT 5.6 Luna", req: 2050, d: "290ms" },
{ id: "glm-5.3-flash", name: "GLM-5.3-Flash", req: 3160, baseReq: 1580, bonus: "2x usage", d: "100ms" },
{ id: "minimax-m3", name: "MiniMax M3", req: 3200, d: "210ms" },
{ id: "qwen3.7-plus", name: "Qwen3.7 Plus", req: 4300, d: "300ms" },
{ id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", req: 7600, d: "330ms" },
{ id: "longcat-2.0", name: "LongCat-2.0", req: 11400, d: "335ms" },
{ id: "mimo-v2.5", name: "MiMo-V2.5", req: 30100, d: "340ms" },
{ id: "hy3", name: "Hy3", req: 34400, baseReq: 4300, d: "320ms" },
{ id: "hy3", name: "Hy3", req: 34400, baseReq: 4300, bonus: "8x usage", 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
@@ -220,7 +220,7 @@ function LimitsGraph(props: { href: string }) {
</span>
)}
{"infinite" in m && <span data-limited>({i18n.t("go.graph.limitedTime")})</span>}
{m.baseReq && <span data-bonus>8x usage</span>}
{"bonus" in m && <span data-bonus>{m.bonus}</span>}
</span>
)}
</For>
@@ -511,7 +511,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>
@@ -640,8 +640,9 @@ 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-Flash</li>
<li>GLM-5.3</li>
<li>GLM-5.2</li>
<li>GLM-5.1</li>
@@ -662,7 +663,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">
@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test"
import { sanitizeServerActionRequest } from "../src/lib/server-action"
describe("server action referer", () => {
test("preserves same-origin return locations", () => {
const request = new Request("https://dev.opencode.ai/_server?id=action", {
headers: { referer: "https://dev.opencode.ai/auth?next=%2Fconsole" },
})
expect(sanitizeServerActionRequest(request)).toBe(request)
})
test("replaces unsafe return locations with the request origin", () => {
const referers = ["https://evil.example/phishing-login", "not a url", undefined]
expect(
referers.map((referer) =>
sanitizeServerActionRequest(
new Request("https://dev.opencode.ai/_server?id=action", {
headers: referer === undefined ? undefined : { referer },
}),
).headers.get("referer"),
),
).toEqual(["https://dev.opencode.ai", "https://dev.opencode.ai", "https://dev.opencode.ai"])
})
test("does not change other routes", () => {
const request = new Request("https://dev.opencode.ai/auth", {
headers: { referer: "https://evil.example/phishing-login" },
})
expect(sanitizeServerActionRequest(request)).toBe(request)
})
})
+1
View File
@@ -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"]
}
}
+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")
+6 -26
View File
@@ -33,7 +33,6 @@ import { ConfigParse } from "./parse"
import { ConfigPaths } from "./paths"
import { ConfigPlugin } from "./plugin"
import { ConfigVariable } from "./variable"
import { ConfigV2Compat } from "./v2-compat"
import { Npm } from "@opencode-ai/core/npm"
import { withTransientReadRetry } from "@/util/effect-http-client"
@@ -185,19 +184,6 @@ const layer = Layer.effect(
const readConfigFile = (filepath: string) => fs.readFileStringSafe(filepath).pipe(Effect.orDie)
const decodeConfig = Effect.fnUntraced(function* (input: unknown, source: string) {
const result = ConfigV2Compat.lower(normalizeLoadedConfig(input), source)
yield* Effect.forEach(result.diagnostics, (diagnostic) =>
Effect.logWarning("configuration compatibility diagnostic", {
source,
path: diagnostic.path,
kind: diagnostic.kind,
action: diagnostic.message,
}),
)
return ConfigParse.schema(ConfigV1.Info, result.value, source)
})
const fetchRemoteJson = Effect.fnUntraced(function* <S extends Schema.Top>(
url: string,
headers: Record<string, string> | undefined,
@@ -238,7 +224,7 @@ const layer = Layer.effect(
),
)
const parsed = ConfigParse.jsonc(expanded, source)
const data = yield* decodeConfig(parsed, source)
const data = ConfigParse.schema(ConfigV1.Info, normalizeLoadedConfig(parsed), source)
if (!("path" in options)) return data
yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
@@ -639,13 +625,8 @@ const layer = Layer.effect(
const dir = yield* InstanceState.directory
const file = path.join(dir, "config.json")
const existing = yield* loadFile(file)
const text = yield* readConfigFile(file)
const original = text ? ConfigParse.jsonc(text, file) : writable(existing)
yield* fs
.writeFileString(
file,
JSON.stringify(mergeDeep(isRecord(original) ? original : writable(existing), writable(config)), null, 2),
)
.writeFileString(file, JSON.stringify(mergeDeep(writable(existing), writable(config)), null, 2))
.pipe(Effect.orDie)
})
@@ -661,16 +642,15 @@ const layer = Layer.effect(
let next: Info
let changed: boolean
if (!file.endsWith(".jsonc")) {
const existing = ConfigParse.jsonc(before, file)
ConfigParse.schema(ConfigV1.Info, ConfigV2Compat.lower(normalizeLoadedConfig(existing), file).value, file)
const merged = mergeDeep(isRecord(existing) ? existing : {}, patch)
const existing = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(before, file), file)
const merged = mergeDeep(writable(existing), patch)
const serialized = JSON.stringify(merged, null, 2)
next = yield* decodeConfig(merged, file)
changed = serialized !== before
if (changed) yield* fs.writeFileString(file, serialized).pipe(Effect.orDie)
next = merged
} else {
const updated = patchJsonc(before, patch)
next = yield* decodeConfig(ConfigParse.jsonc(updated, file), file)
next = ConfigParse.schema(ConfigV1.Info, ConfigParse.jsonc(updated, file), file)
changed = updated !== before
if (changed) yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
}
-549
View File
@@ -1,549 +0,0 @@
export * as ConfigV2Compat from "./v2-compat"
import { isDeepStrictEqual } from "node:util"
import { Option, Schema } from "effect"
import { Permission } from "@opencode-ai/schema/permission"
import { NonNegativeInt, PositiveInt } from "@opencode-ai/core/schema"
import { ConfigAttachmentV1 } from "@opencode-ai/core/v1/config/attachment"
import { ConfigLSPV1 } from "@opencode-ai/core/v1/config/lsp"
import { ConfigPermissionV1 } from "@opencode-ai/core/v1/config/permission"
import { InvalidError } from "@opencode-ai/core/v1/config/error"
export interface Diagnostic {
readonly kind: "invalid" | "unsupported" | "conflict"
readonly path: readonly string[]
readonly message: string
}
export interface Result {
readonly value: unknown
readonly diagnostics: readonly Diagnostic[]
}
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
const Record = Schema.Record(Schema.String, Schema.Unknown)
const Timeout = Schema.Struct({
startup: Schema.optional(PositiveInt),
catalog: Schema.optional(PositiveInt),
execution: Schema.optional(PositiveInt),
})
const OAuth = Schema.Struct({
client_id: Schema.optional(Schema.String),
client_secret: Schema.optional(Schema.String),
scope: Schema.optional(Schema.String),
callback_port: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 65535 }))),
redirect_uri: Schema.optional(Schema.String),
})
const Server = Schema.Union([
Schema.Struct({
type: Schema.Literal("local"),
command: Schema.Array(Schema.String),
cwd: Schema.optional(Schema.String),
environment: Schema.optional(Schema.Record(Schema.String, Schema.String)),
disabled: Schema.optional(Schema.Boolean),
codemode: Schema.optional(Schema.Boolean),
timeout: Schema.optional(Timeout),
}),
Schema.Struct({
type: Schema.Literal("remote"),
url: Schema.String,
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
oauth: Schema.optional(Schema.Union([OAuth, Schema.Literal(false)])),
disabled: Schema.optional(Schema.Boolean),
codemode: Schema.optional(Schema.Boolean),
timeout: Schema.optional(Timeout),
}),
])
const Selection = Schema.Union([
Schema.String.check(Schema.isPattern(/^[^/#]+\/[^#]+(?:#[^#]+)?$/)),
Schema.Struct({
providerID: Schema.String.check(Schema.isPattern(/^[^/#]+$/)),
model: Schema.String.check(Schema.isPattern(/^[^#]+$/)),
variant: Schema.optional(Schema.String.check(Schema.isPattern(/^[^#]+$/))),
}),
])
const Agent = Schema.Struct({
model: Schema.optional(Selection),
request: Schema.optional(
Schema.Struct({
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
body: Schema.optional(Schema.Record(Schema.String, Schema.Json)),
}),
),
system: Schema.optional(Schema.String),
description: Schema.optional(Schema.String),
mode: Schema.optional(Schema.Literals(["subagent", "primary", "all"])),
hidden: Schema.optional(Schema.Boolean),
color: Schema.optional(Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/))),
steps: Schema.optional(PositiveInt),
disabled: Schema.optional(Schema.Boolean),
permissions: Schema.optional(Permission.Ruleset),
})
const Command = Schema.Struct({
template: Schema.String,
description: Schema.optional(Schema.String),
agent: Schema.optional(Schema.String),
model: Schema.optional(Selection),
subtask: Schema.optional(Schema.Boolean),
})
const decodeRecord = Schema.decodeUnknownOption(Record, decodeOptions)
const decodeLspEntry = Schema.decodeUnknownOption(ConfigLSPV1.Entry, decodeOptions)
const decodePermissions = Schema.decodeUnknownOption(Permission.Ruleset, decodeOptions)
const decodeLegacyPermissions = Schema.decodeUnknownOption(ConfigPermissionV1.Info, decodeOptions)
const builtinServers = new Set<string>(ConfigLSPV1.builtinServerIds)
const scalarPermissions = new Set(["todowrite", "question", "webfetch", "websearch", "doom_loop"])
export function lower(input: unknown, source = "configuration"): Result {
const parsed = decodeRecord(input)
if (Option.isNone(parsed)) return { value: input, diagnostics: [] }
const result: Record<string, unknown> = { ...parsed.value }
const diagnostics: Diagnostic[] = []
for (const key of ["plugins", "providers", "websearch", "warming"])
if (Object.hasOwn(parsed.value, key)) unsupported([key], diagnostics)
normalizeSettings(parsed.value, result, diagnostics)
normalizeModel(parsed.value, result, diagnostics)
normalizeSkills(parsed.value, result, diagnostics)
normalizeCompaction(parsed.value, result, diagnostics)
normalizeExperimental(parsed.value, result, diagnostics)
if (Object.hasOwn(parsed.value, "permissions"))
result.permission = lowerPermissions(
parsed.value.permissions,
result.permission,
source,
["permissions"],
diagnostics,
)
normalizeAgents(parsed.value, result, source, diagnostics)
normalizeCommands(parsed.value, result, diagnostics)
normalizeMcp(parsed.value, result, diagnostics)
normalizeLsp(parsed.value, result, diagnostics)
return { value: result, diagnostics }
}
function normalizeSettings(input: Record<string, unknown>, result: Record<string, unknown>, diagnostics: Diagnostic[]) {
if (Object.hasOwn(input, "snapshots")) {
const value = decodeValue(Schema.Boolean, input.snapshots, ["snapshots"], diagnostics)
if (value !== undefined) preferLegacy(result, "snapshot", value, ["snapshots"], diagnostics)
}
if (Object.hasOwn(input, "media")) {
const value = decodeValue(ConfigAttachmentV1.Info, input.media, ["media"], diagnostics)
if (value !== undefined) preferLegacy(result, "attachment", value, ["media"], diagnostics)
}
}
function normalizeModel(input: Record<string, unknown>, result: Record<string, unknown>, diagnostics: Diagnostic[]) {
if (!Object.hasOwn(input, "model")) return
const selection = Schema.decodeUnknownOption(Selection, decodeOptions)(input.model)
if (Option.isNone(selection)) return
const value = lowerSelection(selection.value)
result.model = value.model
if (value.variant !== undefined) unsupported(["model", "variant"], diagnostics)
}
function normalizeSkills(input: Record<string, unknown>, result: Record<string, unknown>, diagnostics: Diagnostic[]) {
if (!Array.isArray(input.skills)) return
const skills = decodeValue(Schema.Array(Schema.String), input.skills, ["skills"], diagnostics)
if (skills === undefined) return
result.skills = {
paths: skills.filter((value) => !/^https?:\/\//i.test(value)),
urls: skills.filter((value) => /^https?:\/\//i.test(value)),
}
}
function normalizeCompaction(
input: Record<string, unknown>,
result: Record<string, unknown>,
diagnostics: Diagnostic[],
) {
const compaction = decodeRecord(input.compaction)
if (Option.isNone(compaction)) return
const value = { ...compaction.value }
if (Object.hasOwn(value, "keep")) {
const keep = decodeValue(Record, value.keep, ["compaction", "keep"], diagnostics)
if (keep !== undefined && Object.hasOwn(keep, "tokens")) {
const tokens = decodeValue(NonNegativeInt, keep.tokens, ["compaction", "keep", "tokens"], diagnostics)
if (tokens !== undefined)
preferLegacy(value, "preserve_recent_tokens", tokens, ["compaction", "keep", "tokens"], diagnostics)
}
}
if (Object.hasOwn(value, "buffer")) {
const buffer = decodeValue(NonNegativeInt, value.buffer, ["compaction", "buffer"], diagnostics)
if (buffer !== undefined) preferLegacy(value, "reserved", buffer, ["compaction", "buffer"], diagnostics)
}
result.compaction = value
}
function normalizeExperimental(
input: Record<string, unknown>,
result: Record<string, unknown>,
diagnostics: Diagnostic[],
) {
const experimental = decodeRecord(input.experimental)
if (Option.isNone(experimental)) return
if (Object.hasOwn(experimental.value, "portable_shell_scanner"))
unsupported(["experimental", "portable_shell_scanner"], diagnostics)
if (!Object.hasOwn(experimental.value, "subagent_depth")) return
const depth = decodeValue(
NonNegativeInt,
experimental.value.subagent_depth,
["experimental", "subagent_depth"],
diagnostics,
)
if (depth !== undefined)
preferLegacy(result, "subagent_depth", depth, ["experimental", "subagent_depth"], diagnostics)
}
function normalizeAgents(
input: Record<string, unknown>,
result: Record<string, unknown>,
source: string,
diagnostics: Diagnostic[],
) {
if (!Object.hasOwn(input, "agents")) return
const agents = decodeValue(Record, input.agents, ["agents"], diagnostics)
if (agents === undefined) return
const legacy = decodeRecord(result.agent)
const merged: Record<string, unknown> = Option.isSome(legacy) ? { ...legacy.value } : {}
for (const [name, value] of Object.entries(agents)) {
const path = ["agents", name]
// A shadowed native agent must not introduce new fatal permission failures.
if (Object.hasOwn(merged, name)) {
if (!isDeepStrictEqual(merged[name], value)) conflict(path, diagnostics)
continue
}
const record = decodeRecord(value)
const permissions =
Option.isSome(record) && Object.hasOwn(record.value, "permissions")
? lowerPermissions(record.value.permissions, undefined, source, [...path, "permissions"], diagnostics)
: undefined
const parsed = decodeValue(Agent, value, path, diagnostics)
if (parsed === undefined) continue
if (parsed.request?.headers !== undefined) unsupported([...path, "request", "headers"], diagnostics)
const agent = lowerAgent(parsed)
if (permissions !== undefined) agent.permission = permissions
setOwn(merged, name, agent)
}
if (Object.hasOwn(result, "agent") && Option.isNone(legacy)) return
if (Object.keys(merged).length > 0 || Option.isSome(legacy)) result.agent = merged
}
function normalizeCommands(input: Record<string, unknown>, result: Record<string, unknown>, diagnostics: Diagnostic[]) {
if (!Object.hasOwn(input, "commands")) return
const commands = decodeValue(Record, input.commands, ["commands"], diagnostics)
if (commands === undefined) return
const legacy = decodeRecord(result.command)
if (Object.hasOwn(result, "command") && Option.isNone(legacy)) return
const merged: Record<string, unknown> = Option.isSome(legacy) ? { ...legacy.value } : {}
for (const [name, value] of Object.entries(commands)) {
const path = ["commands", name]
const parsed = decodeValue(Command, value, path, diagnostics)
if (parsed === undefined) continue
preferLegacy(merged, name, lowerCommand(parsed), path, diagnostics)
}
if (Object.keys(merged).length > 0 || Option.isSome(legacy)) result.command = merged
}
function normalizeMcp(input: Record<string, unknown>, result: Record<string, unknown>, diagnostics: Diagnostic[]) {
const mcp = decodeRecord(input.mcp)
if (Option.isNone(mcp)) return
const servers: Record<string, unknown> = {}
const nested = decodeRecord(mcp.value.servers)
const envelope = Option.isSome(nested) && !isDirectServer(nested.value)
const timeoutRecord = decodeRecord(mcp.value.timeout)
const timeout = Schema.decodeUnknownOption(Timeout, decodeOptions)(mcp.value.timeout)
const globalTimeout =
Option.isSome(timeout) &&
Option.isSome(timeoutRecord) &&
!isDirectServer(timeoutRecord.value) &&
(Object.keys(timeoutRecord.value).length === 0 ||
["startup", "catalog", "execution"].some((key) => Object.hasOwn(timeoutRecord.value, key)))
for (const [name, value] of Object.entries(mcp.value)) {
if (name === "servers" && envelope) continue
if (name === "timeout" && globalTimeout) continue
const path = ["mcp", name]
const record = decodeRecord(value)
const oauth = Option.isSome(record) ? decodeRecord(record.value.oauth) : Option.none()
const native =
Option.isSome(record) &&
(Object.hasOwn(record.value, "disabled") ||
Object.hasOwn(record.value, "codemode") ||
typeof record.value.timeout === "object" ||
(Option.isSome(oauth) &&
["client_id", "client_secret", "callback_port", "redirect_uri"].some((key) =>
Object.hasOwn(oauth.value, key),
)))
// Keep invalid flat entries for the final V1 decoder rather than sanitizing them.
setOwn(servers, name, native ? (normalizeServer(value, path, diagnostics) ?? value) : value)
}
if (envelope && Option.isSome(nested)) {
for (const [name, value] of Object.entries(nested.value)) {
const path = ["mcp", "servers", name]
if (Object.hasOwn(servers, name)) {
if (!isDeepStrictEqual(servers[name], value)) conflict(path, diagnostics)
continue
}
const record = decodeRecord(value)
if (
Option.isSome(record) &&
typeof record.value.enabled === "boolean" &&
!Object.hasOwn(record.value, "disabled")
) {
setOwn(servers, name, value)
continue
}
const server = normalizeServer(value, path, diagnostics)
if (server !== undefined) setOwn(servers, name, server)
}
}
result.mcp = servers
if (!globalTimeout || Option.isNone(timeout)) return
const value = lowerTimeout(timeout.value)
if (value === undefined) {
if (Object.keys(timeout.value).length) unsupported(["mcp", "timeout"], diagnostics)
return
}
const existing = decodeRecord(result.experimental)
if (Object.hasOwn(result, "experimental") && Option.isNone(existing)) return
const experimental = Option.isSome(existing) ? { ...existing.value } : {}
preferLegacy(experimental, "mcp_timeout", value, ["mcp", "timeout"], diagnostics)
result.experimental = experimental
}
function isDirectServer(value: Record<string, unknown>) {
// Object-valued entries can be servers literally named "type" or "enabled".
return ["type", "enabled"].some(
(key) =>
Object.hasOwn(value, key) && (value[key] === null || typeof value[key] !== "object" || Array.isArray(value[key])),
)
}
function normalizeServer(input: unknown, path: string[], diagnostics: Diagnostic[]) {
const server = decodeValue(Server, input, path, diagnostics)
if (server === undefined) return
if (server.codemode !== undefined) unsupported([...path, "codemode"], diagnostics)
if (server.timeout && lowerTimeout(server.timeout) === undefined && Object.keys(server.timeout).length)
unsupported([...path, "timeout"], diagnostics)
const raw = decodeRecord(input)
if (Option.isNone(raw) || !Object.hasOwn(raw.value, "enabled")) return lowerServer(server)
if (server.disabled !== undefined && raw.value.enabled === server.disabled)
conflict([...path, "disabled"], diagnostics)
return { ...lowerServer(server), enabled: raw.value.enabled }
}
function normalizeLsp(input: Record<string, unknown>, result: Record<string, unknown>, diagnostics: Diagnostic[]) {
const lsp = decodeRecord(input.lsp)
if (Option.isNone(lsp)) return
result.lsp = Object.fromEntries(
Object.entries(lsp.value).filter(([name, value]) => {
if (builtinServers.has(name)) return true
const entry = decodeLspEntry(value)
if (Option.isNone(entry)) return true
if (entry.value.disabled === true) return true
if ("extensions" in entry.value && entry.value.extensions !== undefined) return true
unsupported(["lsp", name], diagnostics)
return false
}),
)
}
function lowerPermissions(input: unknown, legacy: unknown, source: string, path: string[], diagnostics: Diagnostic[]) {
const native = decodePermissions(input)
if (Option.isNone(native)) return invalidPermissions(source, path, "Invalid native permission rules")
const previous = legacy === undefined ? Option.none() : decodeLegacyPermissions(legacy)
if (legacy !== undefined && Option.isNone(previous))
return invalidPermissions(source, path, "Existing legacy permissions are invalid")
const inherited = Option.isSome(previous)
? Object.entries(previous.value).flatMap(([action, value]) =>
typeof value === "string"
? [{ action: nativeAction(action), resource: "*", effect: value }]
: Object.entries(value).map(([resource, effect]) => ({ action: nativeAction(action), resource, effect })),
)
: []
const normalized = [...native.value, ...inherited].map((rule, index) => {
if (["bash", "task", "write", "patch"].includes(rule.action))
return invalidPermissions(
source,
[...path, String(index), "action"],
"Permission action cannot be lowered safely",
)
if (rule.action !== "*" && /[*?]/.test(rule.action))
return invalidPermissions(
source,
[...path, String(index), "action"],
"Wildcard permission actions cannot be lowered safely",
)
if (/^\d+$/.test(rule.action) || /^\d+$/.test(rule.resource))
return invalidPermissions(source, [...path, String(index)], "Numeric permission keys cannot preserve rule order")
const action = rule.action === "shell" ? "bash" : rule.action === "subagent" ? "task" : rule.action
if (scalarPermissions.has(action) && rule.resource !== "*")
return invalidPermissions(
source,
[...path, String(index), "resource"],
"Permission action does not support resource-specific rules in V1",
)
if (!["read", "edit", "external_directory"].includes(action) && /^(?:~|\$HOME)/.test(rule.resource))
return invalidPermissions(
source,
[...path, String(index), "resource"],
"Permission resource has incompatible home-directory expansion",
)
return { action, resource: rule.resource, effect: rule.effect }
})
const last = new Map(normalized.map((rule, index) => [JSON.stringify([rule.action, rule.resource]), index]))
const remaining = normalized.filter((rule, index) => last.get(JSON.stringify([rule.action, rule.resource])) === index)
const groups = new Map<string, typeof remaining>()
for (const [index, rule] of remaining.entries()) {
const group = groups.get(rule.action)
if (group !== undefined && remaining[index - 1]?.action !== rule.action)
return invalidPermissions(source, path, "Permission action order cannot be represented safely in V1")
if (group !== undefined) {
group.push(rule)
continue
}
groups.set(rule.action, [rule])
}
if (
native.value.some((rule) =>
inherited.some(
(previous) =>
previous.action === rule.action && previous.resource === rule.resource && previous.effect !== rule.effect,
),
)
)
conflict(path, diagnostics)
return Object.fromEntries(
Array.from(groups, ([action, rules]) => [
action,
rules.length === 1 && rules[0]?.resource === "*"
? rules[0].effect
: Object.fromEntries(rules.map((rule) => [rule.resource, rule.effect])),
]),
)
}
function nativeAction(action: string) {
if (action === "bash") return "shell"
if (action === "task") return "subagent"
if (action === "write" || action === "patch") return "edit"
return action
}
function invalidPermissions(source: string, path: string[], message: string): never {
throw new InvalidError({ path: source, message, issues: [{ path, message }] })
}
function lowerSelection(input: Schema.Schema.Type<typeof Selection>) {
if (typeof input !== "string") {
return {
model: `${input.providerID}/${input.model}`,
...(input.variant !== undefined ? { variant: input.variant } : {}),
}
}
const index = input.indexOf("#")
if (index === -1) return { model: input }
return { model: input.slice(0, index), variant: input.slice(index + 1) }
}
function lowerTimeout(input: Schema.Schema.Type<typeof Timeout>) {
if (input.startup !== undefined) return undefined
if (input.catalog === undefined || input.execution === undefined) return undefined
if (input.catalog !== input.execution) return undefined
return input.catalog
}
function lowerServer(input: Schema.Schema.Type<typeof Server>) {
const result: Record<string, unknown> = {
...input,
enabled: input.disabled !== true,
}
delete result.disabled
delete result.codemode
delete result.timeout
if (input.timeout) {
const timeout = lowerTimeout(input.timeout)
if (timeout !== undefined) result.timeout = timeout
}
if (input.type === "remote" && input.oauth && typeof input.oauth === "object") {
const oauth: Record<string, unknown> = {}
if (input.oauth.client_id !== undefined) oauth.clientId = input.oauth.client_id
if (input.oauth.client_secret !== undefined) oauth.clientSecret = input.oauth.client_secret
if (input.oauth.scope !== undefined) oauth.scope = input.oauth.scope
if (input.oauth.callback_port !== undefined) oauth.callbackPort = input.oauth.callback_port
if (input.oauth.redirect_uri !== undefined) oauth.redirectUri = input.oauth.redirect_uri
result.oauth = oauth
}
return result
}
function lowerAgent(input: Schema.Schema.Type<typeof Agent>) {
const result: Record<string, unknown> = {}
for (const key of ["description", "mode", "hidden", "color", "steps"] as const) {
if (input[key] !== undefined) result[key] = input[key]
}
if (input.system !== undefined) result.prompt = input.system
if (input.disabled !== undefined) result.disable = input.disabled
if (input.model !== undefined) Object.assign(result, lowerSelection(input.model))
if (input.request?.body !== undefined) result.options = input.request.body
return result
}
function lowerCommand(input: Schema.Schema.Type<typeof Command>) {
return { ...input, ...(input.model !== undefined ? lowerSelection(input.model) : {}) }
}
function decodeValue<S extends Schema.Codec<unknown, unknown, never, never>>(
schema: S,
value: unknown,
path: string[],
diagnostics: Diagnostic[],
) {
const decoded = Schema.decodeUnknownOption(schema, decodeOptions)(value)
if (Option.isSome(decoded)) return decoded.value
diagnostics.push({ kind: "invalid", path, message: "Native setting could not be lowered because it is malformed" })
return undefined
}
function preferLegacy(
target: Record<string, unknown>,
key: string,
value: unknown,
path: string[],
diagnostics: Diagnostic[],
) {
if (Object.hasOwn(target, key)) {
if (!isDeepStrictEqual(target[key], value)) conflict(path, diagnostics)
return
}
setOwn(target, key, value)
}
function setOwn(target: Record<string, unknown>, key: string, value: unknown) {
Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true })
}
function unsupported(path: string[], diagnostics: Diagnostic[]) {
diagnostics.push({ kind: "unsupported", path, message: "Omitted native setting that cannot be represented in V1" })
}
function conflict(path: string[], diagnostics: Diagnostic[]) {
diagnostics.push({ kind: "conflict", path, message: "Retained legacy value over native value" })
}
+1 -152
View File
@@ -2,7 +2,7 @@ import { test, expect, describe, afterEach, beforeEach, spyOn } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { Cause, Effect, Exit, Layer, Logger, Option } from "effect"
import { Cause, Effect, Exit, Layer, Option } from "effect"
import { NamedError } from "@opencode-ai/core/util/error"
import { FetchHttpClient, HttpClient, HttpClientResponse } from "effect/unstable/http"
import { Config } from "@/config/config"
@@ -397,157 +397,6 @@ it.effect("updates global config and omits empty shell key in jsonc", () =>
),
)
it.effect("updates global JSON without removing native V2 configuration", () =>
withGlobalConfig(
{
config: {
mcp: {
servers: {
modern: { type: "local", command: ["modern-mcp"], disabled: true },
},
},
providers: { example: { settings: { apiKey: "keep-me" } } },
},
},
({ dir }) =>
Effect.gen(function* () {
const messages: unknown[] = []
const updated = yield* Config.use.updateGlobal({ username: "updated" }).pipe(
Effect.provide(
Logger.layer([
Logger.make<unknown, void>((options) => {
messages.push(options.message)
}),
]),
),
)
const written = yield* FSUtil.use.readJson(path.join(dir, "opencode.json"))
expect(written).toMatchObject({
username: "updated",
mcp: { servers: { modern: { type: "local", command: ["modern-mcp"], disabled: true } } },
providers: { example: { settings: { apiKey: "keep-me" } } },
})
expect(updated.info.mcp?.modern).toMatchObject({ enabled: false })
expect(JSON.stringify(messages)).not.toContain("keep-me")
expect(
messages.filter((item) => Array.isArray(item) && item[0] === "configuration compatibility diagnostic"),
).toEqual([
[
"configuration compatibility diagnostic",
expect.objectContaining({
source: path.join(dir, "opencode.json"),
kind: "unsupported",
path: ["providers"],
}),
],
])
}),
),
)
it.effect("updates global JSONC without removing native V2 configuration", () =>
withGlobalConfig(
{
name: "opencode.jsonc",
config: {
mcp: {
servers: {
modern: { type: "local", command: ["modern-mcp"], disabled: true },
},
},
providers: { example: { settings: { apiKey: "keep-me" } } },
},
},
({ dir }) =>
Effect.gen(function* () {
const updated = yield* Config.use.updateGlobal({ username: "updated" })
const written = yield* FSUtil.use.readJson(path.join(dir, "opencode.jsonc"))
expect(written).toMatchObject({
username: "updated",
mcp: { servers: { modern: { type: "local", command: ["modern-mcp"], disabled: true } } },
providers: { example: { settings: { apiKey: "keep-me" } } },
})
expect(updated.info.mcp?.modern).toMatchObject({ enabled: false })
}),
),
)
it.effect("applies legacy global updates over existing native V2 settings", () =>
withGlobalConfig(
{
config: {
snapshots: true,
permissions: [{ action: "read", resource: "*", effect: "allow" }],
agents: { reviewer: { disabled: false } },
mcp: { servers: { modern: { type: "local", command: ["modern-mcp"], disabled: false } } },
},
},
() =>
Effect.gen(function* () {
const updated = yield* Config.use.updateGlobal({
snapshot: false,
permission: { read: "deny" },
agent: { reviewer: { disable: true } },
mcp: { modern: { enabled: false } },
})
expect(updated.info.snapshot).toBe(false)
expect(updated.info.permission?.read).toBe("deny")
expect(updated.info.agent?.reviewer?.disable).toBe(true)
expect(updated.info.mcp?.modern).toEqual({ enabled: false })
}),
),
)
it.instance("updates project config without removing native V2 configuration", () =>
Effect.gen(function* () {
const test = yield* TestInstance
yield* writeConfigEffect(
test.directory,
{
$schema: "https://opencode.ai/config.json",
mcp: { servers: { modern: { type: "local", command: ["modern-mcp"], disabled: true } } },
providers: { example: { settings: { apiKey: "keep-me" } } },
},
"config.json",
)
yield* Config.use.update({ username: "updated" })
expect(yield* FSUtil.use.readJson(path.join(test.directory, "config.json"))).toMatchObject({
username: "updated",
mcp: { servers: { modern: { type: "local", command: ["modern-mcp"], disabled: true } } },
providers: { example: { settings: { apiKey: "keep-me" } } },
})
}),
)
it.effect("native project MCP servers override inherited V1 disabled state", () =>
withConfigTree(
{
global: {
mcp: {
shared: { type: "local", command: ["global-mcp"], enabled: false },
},
},
project: {
mcp: {
servers: {
shared: { type: "local", command: ["project-mcp"] },
},
},
},
},
Effect.gen(function* () {
expect((yield* Config.use.get()).mcp?.shared).toMatchObject({
type: "local",
command: ["project-mcp"],
enabled: true,
})
}),
),
)
it.instance(
"loads formatter boolean config",
Effect.gen(function* () {
@@ -1,802 +0,0 @@
import { describe, expect, test } from "bun:test"
import { ConfigV1 } from "@opencode-ai/core/v1/config/config"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { httpClient } from "@opencode-ai/core/effect/app-node-platform"
import { FSUtil } from "@opencode-ai/core/fs-util"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { Npm } from "@opencode-ai/core/npm"
import { Effect, Layer, Logger } from "effect"
import { HttpClient } from "effect/unstable/http"
import path from "path"
import { Account } from "../../src/account/account"
import { Auth } from "../../src/auth"
import { Config } from "../../src/config/config"
import { ConfigParse } from "../../src/config/parse"
import { ConfigV2Compat } from "../../src/config/v2-compat"
import { Env } from "../../src/env"
import { AccountTest } from "../fake/account"
import { AuthTest } from "../fake/auth"
import { NpmTest } from "../fake/npm"
import { TestInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
const source = "test:v2-compat"
const lower = (input: unknown) => ConfigParse.schema(ConfigV1.Info, ConfigV2Compat.lower(input, source).value, source)
const it = testEffect(
LayerNode.compile(LayerNode.group([Config.node, FSUtil.node, Env.node, CrossSpawnSpawner.node]), [
[Auth.node, AuthTest.empty],
[Account.node, AccountTest.empty],
[Npm.node, NpmTest.noop],
[
httpClient,
Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => Effect.die(`unexpected http request: ${request.method} ${request.url}`)),
),
],
]),
)
describe("ConfigV2Compat.lower", () => {
test("returns structured invalid diagnostics while retaining supported siblings", () => {
const result = ConfigV2Compat.lower({
mcp: {
servers: {
broken: { type: "local", command: "not-an-array" },
working: { type: "local", command: ["working-mcp"] },
},
},
agents: { broken: { steps: "many" } },
commands: { broken: { template: 42 } },
})
expect(result.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "invalid", path: ["mcp", "servers", "broken"] }),
expect.objectContaining({ kind: "invalid", path: ["agents", "broken"] }),
expect.objectContaining({ kind: "invalid", path: ["commands", "broken"] }),
]),
)
expect(ConfigParse.schema(ConfigV1.Info, result.value, source).mcp).toEqual({
working: { type: "local", command: ["working-mcp"], enabled: true },
})
})
test("reports unsupported settings and lossy conversions without their values", () => {
const secret = "do-not-log-credentials"
const result = ConfigV2Compat.lower({
model: { providerID: "example", model: "model", variant: "high" },
plugins: [{ package: "native-plugin", options: { token: secret } }],
providers: { example: { settings: { apiKey: secret } } },
websearch: secret,
warming: true,
experimental: { portable_shell_scanner: true },
agents: { reviewer: { request: { headers: { Authorization: secret } } } },
mcp: {
servers: {
remote: {
type: "remote",
url: `https://example.com/?token=${secret}`,
oauth: { client_secret: secret },
codemode: false,
timeout: { execution: 60000 },
},
},
},
lsp: { custom: { command: ["custom-lsp"] } },
})
expect(result.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "unsupported", path: ["model", "variant"] }),
expect.objectContaining({ kind: "unsupported", path: ["plugins"] }),
expect.objectContaining({ kind: "unsupported", path: ["providers"] }),
expect.objectContaining({ kind: "unsupported", path: ["websearch"] }),
expect.objectContaining({ kind: "unsupported", path: ["warming"] }),
expect.objectContaining({ kind: "unsupported", path: ["experimental", "portable_shell_scanner"] }),
expect.objectContaining({ kind: "unsupported", path: ["agents", "reviewer", "request", "headers"] }),
expect.objectContaining({ kind: "unsupported", path: ["mcp", "servers", "remote", "codemode"] }),
expect.objectContaining({ kind: "unsupported", path: ["mcp", "servers", "remote", "timeout"] }),
expect.objectContaining({ kind: "unsupported", path: ["lsp", "custom"] }),
]),
)
expect(JSON.stringify(result.diagnostics)).not.toContain(secret)
})
test("reports conflicting forms while retaining the V1 value", () => {
const result = ConfigV2Compat.lower({
snapshot: false,
snapshots: true,
command: { review: { template: "Legacy review" } },
commands: { review: { template: "Native review" } },
mcp: {
shared: { type: "local", command: ["legacy"] },
servers: { shared: { type: "local", command: ["native"] } },
},
})
const config = ConfigParse.schema(ConfigV1.Info, result.value, source)
expect(config.snapshot).toBe(false)
expect(config.command?.review.template).toBe("Legacy review")
expect(config.mcp?.shared).toEqual({ type: "local", command: ["legacy"] })
expect(result.diagnostics.filter((item) => item.kind === "conflict")).toHaveLength(3)
})
test("does not diagnose ordinary V1 configuration or reject invalid V1 roots early", () => {
expect(ConfigV2Compat.lower({ snapshot: false, mcp: { existing: { enabled: false } } }).diagnostics).toEqual([])
expect(ConfigV2Compat.lower({ snapshot: false, snapshots: false }).diagnostics).toEqual([])
const result = ConfigV2Compat.lower(null)
expect(result.value).toBeNull()
expect(() => ConfigParse.schema(ConfigV1.Info, result.value, source)).toThrow()
expect(() => lower({ snapshot: "invalid", snapshots: true })).toThrow()
})
test("keeps malformed MCP servers named servers and timeout for V1 validation", () => {
expect(() => lower({ mcp: { servers: { type: "local", command: "invalid" } } })).toThrow()
expect(() => lower({ mcp: { timeout: { type: "remote", url: 42 } } })).toThrow()
expect(() => lower({ mcp: { servers: { type: "bogus" } } })).toThrow()
expect(() => lower({ mcp: { servers: { type: 42 } } })).toThrow()
expect(() => lower({ mcp: { servers: { enabled: "false" } } })).toThrow()
expect(lower({ mcp: { servers: { enabled: false }, timeout: { enabled: true } } }).mcp).toEqual({
servers: { enabled: false },
timeout: { enabled: true },
})
expect(lower({ mcp: { servers: { type: {}, enabled: false } } }).mcp).toEqual({
servers: { enabled: false },
})
})
test("preserves V1 enablement when flat MCP entries include V2 fields", () => {
expect(
lower({
mcp: {
disabled: { type: "local", command: ["legacy"], enabled: false, codemode: false },
enabled: { type: "local", command: ["legacy"], enabled: true, disabled: true },
},
}).mcp,
).toEqual({
disabled: { type: "local", command: ["legacy"], enabled: false },
enabled: { type: "local", command: ["legacy"], enabled: true },
})
expect(() =>
lower({ mcp: { invalid: { type: "local", command: ["legacy"], enabled: "false", codemode: false } } }),
).toThrow("ConfigInvalidError")
})
test("accepts enveloped servers named type and enabled", () => {
expect(
lower({
mcp: {
servers: {
type: { type: "local", command: ["type-mcp"] },
enabled: { type: "remote", url: "https://example.com/mcp" },
},
},
}).mcp,
).toEqual({
type: { type: "local", command: ["type-mcp"], enabled: true },
enabled: { type: "remote", url: "https://example.com/mcp", enabled: true },
})
})
test("does not repair malformed V1 containers or shadowed entries with V2 values", () => {
const cases = [
{ agent: null, agents: { reviewer: { system: "Native prompt" } } },
{ command: [], commands: { review: { template: "Native command" } } },
{ attachment: false, media: { image: { auto_resize: true } } },
{ experimental: null, mcp: { timeout: { catalog: 3000, execution: 3000 } } },
{ agent: { reviewer: 42 }, agents: { reviewer: { system: "Native prompt" } } },
{ command: { review: 42 }, commands: { review: { template: "Native command" } } },
{ mcp: { shared: 42, servers: { shared: { type: "local", command: ["native"] } } } },
]
cases.forEach((input) => expect(() => lower(input)).toThrow("ConfigInvalidError"))
})
test("keeps secrets out of invalid and conflict diagnostics", () => {
const secret = "secret-never-in-diagnostics"
const result = ConfigV2Compat.lower({
commands: { malformed: { template: { token: secret } } },
mcp: {
shared: { type: "remote", url: "https://example.com", headers: { Authorization: secret } },
servers: {
shared: { type: "remote", url: "https://example.com", headers: { Authorization: `${secret}-changed` } },
malformed: { type: "remote", url: `https://example.com?token=${secret}`, disabled: secret },
},
},
})
expect(result.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "conflict", path: ["mcp", "servers", "shared"] }),
expect.objectContaining({ kind: "invalid", path: ["commands", "malformed"] }),
expect.objectContaining({ kind: "invalid", path: ["mcp", "servers", "malformed"] }),
]),
)
expect(JSON.stringify(result.diagnostics)).not.toContain(secret)
})
test("merges flat V1 and nested V2 MCP servers, with legacy definitions winning", () => {
const config = lower({
mcp: {
legacy: { type: "local", command: ["legacy-mcp"], enabled: false },
shared: { type: "remote", url: "https://legacy.example.com/mcp" },
servers: {
shared: { type: "remote", url: "https://native.example.com/mcp", disabled: false },
native: { type: "local", command: ["native-mcp"], disabled: true },
},
},
})
expect(config.mcp).toEqual({
legacy: { type: "local", command: ["legacy-mcp"], enabled: false },
shared: { type: "remote", url: "https://legacy.example.com/mcp" },
native: { type: "local", command: ["native-mcp"], enabled: false },
})
})
test("preserves flat MCP servers named servers and timeout", () => {
const config = lower({
mcp: {
servers: { type: "local", command: ["server-named-servers"] },
timeout: { type: "remote", url: "https://timeout.example.com/mcp" },
},
})
expect(config.mcp).toEqual({
servers: { type: "local", command: ["server-named-servers"] },
timeout: { type: "remote", url: "https://timeout.example.com/mcp" },
})
})
test("preserves enabled-only V1 MCP entries and existing enabled states", () => {
const config = lower({
mcp: {
disabled: { enabled: false },
enabled: { enabled: true },
existing: { type: "local", command: ["existing-mcp"], enabled: true },
},
})
expect(config.mcp).toEqual({
disabled: { enabled: false },
enabled: { enabled: true },
existing: { type: "local", command: ["existing-mcp"], enabled: true },
})
})
test("inverts explicit V2 disabled states and enables servers by default", () => {
const config = lower({
mcp: {
servers: {
implicit: { type: "remote", url: "https://implicit.example.com/mcp" },
enabled: { type: "local", command: ["enabled-mcp"], disabled: false },
disabled: { type: "local", command: ["disabled-mcp"], disabled: true },
},
},
})
expect(config.mcp?.implicit).toEqual({ type: "remote", url: "https://implicit.example.com/mcp", enabled: true })
expect(config.mcp?.enabled).toEqual({ type: "local", command: ["enabled-mcp"], enabled: true })
expect(config.mcp?.disabled).toEqual({ type: "local", command: ["disabled-mcp"], enabled: false })
})
test("lowers OAuth field names and preserves disabled OAuth", () => {
const config = lower({
mcp: {
servers: {
authenticated: {
type: "remote",
url: "https://oauth.example.com/mcp",
headers: { Authorization: "Bearer token" },
oauth: {
client_id: "client",
client_secret: "secret",
scope: "read write",
callback_port: 19877,
redirect_uri: "http://127.0.0.1:19877/callback",
},
},
anonymous: { type: "remote", url: "https://anonymous.example.com/mcp", oauth: false },
},
},
})
expect(config.mcp?.authenticated).toEqual({
type: "remote",
url: "https://oauth.example.com/mcp",
headers: { Authorization: "Bearer token" },
enabled: true,
oauth: {
clientId: "client",
clientSecret: "secret",
scope: "read write",
callbackPort: 19877,
redirectUri: "http://127.0.0.1:19877/callback",
},
})
expect(config.mcp?.anonymous).toEqual({
type: "remote",
url: "https://anonymous.example.com/mcp",
enabled: true,
oauth: false,
})
})
test("lowers matching catalog and execution timeouts without collapsing incompatible phases", () => {
const config = lower({
mcp: {
timeout: { catalog: 8000, execution: 8000 },
servers: {
safe: { type: "local", command: ["safe-mcp"], timeout: { catalog: 3000, execution: 3000 } },
unsafe: { type: "local", command: ["unsafe-mcp"], timeout: { catalog: 2000, execution: 4000 } },
partial: { type: "local", command: ["partial-mcp"], timeout: { execution: 5000 } },
startup: {
type: "local",
command: ["startup-mcp"],
timeout: { startup: 1000, catalog: 3000, execution: 3000 },
},
},
},
})
expect(config.experimental?.mcp_timeout).toBe(8000)
expect(config.mcp?.safe).toEqual({ type: "local", command: ["safe-mcp"], enabled: true, timeout: 3000 })
expect(config.mcp?.unsafe).toEqual({ type: "local", command: ["unsafe-mcp"], enabled: true })
expect(config.mcp?.partial).toEqual({ type: "local", command: ["partial-mcp"], enabled: true })
expect(config.mcp?.startup).toEqual({ type: "local", command: ["startup-mcp"], enabled: true })
})
test("does not invent a global V1 timeout from startup or catalog phases", () => {
const config = lower({ mcp: { timeout: { startup: 1000, catalog: 2000 } } })
expect(config.experimental?.mcp_timeout).toBeUndefined()
expect(config.mcp?.timeout).toBeUndefined()
})
test("lowers snapshots, media, compaction, and experimental subagent depth", () => {
const config = lower({
snapshots: false,
media: {
image: { auto_resize: false, max_width: 1920, max_height: 1080, max_base64_bytes: 4096 },
},
compaction: { auto: false, keep: { tokens: 12000 }, buffer: 2048 },
experimental: { subagent_depth: 3 },
})
expect(config.snapshot).toBe(false)
expect(config.attachment).toEqual({
image: { auto_resize: false, max_width: 1920, max_height: 1080, max_base64_bytes: 4096 },
})
expect(config.compaction).toEqual({ auto: false, preserve_recent_tokens: 12000, reserved: 2048 })
expect(config.subagent_depth).toBe(3)
})
test("keeps legacy scalar, media, compaction, and timeout settings over native equivalents", () => {
const config = lower({
snapshot: false,
snapshots: true,
attachment: { image: { max_width: 640 } },
media: { image: { max_width: 1920 } },
subagent_depth: 1,
experimental: { subagent_depth: 3, mcp_timeout: 4000 },
compaction: {
preserve_recent_tokens: 100,
keep: { tokens: 200 },
reserved: 300,
buffer: 400,
},
mcp: { timeout: { catalog: 8000, execution: 8000 } },
})
expect(config.snapshot).toBe(false)
expect(config.attachment?.image?.max_width).toBe(640)
expect(config.subagent_depth).toBe(1)
expect(config.experimental?.mcp_timeout).toBe(4000)
expect(config.compaction).toEqual({ preserve_recent_tokens: 100, reserved: 300 })
})
test("lowers object model selections and strips unsupported top-level variants", () => {
expect(lower({ model: { providerID: "anthropic", model: "claude-sonnet", variant: "fast" } }).model).toBe(
"anthropic/claude-sonnet",
)
expect(lower({ model: "anthropic/claude-sonnet#fast" }).model).toBe("anthropic/claude-sonnet")
expect(lower({ model: "anthropic/claude-sonnet" }).model).toBe("anthropic/claude-sonnet")
})
test("separates skill URLs from filesystem paths", () => {
const config = lower({
skills: ["./skills", "https://example.com/skills", "/opt/skills", "http://localhost:8080/skills"],
})
expect(config.skills).toEqual({
paths: ["./skills", "/opt/skills"],
urls: ["https://example.com/skills", "http://localhost:8080/skills"],
})
})
test("lowers ordered permission rules and renamed actions", () => {
const config = lower({
permissions: [
{ action: "*", resource: "*", effect: "deny" },
{ action: "shell", resource: "*", effect: "deny" },
{ action: "shell", resource: "git status", effect: "allow" },
{ action: "subagent", resource: "explore", effect: "allow" },
{ action: "question", resource: "*", effect: "ask" },
],
})
expect(config.permission).toEqual({
"*": "deny",
bash: { "*": "deny", "git status": "allow" },
task: { explore: "allow" },
question: "ask",
})
})
test("retains final duplicate permission rules in their effective order", () => {
const config = lower({
permissions: [
{ action: "read", resource: "*", effect: "allow" },
{ action: "read", resource: "secret", effect: "deny" },
{ action: "read", resource: "*", effect: "deny" },
],
})
expect(config.permission).toEqual({ read: { secret: "deny", "*": "deny" } })
expect(Object.keys(typeof config.permission?.read === "object" ? config.permission.read : {})).toEqual([
"secret",
"*",
])
})
test("combines legacy and native permissions with legacy rules last", () => {
const config = lower({
permission: { bash: "deny", edit: "deny" },
permissions: [{ action: "shell", resource: "*", effect: "allow" }],
})
expect(config.permission).toEqual({ bash: "deny", edit: "deny" })
expect(Object.keys(config.permission ?? {})).toEqual(["bash", "edit"])
expect(
lower({ permission: "deny", permissions: [{ action: "read", resource: "*", effect: "allow" }] }).permission,
).toEqual({ read: "allow", "*": "deny" })
})
test("rejects permission rules that would change action precedence", () => {
expect(() =>
lower({
permissions: [
{ action: "read", resource: "*", effect: "allow" },
{ action: "*", resource: "*", effect: "deny" },
{ action: "read", resource: "public", effect: "allow" },
],
}),
).toThrow("ConfigInvalidError")
})
test("rejects unsupported resource, action, and home-expansion permissions", () => {
expect(() =>
lower({ permissions: [{ action: "webfetch", resource: "https://internal.example/*", effect: "deny" }] }),
).toThrow("ConfigInvalidError")
expect(() => lower({ permissions: [{ action: "sh*", resource: "*", effect: "deny" }] })).toThrow(
"ConfigInvalidError",
)
expect(() => lower({ permissions: [{ action: "bash", resource: "*", effect: "deny" }] })).toThrow(
"ConfigInvalidError",
)
expect(() => lower({ permissions: [{ action: "shell", resource: "$HOME/private/*", effect: "deny" }] })).toThrow(
"ConfigInvalidError",
)
})
test("rejects malformed native global and agent permission rules", () => {
expect(() => lower({ permissions: "deny" })).toThrow("ConfigInvalidError")
expect(() => lower({ permissions: [{ action: "read", resource: "*", effect: "permit" }] })).toThrow(
"ConfigInvalidError",
)
expect(() => lower({ agents: { reviewer: { permissions: [{ action: "read" }] } } })).toThrow("ConfigInvalidError")
})
test("lowers agent selections, variants, system prompts, and disabled states", () => {
const config = lower({
agents: {
reviewer: {
model: { providerID: "anthropic", model: "claude-sonnet", variant: "thinking" },
system: "Review carefully.",
description: "Reviews changes",
mode: "subagent",
hidden: true,
color: "#123abc",
steps: 4,
disabled: true,
permissions: [
{ action: "read", resource: "*.ts", effect: "allow" },
{ action: "read", resource: "secret.ts", effect: "deny" },
],
},
quick: { model: "openai/gpt-4.1#fast", disabled: false },
},
})
expect(config.agent?.reviewer).toMatchObject({
model: "anthropic/claude-sonnet",
variant: "thinking",
prompt: "Review carefully.",
description: "Reviews changes",
mode: "subagent",
hidden: true,
color: "#123abc",
steps: 4,
disable: true,
})
expect(config.agent?.reviewer?.permission).toEqual({ read: { "*.ts": "allow", "secret.ts": "deny" } })
expect(config.agent?.quick).toMatchObject({ model: "openai/gpt-4.1", variant: "fast", disable: false })
})
test("keeps legacy agent definitions and lowers separately named native agents", () => {
const config = lower({
agent: {
reviewer: {
prompt: "Legacy prompt",
permission: { bash: "deny", edit: "deny" },
},
},
agents: {
reviewer: {
system: "Native prompt",
permissions: [{ action: "shell", resource: "*", effect: "allow" }],
},
native: {
permissions: [{ action: "shell", resource: "*", effect: "deny" }],
},
},
})
expect(config.agent?.reviewer).toMatchObject({
prompt: "Legacy prompt",
permission: { bash: "deny", edit: "deny" },
})
expect(config.agent?.native?.permission).toEqual({ bash: "deny" })
})
test("keeps legacy commands when native commands use the same name", () => {
const config = lower({
command: { review: { template: "Legacy review" } },
commands: {
review: { template: "Native review" },
modern: { template: "Native command" },
},
})
expect(config.command).toEqual({
review: { template: "Legacy review" },
modern: { template: "Native command" },
})
})
test("lowers command model selections and preserves their variants", () => {
const config = lower({
commands: {
review: {
template: "Review $ARGUMENTS",
description: "Review code",
agent: "reviewer",
model: { providerID: "anthropic", model: "claude-sonnet", variant: "thinking" },
subtask: true,
},
quick: { template: "Quick review", model: "openai/gpt-4.1#fast" },
},
})
expect(config.command?.review).toEqual({
template: "Review $ARGUMENTS",
description: "Review code",
agent: "reviewer",
model: "anthropic/claude-sonnet",
variant: "thinking",
subtask: true,
})
expect(config.command?.quick).toEqual({ template: "Quick review", model: "openai/gpt-4.1", variant: "fast" })
})
test("filters custom LSP servers without extensions while retaining built-ins and disabled servers", () => {
const config = lower({
lsp: {
typescript: { command: ["typescript-language-server", "--stdio"] },
compatible: { command: ["custom-lsp"], extensions: [".custom"] },
incompatible: { command: ["incompatible-lsp"] },
disabled: { disabled: true },
},
})
expect(config.lsp).toEqual({
typescript: { command: ["typescript-language-server", "--stdio"] },
compatible: { command: ["custom-lsp"], extensions: [".custom"] },
disabled: { disabled: true },
})
})
test("lowers native permission rules while ignoring unsupported plugins and providers", () => {
const config = lower({
permissions: [{ action: "read", resource: "*", effect: "allow" }],
plugins: [{ package: "@example/native-plugin" }],
providers: { native: { models: { example: { name: "Native model" } } } },
policies: [{ action: "provider.use", effect: "deny", resource: "openai" }],
})
expect(config.permission).toEqual({ read: "allow" })
expect(config.plugin).toBeUndefined()
expect(config.provider).toBeUndefined()
expect(config.experimental?.policies).toBeUndefined()
})
test("preserves existing V1 policies when other V2 experimental fields are lowered", () => {
const config = lower({
experimental: {
subagent_depth: 2,
policies: [{ effect: "deny", action: "provider.use", resource: "openai" }],
batch_tool: true,
},
})
expect(config.subagent_depth).toBe(2)
expect(config.experimental?.batch_tool).toBe(true)
expect(config.experimental?.policies).toEqual([{ effect: "deny", action: "provider.use", resource: "openai" }])
})
test("does not sanitize malformed V1 fields before schema validation", () => {
expect(() => lower({ model: 42 })).toThrow()
expect(() => lower({ snapshot: "enabled" })).toThrow()
expect(() => lower({ mcp: { broken: { type: "local", command: "not-an-array" } } })).toThrow()
expect(() => lower({ experimental: { mcp_timeout: -1 } })).toThrow()
})
test("does not mutate the input or nested configuration objects", () => {
const input = {
model: { providerID: "anthropic", model: "claude-sonnet", variant: "fast" },
snapshots: true,
skills: ["./skills", "https://example.com/skills"],
mcp: {
existing: { type: "local", command: ["existing-mcp"], enabled: false },
servers: {
native: {
type: "remote",
url: "https://example.com/mcp",
disabled: true,
oauth: { client_id: "client" },
timeout: { execution: 3000 },
},
},
},
agents: { reviewer: { model: "anthropic/claude-sonnet#thinking", disabled: true } },
experimental: { subagent_depth: 2 },
}
const original = structuredClone(input)
lower(input)
expect(input).toEqual(original)
})
})
describe("V2 configuration loading", () => {
it.instance("logs compatibility diagnostics without writing the lowered projection", () =>
Effect.gen(function* () {
const instance = yield* TestInstance
const fs = yield* FSUtil.Service
const file = path.join(instance.directory, "opencode.jsonc")
const text =
'{\n // Retain this comment\n "$schema": "https://opencode.ai/config.json",\n "plugins": ["native-only"]\n}\n'
yield* fs.writeWithDirs(file, text)
const messages: unknown[] = []
const config = yield* Config.use.get().pipe(
Effect.provide(
Logger.layer([
Logger.make<unknown, void>((options) => {
messages.push(options.message)
}),
]),
),
)
expect(config.plugin).toEqual([])
expect(messages).toContainEqual([
"configuration compatibility diagnostic",
expect.objectContaining({ source: file, kind: "unsupported", path: ["plugins"] }),
])
expect(yield* fs.readFileString(file)).toBe(text)
}),
)
it.instance("loads native V2 configuration through the V1 Config service", () =>
Effect.gen(function* () {
const instance = yield* TestInstance
const fs = yield* FSUtil.Service
yield* fs.writeWithDirs(
path.join(instance.directory, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
model: { providerID: "anthropic", model: "claude-sonnet", variant: "fast" },
snapshots: false,
skills: ["./skills", "https://example.com/skills"],
mcp: {
timeout: { catalog: 9000, execution: 9000 },
servers: {
native: { type: "remote", url: "https://native.example.com/mcp", disabled: false },
},
},
permissions: [
{ action: "shell", resource: "*", effect: "deny" },
{ action: "shell", resource: "git status", effect: "allow" },
],
agents: {
reviewer: {
model: "anthropic/claude-sonnet#thinking",
system: "Review carefully.",
permissions: [{ action: "subagent", resource: "explore", effect: "deny" }],
},
},
commands: {
review: {
template: "Review $ARGUMENTS",
model: { providerID: "anthropic", model: "claude-sonnet", variant: "thinking" },
},
},
experimental: { subagent_depth: 2 },
}),
)
const config = yield* Config.use.get()
expect(config.model).toBe("anthropic/claude-sonnet")
expect(config.snapshot).toBe(false)
expect(config.skills).toEqual({ paths: ["./skills"], urls: ["https://example.com/skills"] })
expect(config.mcp?.native).toEqual({ type: "remote", url: "https://native.example.com/mcp", enabled: true })
expect(config.experimental?.mcp_timeout).toBe(9000)
expect(config.permission).toMatchObject({ bash: { "*": "deny", "git status": "allow" } })
expect(config.agent?.reviewer).toMatchObject({
model: "anthropic/claude-sonnet",
variant: "thinking",
prompt: "Review carefully.",
permission: { task: { explore: "deny" } },
})
expect(config.command?.review).toMatchObject({
template: "Review $ARGUMENTS",
model: "anthropic/claude-sonnet",
variant: "thinking",
})
expect(config.subagent_depth).toBe(2)
}),
)
it.instance("keeps legacy TUI normalization when loading a mixed V1 and V2 document", () =>
Effect.gen(function* () {
const instance = yield* TestInstance
const fs = yield* FSUtil.Service
yield* fs.writeWithDirs(
path.join(instance.directory, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
model: { providerID: "openai", model: "gpt-4.1" },
theme: "legacy",
keybinds: { leader: "ctrl+x" },
tui: { scroll_speed: 4 },
mcp: {
legacy: { enabled: false },
servers: { native: { type: "local", command: ["native-mcp"] } },
},
}),
)
const config = yield* Config.use.get()
expect(config.model).toBe("openai/gpt-4.1")
expect(config.mcp?.legacy).toEqual({ enabled: false })
expect(config.mcp?.native).toEqual({ type: "local", command: ["native-mcp"], enabled: true })
expect(config).not.toHaveProperty("theme")
expect(config).not.toHaveProperty("keybinds")
expect(config).not.toHaveProperty("tui")
}),
)
})
+1 -8
View File
@@ -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:"
},
@@ -3,6 +3,7 @@ import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import {
getStatsModelsComparisonData,
type ModelUsagePoint,
type RetentionEntry,
type StatsModelComparisonInput,
type StatsModelComparisonEntry,
} from "@opencode-ai/stats-core/domain/home"
@@ -949,6 +950,17 @@ function buildComparisonDetailSections(models: readonly ComparisonModel[]): Comp
],
usage: models.map((model) => model.stats?.usage ?? []),
},
{
title: "Retention",
badge: "Week 1",
rows: [
comparisonDetailRow(
"Returning users",
models.map((model) => retentionCell(model.stats?.weeklyRetention)),
"higher",
),
],
},
]
}
@@ -1031,6 +1043,15 @@ function percentCell(value: number | undefined): ComparisonDetailCell {
return value === undefined ? { value: "No usage" } : { value: formatPercent(value), score: value }
}
function retentionCell(value: RetentionEntry | null | undefined): ComparisonDetailCell {
if (!value || value.rank === null) return { value: "Pending" }
return {
value: formatPercent(value.rate),
unit: `${formatTokens(value.eligibleUserWeeks)} user-weeks`,
score: value.rate,
}
}
function tokenCell(value: number | undefined, trend: number | undefined): ComparisonDetailCell {
if (value === undefined) return { value: "No usage" }
return { value: formatTokens(value), score: value, trend }
+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": "使用量到達後會顯示同類模型排名。",
+8 -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"
@@ -471,6 +470,7 @@ function ModelMomentumSection(props: { data: StatsModelPageData | null }) {
value={formatInteger(data().totals.sessions)}
/>
<MomentumMetric label={i18n.t("model.tokenShare")} value={formatPercent(data().tokenShare)} />
<MomentumMetric label="Weekly Retention" value={formatModelRetention(data())} />
<MomentumMetric
label="Rank"
value={formatRankLabel(data().rank)}
@@ -541,6 +541,11 @@ function MomentumMetric(props: { label: string; value: string; watermark?: strin
)
}
function formatModelRetention(data: StatsModelPageData) {
if (!data.weeklyRetention || data.weeklyRetention.eligibleUserWeeks < 100) return "Pending"
return formatPercent(data.weeklyRetention.rate)
}
function ModelUsageSection(props: { data: StatsModelPageData | null }) {
const i18n = useI18n()
return (
@@ -892,21 +897,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 +911,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 +929,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 +1021,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] } }] : []
}),
]
+180 -123
View File
@@ -1737,6 +1737,7 @@ body {
:is(
[data-section="leaderboard"],
[data-section="unique-users"],
[data-section="retention"],
[data-section="market-share"],
[data-section="geo-breakdown"],
[data-section="token-cost"],
@@ -1774,6 +1775,127 @@ body {
white-space: nowrap;
}
[data-page="stats"] [data-component="retention-chart"] {
display: grid;
gap: 8px;
min-width: 0;
margin-top: 40px;
}
[data-page="stats"] [data-slot="retention-heading"],
[data-page="stats"] [data-component="retention-chart"] a {
display: grid;
grid-template-columns: 40px minmax(160px, 260px) minmax(180px, 1fr) 64px 84px;
align-items: center;
gap: 16px;
min-width: 0;
}
[data-page="stats"] [data-slot="retention-heading"] {
box-sizing: border-box;
min-height: 28px;
padding: 0 12px;
color: var(--stats-faint);
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 14px;
text-transform: uppercase;
}
[data-page="stats"] [data-slot="retention-heading"] :is(strong, b, em, i) {
font: inherit;
font-style: normal;
}
[data-page="stats"] [data-slot="retention-heading"] :is(b, em) {
text-align: right;
}
[data-page="stats"] [data-component="retention-chart"] ol {
display: grid;
gap: 4px;
min-width: 0;
margin: 0;
padding: 0;
list-style: none;
}
[data-page="stats"] [data-component="retention-chart"] a {
box-sizing: border-box;
min-height: 48px;
padding: 8px 12px;
border: 1px solid var(--stats-line);
color: var(--stats-text);
background: var(--stats-layer);
text-decoration: none;
}
[data-page="stats"] [data-component="retention-chart"] a:hover,
[data-page="stats"] [data-component="retention-chart"] a:focus-visible,
[data-page="stats"] [data-component="retention-chart"] a[data-active="true"] {
border-color: var(--stats-line-strong);
background: var(--stats-layer-2);
outline: none;
}
[data-page="stats"] [data-component="retention-chart"] a > span {
color: var(--stats-faint);
font-size: 13px;
font-weight: 400;
}
[data-page="stats"] [data-component="retention-chart"] a > strong {
min-width: 0;
overflow-wrap: anywhere;
font-size: 13px;
font-weight: 600;
line-height: 18px;
}
[data-page="stats"] [data-component="retention-chart"] a > b,
[data-page="stats"] [data-component="retention-chart"] a > em {
font-size: 13px;
font-style: normal;
font-weight: 500;
text-align: right;
white-space: nowrap;
}
[data-page="stats"] [data-component="retention-chart"] a > em {
color: var(--stats-muted);
}
[data-page="stats"] [data-component="retention-marker"] {
position: relative;
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: center;
height: 16px;
background: linear-gradient(var(--stats-line-strong), var(--stats-line-strong)) center / 100% 1px no-repeat;
}
[data-page="stats"] [data-component="retention-marker"] > span {
justify-self: end;
width: 1px;
height: 8px;
background: var(--stats-line-strong);
}
[data-page="stats"] [data-component="retention-marker"] > em {
position: absolute;
top: 50%;
left: var(--retention-position);
width: 7px;
height: 16px;
background: var(--stats-muted);
transform: translate(-50%, -50%);
}
[data-page="stats"] [data-component="retention-marker"][data-active="true"] > em {
background: var(--stats-accent);
}
[data-page="stats"] [data-component="section-bridge"]:hover {
color: var(--stats-text);
text-decoration: none;
@@ -2264,121 +2386,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));
@@ -3636,7 +3643,7 @@ body {
[data-page="stats"] [data-slot="model-momentum-metrics"] {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
min-width: 0;
}
@@ -6786,6 +6793,57 @@ body {
}
}
@media (max-width: 74rem) {
[data-page="stats"] [data-slot="retention-heading"],
[data-page="stats"] [data-component="retention-chart"] a {
grid-template-columns: 40px minmax(140px, 220px) minmax(140px, 1fr) 60px 76px;
gap: 12px;
}
}
@media (max-width: 47.999rem) {
[data-page="stats"] [data-component="retention-chart"] {
margin-top: 28px;
}
[data-page="stats"] [data-slot="retention-heading"] {
display: none;
}
[data-page="stats"] [data-component="retention-chart"] a {
grid-template-columns: 28px minmax(0, 1fr) 58px 62px;
grid-template-rows: auto 16px;
gap: 8px 10px;
min-height: 68px;
padding: 10px;
}
[data-page="stats"] [data-component="retention-chart"] a > span {
grid-column: 1;
grid-row: 1;
}
[data-page="stats"] [data-component="retention-chart"] a > strong {
grid-column: 2;
grid-row: 1;
}
[data-page="stats"] [data-component="retention-marker"] {
grid-column: 2 / -1;
grid-row: 2;
}
[data-page="stats"] [data-component="retention-chart"] a > b {
grid-column: 3;
grid-row: 1;
}
[data-page="stats"] [data-component="retention-chart"] a > em {
grid-column: 4;
grid-row: 1;
}
}
[data-page="stats"] [data-component="compare-model-modal-scrim"] {
position: fixed;
inset: 0;
@@ -7483,6 +7541,7 @@ body {
[data-page="stats"] [data-section="top-models"],
[data-page="stats"] [data-section="leaderboard"],
[data-page="stats"] [data-section="unique-users"],
[data-page="stats"] [data-section="retention"],
[data-page="stats"] [data-section="market-share"],
[data-page="stats"] [data-section="geo-breakdown"],
[data-page="stats"] [data-section="token-cost"],
@@ -7644,6 +7703,10 @@ body {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
[data-page="stats"] [data-component="model-momentum-metric"]:last-child:nth-child(odd) {
grid-column: 1 / -1;
}
[data-page="stats"] [data-component="model-metric-grid"],
[data-page="stats"] [data-component="model-metric-grid"][data-variant="dense"],
[data-page="stats"] [data-component="model-efficiency-grid"],
@@ -7783,6 +7846,7 @@ body {
[data-page="stats"] [data-section="top-models"],
[data-page="stats"] [data-section="leaderboard"],
[data-page="stats"] [data-section="unique-users"],
[data-page="stats"] [data-section="retention"],
[data-page="stats"] [data-section="market-share"],
[data-page="stats"] [data-section="geo-breakdown"],
[data-page="stats"] [data-section="token-cost"],
@@ -8386,13 +8450,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;
}
+82 -122
View File
@@ -8,6 +8,7 @@ import {
type CountryEntry,
type LeaderboardEntry,
type MarketDay,
type RetentionEntry,
type SessionCostEntry,
type TokenCostEntry,
type UsagePoint,
@@ -21,7 +22,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"
@@ -70,6 +70,7 @@ type StatsHomePageData = {
tokenCost: TokenCostEntry[]
cacheRatio: CacheRatioEntry[]
sessionCost: SessionCostEntry[]
retention: RetentionEntry[]
country: CountryEntry[]
}
@@ -89,6 +90,7 @@ const getData = query(async () => {
tokenCost: priceTokenCostFromCatalog(stats.tokenCost.Go, catalog),
cacheRatio: stats.cacheRatio.Go,
sessionCost: stats.sessionCost.Go,
retention: stats.retention,
country: stats.country["2M"],
} satisfies StatsHomePageData
}, "getStatsHomeData")
@@ -147,6 +149,7 @@ export default function StatsHome() {
<Hero updatedAt={stats().updatedAt} />
<TopModelsSection data={stats().usage} leaderboard={stats().leaderboard} />
<UniqueUsersSection data={stats().users} />
<RetentionSection data={stats().retention} />
<SessionCostSection data={stats().sessionCost} />
<TokenCostSection data={stats().tokenCost} />
<CacheRatioSection data={stats().cacheRatio} />
@@ -317,7 +320,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} />
}
@@ -617,6 +620,82 @@ function UniqueUsersSection(props: { data: UsagePoint[] }) {
)
}
function RetentionSection(props: { data: RetentionEntry[] }) {
const language = useLanguage()
const [activeIndex, setActiveIndex] = createSignal(0)
return (
<section id="retention" data-section="retention">
<SectionTitle id="retention" title="Weekly Retention" description="Weekly users returning the next week." />
<Show
when={props.data.length > 0}
fallback={
<EmptyState
title="No retention data"
description="Retention appears after a complete return week is available."
/>
}
>
<div data-component="retention-chart">
<div data-slot="retention-heading" aria-hidden="true">
<span>Rank</span>
<strong>Model</strong>
<i>Retention</i>
<b>Rate</b>
<em>Eligible</em>
</div>
<ol>
<For each={props.data}>
{(item, index) => (
<li>
<a
data-active={activeIndex() === index() ? "true" : undefined}
href={language.route(
`${import.meta.env.BASE_URL}${modelSlug(item.provider)}/${modelSlug(item.model)}`,
)}
onPointerEnter={() => setActiveIndex(index())}
onFocus={() => setActiveIndex(index())}
onClick={() => setActiveIndex(index())}
aria-label={`${item.model}, ${formatRetentionRate(item.rate)} weekly retention, ${formatUsers(item.eligibleUserWeeks)} eligible user-weeks`}
>
<span>{item.rank === null ? "" : String(item.rank).padStart(2, "0")}</span>
<strong>{item.model}</strong>
<RetentionMarker rate={item.rate} active={activeIndex() === index()} />
<b>{formatRetentionRate(item.rate)}</b>
<em>{formatUsers(item.eligibleUserWeeks)}</em>
</a>
</li>
)}
</For>
</ol>
</div>
</Show>
</section>
)
}
function RetentionMarker(props: { rate: number; active: boolean }) {
const fill = createMemo(() => Math.min(100, Math.max(0, props.rate)))
return (
<i
data-component="retention-marker"
data-active={props.active ? "true" : undefined}
style={{ "--retention-position": `${fill()}%` } as JSX.CSSProperties}
aria-hidden="true"
>
<span />
<span />
<span />
<span />
<em />
</i>
)
}
function formatRetentionRate(value: number) {
return `${value.toFixed(1)}%`
}
function isTopModelsBlankHover(bar: HTMLElement, clientY: number) {
const stack = bar.querySelector<HTMLElement>('[data-slot="top-models-stack"]')
if (!stack) return true
@@ -1074,20 +1153,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 +1167,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 +1185,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>
</>
)}
</>
)
@@ -0,0 +1,18 @@
CREATE TABLE `model_retention` (
`id` bigint AUTO_INCREMENT NOT NULL,
`cohort_date` char(10) NOT NULL,
`dataset` varchar(64) NOT NULL DEFAULT 'all',
`tier` varchar(64) NOT NULL DEFAULT 'all',
`provider` varchar(128) NOT NULL,
`model` varchar(256) NOT NULL,
`eligible_users` bigint NOT NULL DEFAULT 0,
`retained_users` bigint NOT NULL DEFAULT 0,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT `model_retention_id` PRIMARY KEY(`id`),
CONSTRAINT `uniq_model_retention_cohort` UNIQUE(`cohort_date`,`dataset`,`tier`,`provider`,`model`)
);
--> statement-breakpoint
CREATE INDEX `idx_model_retention_recent` ON `model_retention` (`dataset`,`tier`,`cohort_date`);
--> statement-breakpoint
CREATE INDEX `idx_model_retention_model` ON `model_retention` (`model`,`cohort_date`);
@@ -107,6 +107,32 @@ export const geoStat = mysqlTable(
],
)
export const modelRetention = mysqlTable(
"model_retention",
{
id: bigint({ mode: "number" }).autoincrement().primaryKey(),
cohort_date: char({ length: 10 }).notNull(),
dataset: varchar({ length: 64 }).notNull().default("all"),
tier: varchar({ length: 64 }).notNull().default("all"),
provider: varchar({ length: 128 }).notNull(),
model: varchar({ length: 256 }).notNull(),
eligible_users: bigint({ mode: "number" }).notNull().default(0),
retained_users: bigint({ mode: "number" }).notNull().default(0),
...timestampColumns(),
},
(table) => [
uniqueIndex("uniq_model_retention_cohort").on(
table.cohort_date,
table.dataset,
table.tier,
table.provider,
table.model,
),
index("idx_model_retention_recent").on(table.dataset, table.tier, table.cohort_date),
index("idx_model_retention_model").on(table.model, table.cohort_date),
],
)
function periodColumns() {
return {
id: bigint({ mode: "number" }).autoincrement().primaryKey(),
@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test"
import type { RetentionMetricRow } from "./home"
process.env.SST_RESOURCE_App = JSON.stringify({ name: "opencode", stage: "test" })
process.env.SST_RESOURCE_StatsDatabase = JSON.stringify({ url: "mysql://localhost/stats" })
const { buildRetentionEntries } = await import("./home")
describe("retention aggregates", () => {
test("pools the latest seven weekly cohorts and ranks models above the sample floor", () => {
const rows = [
...cohorts("model-a", "provider-a", 8, 20, 10),
...cohorts("model-b", "provider-b", 8, 20, 12),
...cohorts("small-model", "provider-c", 8, 10, 9),
]
const entries = buildRetentionEntries(rows)
expect(entries.find((item) => item.model === "model-a")).toMatchObject({
eligibleUserWeeks: 140,
retainedUserWeeks: 70,
rate: 50,
rank: 2,
})
expect(entries.find((item) => item.model === "model-b")).toMatchObject({
eligibleUserWeeks: 140,
retainedUserWeeks: 84,
rate: 60,
rank: 1,
})
expect(entries.find((item) => item.model === "small-model")).toMatchObject({
eligibleUserWeeks: 70,
retainedUserWeeks: 63,
rate: 90,
rank: null,
})
})
})
function cohorts(model: string, provider: string, count: number, eligibleUsers: number, retainedUsers: number) {
return Array.from({ length: count }, (_, index) => ({
cohortDate: `2026-08-${String(index + 1).padStart(2, "0")}`,
updatedAt: Date.UTC(2026, 7, index + 9),
provider,
model,
eligibleUsers,
retainedUsers,
})) satisfies RetentionMetricRow[]
}
+108 -6
View File
@@ -5,6 +5,7 @@ import { DatabaseError } from "../database"
import type { GeoStatMetric } from "./geo"
import { ModelStatRepo, type ModelStatMetric } from "./model"
import { statProvider } from "./model-normalization"
import { isMissingRetentionTable } from "./retention"
import { DATA_SITE_TIERS, normalizeTier } from "./stat"
export type UsageProduct = "All Users" | "Zen" | "Go" | "Enterprise"
@@ -23,6 +24,15 @@ export type LeaderboardEntry = {
export type TokenCostEntry = { model: string; total: number; input: number; output: number; cached: number }
export type CacheRatioEntry = { model: string; ratio: number; cached: number; uncached: number; total: number }
export type SessionCostEntry = { model: string; cost: number; tokens: number }
export type RetentionEntry = {
model: string
provider: string
author: string
rate: number
eligibleUserWeeks: number
retainedUserWeeks: number
rank: number | null
}
export type CountryEntry = { country: string; continent: string; tokens: number; share: number; rank: number }
export type ModelUsagePoint = { date: string; tokens: number; users: number; sessions: number; cost: number }
export type ModelMixEntry = { label: string; tokens: number; share: number }
@@ -54,6 +64,7 @@ export type StatsModelData = {
totalModels: number
tokenShare: number
tokenChange: number
weeklyRetention: RetentionEntry | null
totals: {
sessions: number
uniqueUsers: number
@@ -94,6 +105,7 @@ export type StatsModelComparisonEntry = {
totalModels: number
tokenShare: number
tokenChange: number
weeklyRetention: RetentionEntry | null
totals: StatsModelData["totals"]
usage: ModelUsagePoint[]
}
@@ -114,6 +126,7 @@ export type StatsHomeData = {
tokenCost: Record<TokenProduct, TokenCostEntry[]>
cacheRatio: Record<TokenProduct, CacheRatioEntry[]>
sessionCost: Record<TokenProduct, SessionCostEntry[]>
retention: RetentionEntry[]
country: Record<UsageRange, CountryEntry[]>
}
@@ -129,6 +142,9 @@ const DAY_MS = 86_400_000
const TOKEN_SCALE = 1_000_000
const DOLLARS_PER_MICROCENT = 1 / 100_000_000
const METRIC_MODEL_LIMIT = 10
const RETENTION_MODEL_LIMIT = 15
const RETENTION_MIN_ELIGIBLE_USER_WEEKS = 100
const RETENTION_COHORT_WEEKS = 7
const TOP_MODEL_SEGMENT_LIMIT = 9
// Preserve the response shape while the public site presents Go and Free as one cohort.
const SITE_PRODUCT = "Go"
@@ -144,6 +160,14 @@ type GeoMetricRow = Omit<GeoStatMetric, "updatedAt"> & {
periodStart: number
updatedAt: number
}
export type RetentionMetricRow = {
cohortDate: string
updatedAt: number
provider: string
model: string
eligibleUsers: number
retainedUsers: number
}
type DateWindow = { start: number; end: number; previousStart: number; previousEnd: number }
type Bucket = { start: number; end: number; label: string }
@@ -167,8 +191,12 @@ type RawRow = Record<string, unknown>
export function getStatsHomeData(): Effect.Effect<StatsHomeData, StatsDataError> {
return Effect.tryPromise({
try: async () => {
const [modelRows, geoRows] = await Promise.all([listModelDaily(), listGeoDaily()])
return buildStatsHomeData(modelRows, geoRows)
const [modelRows, geoRows, retentionRows] = await Promise.all([
listModelDaily(),
listGeoDaily(),
listRetentionWeekly(),
])
return buildStatsHomeData(modelRows, geoRows, retentionRows)
},
catch: (cause) => new StatsDataError(cause),
})
@@ -180,7 +208,7 @@ export function getStatsModelData(
): Effect.Effect<StatsModelData | null, StatsDataError> {
return Effect.tryPromise({
try: async () => {
const modelRows = await listModelDaily()
const [modelRows, retentionRows] = await Promise.all([listModelDaily(), listRetentionWeekly()])
const normalized = modelRows.flatMap(normalizeStatRow)
const resolvedModel = resolveModelName(model, normalized, provider)
if (!resolvedModel) return null
@@ -192,6 +220,7 @@ export function getStatsModelData(
provider: resolveModelProvider(resolvedModel, normalized, provider),
}),
provider,
retentionRows,
)
},
catch: (cause) => new StatsDataError(cause),
@@ -260,6 +289,27 @@ async function listGeoDaily(opts?: { provider?: string; model?: string }): Promi
}))
}
async function listRetentionWeekly(): Promise<RetentionMetricRow[]> {
try {
return (
await queryRows(
`select cohort_date, updated_at, provider, model, eligible_users, retained_users
from model_retention where dataset = 'zen' and tier = 'Go' order by cohort_date`,
)
).map((row) => ({
cohortDate: stringValue(row.cohort_date),
updatedAt: dateValue(row.updated_at).getTime(),
provider: stringValue(row.provider),
model: stringValue(row.model),
eligibleUsers: numberValue(row.eligible_users),
retainedUsers: numberValue(row.retained_users),
}))
} catch (cause) {
if (isMissingRetentionTable(cause)) return []
throw cause
}
}
async function queryRows(query: string, params: string[] = []) {
return (await new Client({ url: databaseUrl() }).execute(query, params)).rows as RawRow[]
}
@@ -285,8 +335,16 @@ export const getStatsModelsComparisonData: (
) => Effect.Effect<StatsModelComparisonData, DatabaseError, ModelStatRepo> = Effect.fn("StatsModelsComparison.getData")(
function* (models) {
const modelStats = yield* ModelStatRepo
const rows = yield* modelStats.listDaily()
const entries = models.map((model) => toComparisonEntry(buildStatsModelData(model.model, rows, [], model.provider)))
const [rows, retentionRows] = yield* Effect.all([
modelStats.listDaily(),
Effect.tryPromise({
try: listRetentionWeekly,
catch: (cause) => DatabaseError.make({ cause }),
}),
])
const entries = models.map((model) =>
toComparisonEntry(buildStatsModelData(model.model, rows, [], model.provider, retentionRows)),
)
const latest = entries
.map((model) => model?.updatedAt)
.flatMap((value) => (value ? [dateTime(value)] : []))
@@ -309,7 +367,11 @@ export const getStatsModelComparisonData = (
{ provider: secondProvider, model: secondModel },
])
function buildStatsHomeData(modelRows: ModelStatMetric[], geoRows: GeoStatMetric[]): StatsHomeData {
function buildStatsHomeData(
modelRows: ModelStatMetric[],
geoRows: GeoStatMetric[],
retentionRows: RetentionMetricRow[],
): StatsHomeData {
const normalized = modelRows.flatMap(normalizeStatRow)
const geo = geoRows.flatMap(normalizeGeoRow)
const periods = [...normalized, ...geo]
@@ -357,6 +419,9 @@ function buildStatsHomeData(modelRows: ModelStatMetric[], geoRows: GeoStatMetric
sessionCost: createTokenProductRecord((product) =>
buildSessionCost(normalized, product, getWindow("1W", earliest, latest)),
),
retention: buildRetentionEntries(retentionRows)
.filter((item) => item.rank !== null)
.slice(0, RETENTION_MODEL_LIMIT),
country: createRangeRecord((range) => buildCountryStats(geo, getWindow(range, earliest, latest))),
}
}
@@ -366,6 +431,7 @@ function buildStatsModelData(
modelRows: ModelStatMetric[],
geoRows: GeoStatMetric[],
providerParam?: string,
retentionRows: RetentionMetricRow[] = [],
): StatsModelData | null {
const normalized = modelRows.flatMap(normalizeStatRow)
const geo = geoRows.flatMap(normalizeGeoRow)
@@ -401,6 +467,7 @@ function buildStatsModelData(
const peerRank = rankIndex >= 0 ? rankIndex + 1 : 1
const totalTokens = windowPeers.reduce((sum, item) => sum + item.totalTokens, 0)
const peerTokens = rankPeers.reduce((sum, item) => sum + item.totalTokens, 0)
const weeklyRetention = buildRetentionEntries(retentionRows).find((item) => item.model === model) ?? null
return {
updatedAt: Number.isFinite(latestUpdate) ? new Date(latestUpdate).toISOString() : null,
@@ -413,6 +480,7 @@ function buildStatsModelData(
totalModels: windowPeers.length,
tokenShare: totalTokens > 0 ? round((current.totalTokens / totalTokens) * 100, 2) : 0,
tokenChange: percentChange(current.totalTokens, previous.totalTokens),
weeklyRetention,
totals: {
sessions: current.sessions,
uniqueUsers: current.uniqueUsers,
@@ -494,6 +562,7 @@ function toComparisonEntry(data: StatsModelData | null): StatsModelComparisonEnt
totalModels: data.totalModels,
tokenShare: data.tokenShare,
tokenChange: data.tokenChange,
weeklyRetention: data.weeklyRetention,
totals: data.totals,
usage: data.usage,
}
@@ -509,10 +578,43 @@ function emptyStatsHomeData(): StatsHomeData {
tokenCost: createTokenProductRecord(() => []),
cacheRatio: createTokenProductRecord(() => []),
sessionCost: createTokenProductRecord(() => []),
retention: [],
country: createRangeRecord(() => []),
}
}
export function buildRetentionEntries(rows: RetentionMetricRow[]): RetentionEntry[] {
const cohortDates = [...new Set(rows.map((row) => row.cohortDate))].toSorted().slice(-RETENTION_COHORT_WEEKS)
const aggregate = rows
.filter((row) => cohortDates.includes(row.cohortDate))
.reduce<Map<string, Omit<RetentionEntry, "author" | "rate" | "rank">>>((result, row) => {
const current = result.get(row.model)
result.set(row.model, {
model: row.model,
provider: current?.provider ?? row.provider,
eligibleUserWeeks: (current?.eligibleUserWeeks ?? 0) + row.eligibleUsers,
retainedUserWeeks: (current?.retainedUserWeeks ?? 0) + row.retainedUsers,
})
return result
}, new Map())
const entries = [...aggregate.values()].map((item) => ({
...item,
author: formatProvider(item.provider),
rate: item.eligibleUserWeeks > 0 ? round((item.retainedUserWeeks / item.eligibleUserWeeks) * 100, 1) : 0,
}))
const ranks = new Map(
entries
.filter((item) => item.eligibleUserWeeks >= RETENTION_MIN_ELIGIBLE_USER_WEEKS)
.toSorted(
(a, b) => b.rate - a.rate || b.eligibleUserWeeks - a.eligibleUserWeeks || a.model.localeCompare(b.model),
)
.map((item, index) => [item.model, index + 1]),
)
return entries
.map((item) => ({ ...item, rank: ranks.get(item.model) ?? null }))
.toSorted((a, b) => (a.rank ?? Number.MAX_SAFE_INTEGER) - (b.rank ?? Number.MAX_SAFE_INTEGER))
}
function buildUsagePoints(
rows: StatMetricRow[],
product: UsageProduct,
@@ -1,5 +1,12 @@
import { describe, expect, test } from "bun:test"
import { buildStatsQueries, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./inference"
import {
buildRetentionQueries,
buildStatsQueries,
toGeoAggregate,
toModelAggregate,
toProviderAggregate,
toRetentionAggregate,
} from "./inference"
import { modelAuthor, normalizeInferenceModel, statModel, statProvider } from "./model-normalization"
describe("inference stat normalization", () => {
@@ -155,6 +162,62 @@ describe("inference stat normalization", () => {
expect(query).toContain("(source = 'inference-legacy' AND started_at < '2026-08-11T10:57:48.186Z')")
expect(query).toContain("(source = 'inference' AND started_at >= '2026-08-11T10:57:48.186Z')")
})
test("builds complete week-over-week retention queries", () => {
const queries = buildRetentionQueries(new Date("2026-08-10T00:00:00.000Z"), new Date("2026-08-31T00:00:00.000Z"), {
namespace: "inference",
table: "generation",
dataset: "zen",
})
expect(queries).toHaveLength(1)
expect(queries[0]?.cohortDates).toEqual(["2026-08-10", "2026-08-17"])
expect(queries[0]?.query).toContain("AND product = 'go'")
expect(queries[0]?.query).toContain("COUNT(*) AS model_requests")
expect(queries[0]?.query).toContain("SUM(model_requests) AS total_requests")
expect(queries[0]?.query).toContain("MAX(model_requests) AS max_model_requests")
expect(queries[0]?.query).toContain("GROUP BY cohort_date, user_key")
expect(queries[0]?.query).toContain("INNER JOIN user_totals")
expect(queries[0]?.query).toContain("model_usage.model_requests = user_totals.max_model_requests")
expect(queries[0]?.query).toContain("user_totals.total_requests >= 10")
expect(queries[0]?.query).toContain(
"CAST(model_usage.model_requests AS double) / NULLIF(user_totals.total_requests, 0) >= 0.8",
)
expect(queries[0]?.query).not.toContain(" OVER (")
expect(queries[0]?.query).toContain("WHEN '2026-08-17' THEN '2026-08-10'")
expect(queries[0]?.query).toContain("WHEN '2026-08-24' THEN '2026-08-17'")
expect(queries[0]?.query).toContain("started_at >= '2026-08-10T00:00:00.000Z'")
expect(queries[0]?.query).toContain("started_at < '2026-08-31T00:00:00.000Z'")
expect(queries[0]?.query).toContain("LEFT JOIN returned ON primary_models.user_key = returned.user_key")
expect(queries[0]?.query).toContain("primary_models.cohort_date = returned.cohort_date")
expect(queries[0]?.query).toContain("'Go' AS tier")
expect(queries[0]?.query).toContain("COUNT(*) AS eligible_users")
expect(queries[0]?.query).toContain("LIMIT 10000")
})
test("maps retention query results", () => {
expect(
toRetentionAggregate({
cohort_date: "2026-08-10",
dataset: "zen",
tier: "all",
provider: "deepseek",
model: "deepseek-v4-flash-free",
eligible_users: "125",
retained_users: "74",
}),
).toEqual([
{
cohortDate: "2026-08-10",
dataset: "zen",
tier: "all",
provider: "deepseek",
model: "deepseek-v4-flash",
eligibleUsers: 125,
retainedUsers: 74,
},
])
})
})
function aggregate(model: string, provider: string) {
+161
View File
@@ -12,6 +12,7 @@ import {
statProvider,
} from "./model-normalization"
import type { ProviderStatAggregate } from "./provider"
import type { RetentionStatAggregate } from "./retention"
import {
normalizeCountry,
normalizeTier,
@@ -23,6 +24,7 @@ import {
export type StatDimension = "model" | "provider" | "geo" | "geo_model"
export type StatsQuerySource = { namespace: string; table: string; dataset: string }
export type RetentionQuery = { cohortDates: string[]; query: string }
type StatsQueryFamily = "usage" | "geo"
const DAY_MS = 86_400_000
@@ -46,6 +48,139 @@ export function buildStatsQueries(periodStart: Date, periodEnd: Date, input?: St
)
}
export function buildRetentionQueries(periodStart: Date, periodEnd: Date, input?: StatsQuerySource): RetentionQuery[] {
const source = input ?? {
namespace: Resource.R2Sql.namespace,
table: Resource.R2Sql.table,
dataset: Resource.StatsSyncConfig.dataset,
}
const periods = retentionPeriods(periodStart, periodEnd)
if (periods.length === 0) return []
return [
{
cohortDates: periods.map((period) => period.start.toISOString().slice(0, 10)),
query: buildRetentionQuery(periods, source),
},
]
}
function buildRetentionQuery(
periods: { start: Date; end: Date; returnStart: Date; returnEnd: Date }[],
source: StatsQuerySource,
) {
const first = periods[0]
const last = periods.at(-1)!
const scanStartValue = sqlString(first.start.toISOString())
const scanEndValue = sqlString(last.returnEnd.toISOString())
const ingestEndValue = sqlString(new Date(last.returnEnd.getTime() + DAY_MS).toISOString())
const sourceTable = [source.namespace, source.table].map(sqlIdentifier).join(".")
const activityWeeks = [
...new Map(
periods.flatMap((period) => [period.start, period.returnStart]).map((date) => [date.toISOString(), date]),
).values(),
].toSorted((a, b) => a.getTime() - b.getTime())
const activityWeekSql = `CASE
${activityWeeks
.map(
(date) =>
` WHEN started_at >= ${sqlString(date.toISOString())} AND started_at < ${sqlString(new Date(date.getTime() + WEEK_MS).toISOString())} THEN ${sqlString(date.toISOString().slice(0, 10))}`,
)
.join("\n")}
ELSE null
END`
const cohortDates = periods.map((period) => sqlString(period.start.toISOString().slice(0, 10))).join(", ")
const returnDates = periods.map((period) => sqlString(period.returnStart.toISOString().slice(0, 10))).join(", ")
const returnCohortSql = `CASE activity_week
${periods
.map(
(period) =>
` WHEN ${sqlString(period.returnStart.toISOString().slice(0, 10))} THEN ${sqlString(period.start.toISOString().slice(0, 10))}`,
)
.join("\n")}
END`
return `
WITH normalized AS (
SELECT
${activityWeekSql} AS activity_week,
${statModelSql("model_requested", "route_model")} AS model,
COALESCE(NULLIF(route_model, ''), '') AS provider_model,
COALESCE(NULLIF(provider_id, ''), '') AS raw_provider,
COALESCE(NULLIF(user_id, ''), NULLIF(workspace_id, ''), NULLIF(service_api_key_id, '')) AS user_key
FROM ${sourceTable}
WHERE event_type = 'generation.completed'
AND source IN ('inference', 'inference-legacy')
AND (
(source = 'inference-legacy' AND started_at < ${sqlString(LIVE_SOURCE_START)})
OR (source = 'inference' AND started_at >= ${sqlString(LIVE_SOURCE_START)})
)
AND product = 'go'
AND model_requested IS NOT NULL
AND model_requested <> ''
AND __ingest_ts >= ${scanStartValue}
AND __ingest_ts < ${ingestEndValue}
AND started_at >= ${scanStartValue}
AND started_at < ${scanEndValue}
), filtered AS (
SELECT
activity_week,
${statProviderSql("model", "provider_model", "raw_provider")} AS provider,
model,
user_key
FROM normalized
WHERE activity_week IS NOT NULL
AND user_key <> ''
AND lower(model) NOT IN (${[...EXCLUDED_MODELS].map(sqlString).join(", ")})
), model_usage AS (
SELECT
activity_week AS cohort_date,
user_key,
provider,
model,
COUNT(*) AS model_requests
FROM filtered
WHERE activity_week IN (${cohortDates})
GROUP BY activity_week, user_key, provider, model
), user_totals AS (
SELECT
cohort_date,
user_key,
SUM(model_requests) AS total_requests,
MAX(model_requests) AS max_model_requests
FROM model_usage
GROUP BY cohort_date, user_key
), primary_models AS (
SELECT model_usage.cohort_date, model_usage.user_key, model_usage.provider, model_usage.model
FROM model_usage
INNER JOIN user_totals ON model_usage.cohort_date = user_totals.cohort_date
AND model_usage.user_key = user_totals.user_key
AND model_usage.model_requests = user_totals.max_model_requests
WHERE user_totals.total_requests >= 10
AND CAST(model_usage.model_requests AS double) / NULLIF(user_totals.total_requests, 0) >= 0.8
), returned AS (
SELECT
${returnCohortSql} AS cohort_date,
user_key
FROM filtered
WHERE activity_week IN (${returnDates})
GROUP BY ${returnCohortSql}, user_key
)
SELECT
primary_models.cohort_date,
${sqlString(source.dataset)} AS dataset,
'Go' AS tier,
primary_models.provider,
primary_models.model,
COUNT(*) AS eligible_users,
SUM(CASE WHEN returned.user_key IS NULL THEN 0 ELSE 1 END) AS retained_users
FROM primary_models
LEFT JOIN returned ON primary_models.user_key = returned.user_key
AND primary_models.cohort_date = returned.cohort_date
GROUP BY primary_models.cohort_date, primary_models.provider, primary_models.model
LIMIT 10000
`
}
function buildStatsQuery(
period: { grain: "day" | "week"; key: string; start: Date; end: Date },
source: StatsQuerySource,
@@ -223,6 +358,21 @@ export function toGeoAggregate(data: R2SqlData): GeoStatAggregate[] {
])
}
export function toRetentionAggregate(data: R2SqlData): RetentionStatAggregate[] {
if (!data.cohort_date || !data.model) return []
return [
{
cohortDate: data.cohort_date,
dataset: data.dataset || Resource.StatsSyncConfig.dataset,
tier: data.tier || "all",
provider: statProvider(data.model, "", data.provider) || "unknown",
model: statModel(data.model, undefined),
eligibleUsers: integer(data, "eligible_users"),
retainedUsers: integer(data, "retained_users"),
},
]
}
function toStatBaseAggregate(data: R2SqlData): StatBaseAggregate[] {
const grain = data.grain === "day" || data.grain === "week" ? data.grain : undefined
if (!grain || !data.period_key) return []
@@ -300,6 +450,17 @@ function statPeriods(grain: "day" | "week", periodStart: Date, periodEnd: Date)
})
}
function retentionPeriods(periodStart: Date, periodEnd: Date) {
const first = startOfIsoWeek(periodStart)
const completeEnd = startOfIsoWeek(periodEnd)
const count = Math.max(0, Math.floor((completeEnd.getTime() - first.getTime()) / WEEK_MS) - 1)
return Array.from({ length: count }, (_, index) => {
const start = new Date(first.getTime() + index * WEEK_MS)
const end = new Date(start.getTime() + WEEK_MS)
return { start, end, returnStart: end, returnEnd: new Date(end.getTime() + WEEK_MS) }
})
}
function statModelSql(model: string, providerModel: string) {
return `COALESCE(NULLIF(regexp_replace(CASE
WHEN lower(${model}) = 'big-pickle' THEN regexp_replace(NULLIF(${providerModel}, ''), '^.*/', '')
+110
View File
@@ -0,0 +1,110 @@
import { and, eq, inArray } from "drizzle-orm"
import { Context, Effect, Layer } from "effect"
import { DatabaseError, DrizzleClient } from "../database"
import { modelRetention } from "../database/schema"
import { chunks, UPSERT_CHUNK_SIZE } from "./stat"
export type RetentionStatRow = typeof modelRetention.$inferInsert
export type RetentionStatAggregate = {
cohortDate: string
dataset: string
tier: string
provider: string
model: string
eligibleUsers: number
retainedUsers: number
}
export declare namespace RetentionStatRepo {
export interface Service {
readonly available: () => Effect.Effect<boolean, DatabaseError>
readonly replace: (
rows: RetentionStatRow[],
scope: { cohortDates: string[]; dataset: string; tier: string },
) => Effect.Effect<void, DatabaseError>
}
}
export class RetentionStatRepo extends Context.Service<RetentionStatRepo, RetentionStatRepo.Service>()(
"@opencode/stats/RetentionStatRepo",
) {
static readonly layer: Layer.Layer<RetentionStatRepo, never, DrizzleClient> = Layer.effect(
RetentionStatRepo,
Effect.gen(function* () {
const db = yield* DrizzleClient
const available = Effect.fn("RetentionStatRepo.available")(function* () {
return yield* Effect.tryPromise({
try: async () => {
try {
await db.select({ id: modelRetention.id }).from(modelRetention).limit(1)
return true
} catch (cause) {
if (isMissingRetentionTable(cause)) return false
throw cause
}
},
catch: (cause) => DatabaseError.make({ cause }),
})
})
const replace = Effect.fn("RetentionStatRepo.replace")(function* (
rows: RetentionStatRow[],
scope: { cohortDates: string[]; dataset: string; tier: string },
) {
if (scope.cohortDates.length === 0) return
yield* Effect.tryPromise({
try: () =>
db
.delete(modelRetention)
.where(
and(
inArray(modelRetention.cohort_date, scope.cohortDates),
eq(modelRetention.dataset, scope.dataset),
eq(modelRetention.tier, scope.tier),
),
),
catch: (cause) => DatabaseError.make({ cause }),
})
yield* Effect.forEach(
chunks(rows, UPSERT_CHUNK_SIZE),
(chunk) =>
Effect.tryPromise({
try: () => db.insert(modelRetention).values(chunk),
catch: (cause) => DatabaseError.make({ cause }),
}),
{ discard: true },
)
})
return RetentionStatRepo.of({ available, replace })
}),
)
}
export function rowsFromAggregates(aggregates: RetentionStatAggregate[]): RetentionStatRow[] {
return aggregates.map((row) => ({
cohort_date: row.cohortDate,
dataset: row.dataset,
tier: row.tier,
provider: row.provider,
model: row.model,
eligible_users: row.eligibleUsers,
retained_users: row.retainedUsers,
}))
}
export function isMissingRetentionTable(cause: unknown): boolean {
const text = errorText(cause).toLowerCase()
return text.includes("model_retention") && text.includes("exist")
}
function errorText(cause: unknown): string {
if (cause instanceof Error) return `${cause.message} ${errorText((cause as { cause?: unknown }).cause)}`
if (typeof cause === "object" && cause)
return Object.values(cause as Record<string, unknown>)
.map(errorText)
.join(" ")
return String(cause)
}
+1
View File
@@ -6,6 +6,7 @@ export * as StatsHome from "./domain/home"
export * as Inference from "./domain/inference"
export * as ModelStat from "./domain/model"
export * as ProviderStat from "./domain/provider"
export * as RetentionStat from "./domain/retention"
export * as Stat from "./domain/stat"
export * as Runtime from "./runtime"
export * as StatSync from "./stat-sync"
+7 -3
View File
@@ -4,10 +4,14 @@ import { layer as databaseLayer } from "./database"
import { GeoStatRepo } from "./domain/geo"
import { ModelStatRepo } from "./domain/model"
import { ProviderStatRepo } from "./domain/provider"
import { RetentionStatRepo } from "./domain/retention"
const repoLayer = Layer.mergeAll(ModelStatRepo.layer, ProviderStatRepo.layer, GeoStatRepo.layer).pipe(
Layer.provide(databaseLayer),
)
const repoLayer = Layer.mergeAll(
ModelStatRepo.layer,
ProviderStatRepo.layer,
GeoStatRepo.layer,
RetentionStatRepo.layer,
).pipe(Layer.provide(databaseLayer))
export const layer = Layer.mergeAll(AppConfig.layer, databaseLayer, repoLayer)
export const runtime = ManagedRuntime.make(layer)
+53 -9
View File
@@ -2,16 +2,27 @@ import { DateTime, Effect } from "effect"
import { Resource } from "sst/resource"
import { DatabaseError } from "./database"
import { GeoStatRepo, rowsFromAggregates as geoRowsFromAggregates } from "./domain/geo"
import { buildStatsQueries, toGeoAggregate, toModelAggregate, toProviderAggregate } from "./domain/inference"
import {
buildRetentionQueries,
buildStatsQueries,
toGeoAggregate,
toModelAggregate,
toProviderAggregate,
toRetentionAggregate,
} from "./domain/inference"
import { ModelStatRepo, rowsFromAggregates as modelRowsFromAggregates } from "./domain/model"
import { ProviderStatRepo, rowsFromAggregates as providerRowsFromAggregates } from "./domain/provider"
import { startOfIsoWeek } from "./domain/stat"
import { RetentionStatRepo, rowsFromAggregates as retentionRowsFromAggregates } from "./domain/retention"
import { startOfIsoWeek, startOfUtcDay } from "./domain/stat"
import { R2Sql, R2SqlQueryError } from "./r2-sql"
const DATALAKE_INGESTION_LAG_MS = 5 * 60_000
const STATS_DATA_START_MS = new Date("2026-05-28T00:00:00.000Z").getTime()
const WEEK_MS = 7 * 86_400_000
const DISPLAY_WINDOW_MS = 56 * 86_400_000
// A retention result needs one complete activity week plus its complete return
// week. Keep another partial week of slack around the ISO-week boundary.
const RETENTION_INCREMENTAL_LOOKBACK_MS = 16 * 86_400_000
// Anchor incremental passes to the ISO week containing this lookback, so the pass
// after a week boundary still recomputes the previous week's final aggregates even
// if the boundary pass itself failed.
@@ -19,11 +30,12 @@ const INCREMENTAL_LOOKBACK_MS = 2 * 3_600_000
export type SyncStatsResult = { ok: true; rows: number; startedAt: string; periodStart: string; periodEnd: string }
export type SyncStatsError = R2SqlQueryError | DatabaseError
type SyncStatsServices = R2Sql | ModelStatRepo | ProviderStatRepo | GeoStatRepo | RetentionStatRepo
export const syncStats: (options?: {
full?: boolean
}) => Effect.Effect<SyncStatsResult, SyncStatsError, R2Sql | ModelStatRepo | ProviderStatRepo | GeoStatRepo> =
Effect.fn("StatSync.sync")(function* (options?: { full?: boolean }) {
}) => Effect.Effect<SyncStatsResult, SyncStatsError, SyncStatsServices> = Effect.fn("StatSync.sync")(
function* (options?: { full?: boolean }) {
const startedAt = yield* DateTime.nowAsDate
const periodEnd = new Date(Math.floor((startedAt.getTime() - DATALAKE_INGESTION_LAG_MS) / 60_000) * 60_000)
const periodStart = options?.full ? fullPeriodStart(periodEnd) : incrementalPeriodStart(periodEnd)
@@ -31,6 +43,7 @@ export const syncStats: (options?: {
const modelStats = yield* ModelStatRepo
const providerStats = yield* ProviderStatRepo
const geoStats = yield* GeoStatRepo
const retentionStats = yield* RetentionStatRepo
yield* logRuntimeCheck()
@@ -44,11 +57,39 @@ export const syncStats: (options?: {
const geoRows = geoRowsFromAggregates(
rows.filter((row) => row.dimension === "geo" || row.dimension === "geo_model").flatMap(toGeoAggregate),
)
const retentionAvailable = yield* retentionStats.available()
const retentionQueries = retentionAvailable
? buildRetentionQueries(
options?.full
? periodStart
: new Date(
Math.max(startOfUtcDay(periodEnd).getTime() - RETENTION_INCREMENTAL_LOOKBACK_MS, STATS_DATA_START_MS),
),
startOfUtcDay(periodEnd),
)
: []
const retentionRows = retentionRowsFromAggregates(
yield* Effect.forEach(retentionQueries, (item) => r2Sql.query(item.query), { concurrency: 4 }).pipe(
Effect.map((batches) => batches.flatMap((batch) => batch.flatMap(toRetentionAggregate))),
),
)
yield* Effect.all([modelStats.upsert(modelRows), providerStats.upsert(providerRows), geoStats.upsert(geoRows)], {
concurrency: "unbounded",
discard: true,
})
yield* Effect.all(
[
modelStats.upsert(modelRows),
providerStats.upsert(providerRows),
geoStats.upsert(geoRows),
retentionStats.replace(retentionRows, {
cohortDates: retentionQueries.flatMap((item) => item.cohortDates),
dataset: Resource.StatsSyncConfig.dataset,
tier: "Go",
}),
],
{
concurrency: "unbounded",
discard: true,
},
)
yield* Effect.all(
[
modelStats.deleteRetiredDimensions(modelRows),
@@ -66,6 +107,8 @@ export const syncStats: (options?: {
rows: modelRows.length,
providerRows: providerRows.length,
geoRows: geoRows.length,
retentionRows: retentionRows.length,
retentionAvailable,
stage: Resource.App.stage,
})}`,
)
@@ -77,7 +120,8 @@ export const syncStats: (options?: {
periodStart: periodStart.toISOString(),
periodEnd: periodEnd.toISOString(),
}
})
},
)
// May 27 was partial, so keep stats anchored at the first complete day.
function fullPeriodStart(periodEnd: Date) {
+14 -14
View File
@@ -49,7 +49,8 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
تشمل قائمة النماذج الحالية:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -71,7 +72,6 @@ OpenCode Go هو اشتراك منخفض التكلفة بقيمة **$10/شهر
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (لفترة محدودة)
قد تتغير قائمة النماذج مع استمرارنا في اختبار نماذج جديدة وإضافتها.
@@ -91,8 +91,9 @@ 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-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -113,11 +114,11 @@ 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-Flash — 1,000 input، و55,000 cached، و200 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,9 +142,11 @@ 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -170,14 +173,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,8 +212,9 @@ 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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -235,7 +236,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,8 +255,9 @@ https://opencode.ai/zen/go/v1/models
| النموذج | تدريب النموذج | الاحتفاظ بالبيانات |
| ---------------------------- | ------------- | ------------------ |
| Grok 4.5 | غير مستخدَمة | 30 يومًا |
| Grok 4.6 | غير مستخدَمة | 30 يومًا |
| GPT 5.6 Luna | غير مستخدَمة | 30 يومًا |
| GLM-5.3-Flash | غير مستخدَمة | 0 أيام |
| GLM-5.3 | غير مستخدَمة | 0 أيام |
| GLM-5.2 | غير مستخدَمة | 0 أيام |
| GLM-5.1 | غير مستخدَمة | 0 أيام |
@@ -277,9 +278,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> إذا كانت لديك أي أسئلة.
+14 -14
View File
@@ -59,7 +59,8 @@ 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-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +82,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,8 +101,9 @@ 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-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -123,11 +124,11 @@ 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-Flash — 1,000 ulaznih (input), 55,000 keširanih, 200 izlaznih (output) 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,9 +152,11 @@ 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -180,14 +183,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,8 +224,9 @@ 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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +248,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,8 +269,9 @@ 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-Flash | Ne koristi se | 0 dana |
| GLM-5.3 | Ne koristi se | 0 dana |
| GLM-5.2 | Ne koristi se | 0 dana |
| GLM-5.1 | Ne koristi se | 0 dana |
@@ -291,9 +292,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.
+1
View File
@@ -701,6 +701,7 @@ OpenCode can be configured using environment variables.
| `OPENCODE_FAKE_VCS` | string | Fake VCS provider for testing purposes |
| `OPENCODE_CLIENT` | string | Client identifier (defaults to `cli`) |
| `OPENCODE_ENABLE_EXA` | boolean | Enable Exa web search tools |
| `OPENCODE_ENABLE_PARALLEL` | boolean | Enable Parallel web search tools |
| `OPENCODE_SERVER_PASSWORD` | string | Enable basic auth for `serve`/`web` |
| `OPENCODE_SERVER_USERNAME` | string | Override basic auth username (default `opencode`) |
| `OPENCODE_MODELS_URL` | string | Custom URL for fetching models configuration |
+14 -14
View File
@@ -59,7 +59,8 @@ Kun ét medlem per arbejdsområde kan abonnere på OpenCode Go.
Den nuværende liste over modeller inkluderer:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +82,6 @@ Den nuværende liste over modeller inkluderer:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (i en begrænset periode)
Listen over modeller kan ændre sig, efterhånden som vi tester og tilføjer nye.
@@ -101,8 +101,9 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo
| Model | anmodninger pr. 5 timer | anmodninger pr. uge | anmodninger pr. måned |
| ---------------------------- | ----------------------- | ------------------- | --------------------- |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -123,11 +124,11 @@ Tabellen nedenfor giver et estimeret antal anmodninger baseret på typiske Go-fo
| 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 | - | - | - |
Estimaterne er baseret på observerede anmodningsmønstre:
- Grok 4.51.100 input, 71.500 cachelagrede, 220 output-tokens pr. anmodning
- Grok 4.6390 input, 32.500 cachelagrede, 120 output-tokens pr. anmodning
- GLM-5.3-Flash — 1.000 input, 55.000 cachelagrede, 200 output-tokens pr. anmodning
- GLM-5.3/5.2/5.1 — 700 input, 52.000 cachelagrede, 150 output-tokens pr. anmodning
- GPT 5.6 Luna — 1.000 input, 50.000 cachelagrede, 220 output-tokens pr. anmodning
- Kimi K3 — 1.050 input, 76.500 cachelagrede, 300 output-tokens pr. anmodning
@@ -151,9 +152,11 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig
| Model | Input | Output | Cached Read | Cached Write | Forbrug |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ------- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -180,14 +183,11 @@ Estimaterne er også baseret på følgende priser pr. 1M tokens og det månedlig
| 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-tiderne er 01:00-04:00 og 06:00-10:00 UTC fra mandag til fredag; alle andre tider, herunder weekender, er Off-Peak. [Læs mere](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Billeder konverteres til tokens baseret på deres dimensioner og afregnes som inputtokens sammen med teksttokens. [Læs mere](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Gratis i en begrænset periode.
Du kan spore dit nuværende forbrug i **<a href={console}>konsollen</a>**.
:::tip
@@ -224,8 +224,9 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints.
| Model | Model ID | Endpoint | AI SDK Package |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +248,6 @@ Du kan også få adgang til Go-modeller gennem følgende API-endpoints.
| 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` |
Dit [model id](/docs/config/#models) i din OpenCode config
bruger formatet `opencode-go/<model-id>`. For eksempel for Kimi K3, vil du
@@ -269,8 +269,9 @@ https://opencode.ai/zen/go/v1/models
| Model | Modeltræning | Dataopbevaring |
| ---------------------------- | ------------ | -------------- |
| Grok 4.5 | Ikke brugt | 30 dage |
| Grok 4.6 | Ikke brugt | 30 dage |
| GPT 5.6 Luna | Ikke brugt | 30 dage |
| GLM-5.3-Flash | Ikke brugt | 0 dage |
| GLM-5.3 | Ikke brugt | 0 dage |
| GLM-5.2 | Ikke brugt | 0 dage |
| GLM-5.1 | Ikke brugt | 0 dage |
@@ -291,9 +292,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Ikke brugt | 0 dage |
| DeepSeek V4 Flash Vision Exp | Ikke brugt | 0 dage |
| Hy3 | Ikke brugt | 0 dage |
| Ox Alpha Free | Ikke brugt | 0 dage |
- **Grok 4.5:** ZDR deaktiverer vigtige API-funktioner, der afhænger af lagrede data, herunder den tilstandsbevarende Responses API, Files and Collections og Batch API. [Læs mere](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR deaktiverer vigtige API-funktioner, der afhænger af lagrede data, herunder den tilstandsbevarende Responses API, Files and Collections og Batch API. [Læs mere](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Logfiler til overvågning af misbrug genereres ved al brug af API-funktioner og opbevares i op til 30 dage. [Læs mere](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Kraftigt nedsatte tokenpriser til gengæld for tilladelse til at bruge dine prompts og modelsvar til at træne fremtidige Meta-modeller. Tilgængeligheden er begrænset til regioner, der er tilladt i henhold til [politikken for geografisk brug](https://ai.developer.meta.com/legal/geographic-use-policy) fra Meta. [Læs mere](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** ZDR-aftalen fornyes månedligt. Den nuværende aftale er gyldig til og med 31. august 2026.
-3
View File
@@ -117,7 +117,6 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
| 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 @@ Vi understøtter en pay-as-you-go-model. Nedenfor er priserne **pr. 1M tokens**.
| 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 @@ De gratis modeller:
- Nemotron 3 Ultra Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- Nemotron 3.5 Lightning Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- Big Pickle er en stealth-model, som er gratis på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
- Ox Alpha Free er en stealth-model, som er gratis på OpenCode i en begrænset periode. Udbyderen følger en nul-opbevaringspolitik og bruger ikke dine data til at træne modeller.
- Muse Spark 1.2 Contributor Free er tilgængelig på OpenCode i en begrænset periode. Teamet bruger denne tid til at indsamle feedback og forbedre modellen.
<a href={email}>Kontakt os</a>, hvis du har spørgsmål.
+14 -14
View File
@@ -51,7 +51,8 @@ Nur ein Mitglied pro Workspace kann OpenCode Go abonnieren.
Die aktuelle Liste der Modelle umfasst:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -73,7 +74,6 @@ Die aktuelle Liste der Modelle umfasst:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (für begrenzte Zeit)
Die Liste der Modelle kann sich ändern, während wir neue testen und hinzufügen.
@@ -93,8 +93,9 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty
| Model | Anfragen pro 5 Stunden | Anfragen pro Woche | Anfragen pro Monat |
| ---------------------------- | ---------------------- | ------------------ | ------------------ |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -115,11 +116,11 @@ Die folgende Tabelle zeigt eine geschätzte Anzahl von Anfragen basierend auf ty
| 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 | - | - | - |
Die Schätzungen basieren auf beobachteten Anfragemustern:
- Grok 4.51.100 Input-, 71.500 Cached-, 220 Output-Tokens pro Anfrage
- Grok 4.6390 Input-, 32.500 Cached-, 120 Output-Tokens pro Anfrage
- GLM-5.3-Flash — 1.000 Input-, 55.000 Cached-, 200 Output-Tokens pro Anfrage
- GLM-5.3/5.2/5.1 — 700 Input-, 52.000 Cached-, 150 Output-Tokens pro Anfrage
- GPT 5.6 Luna — 1.000 Input-, 50.000 Cached-, 220 Output-Tokens pro Anfrage
- Kimi K3 — 1.050 Input-, 76.500 Cached-, 300 Output-Tokens pro Anfrage
@@ -143,9 +144,11 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und
| Model | Input | Output | Cached Read | Cached Write | Nutzung |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ------- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -172,14 +175,11 @@ Die Schätzungen basieren außerdem auf den folgenden Preisen pro 1M Tokens und
| 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:** Die Peak-Zeiten sind montags bis freitags von 01:00-04:00 und 06:00-10:00 UTC; alle anderen Zeiten, einschließlich der Wochenenden, sind Off-Peak. [Mehr erfahren](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Bilder werden anhand ihrer Abmessungen in Tokens umgewandelt und zusammen mit Text-Tokens als Input-Tokens abgerechnet. [Mehr erfahren](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Für begrenzte Zeit kostenlos.
Du kannst deine aktuelle Nutzung in der **<a href={console}>Console</a>** verfolgen.
:::tip
@@ -214,8 +214,9 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen.
| Modell | Modell-ID | Endpunkt | AI SDK Package |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -237,7 +238,6 @@ Du kannst auf die Go-Modelle auch über die folgenden API-Endpunkte zugreifen.
| 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` |
Die [Modell-ID](/docs/config/#models) in deiner OpenCode Config verwendet das Format `opencode-go/<model-id>`. Für Kimi K3 würdest du beispielsweise `opencode-go/kimi-k3` in deiner Config verwenden.
@@ -257,8 +257,9 @@ https://opencode.ai/zen/go/v1/models
| Modell | Modelltraining | Datenaufbewahrung |
| ---------------------------- | --------------- | ----------------- |
| Grok 4.5 | Nicht verwendet | 30 Tage |
| Grok 4.6 | Nicht verwendet | 30 Tage |
| GPT 5.6 Luna | Nicht verwendet | 30 Tage |
| GLM-5.3-Flash | Nicht verwendet | 0 Tage |
| GLM-5.3 | Nicht verwendet | 0 Tage |
| GLM-5.2 | Nicht verwendet | 0 Tage |
| GLM-5.1 | Nicht verwendet | 0 Tage |
@@ -279,9 +280,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Nicht verwendet | 0 Tage |
| DeepSeek V4 Flash Vision Exp | Nicht verwendet | 0 Tage |
| Hy3 | Nicht verwendet | 0 Tage |
| Ox Alpha Free | Nicht verwendet | 0 Tage |
- **Grok 4.5:** ZDR deaktiviert wichtige API-Funktionen, die von gespeicherten Daten abhängen, einschließlich der zustandsbehafteten Responses API, Files and Collections und der Batch API. [Mehr erfahren](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR deaktiviert wichtige API-Funktionen, die von gespeicherten Daten abhängen, einschließlich der zustandsbehafteten Responses API, Files and Collections und der Batch API. [Mehr erfahren](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Für die Nutzung aller API-Funktionen werden Protokolle zur Missbrauchsüberwachung erstellt und bis zu 30 Tage lang aufbewahrt. [Mehr erfahren](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Stark vergünstigte Tokenpreise im Gegenzug für die Erlaubnis, deine Prompts und Vervollständigungen zum Trainieren zukünftiger Meta-Modelle zu verwenden. Die Verfügbarkeit ist auf Regionen beschränkt, die gemäß der [Richtlinie zur geografischen Nutzung](https://ai.developer.meta.com/legal/geographic-use-policy) von Meta zulässig sind. [Mehr erfahren](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** Die ZDR-Vereinbarung wird monatlich erneuert. Die aktuelle Vereinbarung gilt bis einschließlich 31. August 2026.
-3
View File
@@ -108,7 +108,6 @@ Du kannst auch über die folgenden API-Endpunkte auf unsere Modelle zugreifen.
| 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` |
@@ -136,7 +135,6 @@ Wir unterstützen ein Pay-as-you-go-Modell. Unten findest du die Preise **pro 1M
| 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 | - |
@@ -227,7 +225,6 @@ Die kostenlosen Modelle:
- Nemotron 3 Ultra Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- Nemotron 3.5 Lightning Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- Big Pickle ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
- Ox Alpha Free ist ein Stealth-Modell, das für begrenzte Zeit kostenlos auf OpenCode verfügbar ist. Der Anbieter befolgt eine Zero-Retention-Richtlinie und verwendet deine Daten nicht zum Trainieren von Modellen.
- Muse Spark 1.2 Contributor Free ist für begrenzte Zeit auf OpenCode verfügbar. Das Team nutzt diese Zeit, um Feedback zu sammeln und das Modell zu verbessern.
<a href={email}>Kontaktiere uns</a>, wenn du Fragen hast.
+14 -14
View File
@@ -59,7 +59,8 @@ Solo un miembro por espacio de trabajo puede suscribirse a OpenCode Go.
La lista actual de modelos incluye:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +82,6 @@ La lista actual de modelos incluye:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (por tiempo limitado)
La lista de modelos puede cambiar a medida que probamos y agregamos otros nuevos.
@@ -101,8 +101,9 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los
| Model | peticiones por 5 horas | peticiones por semana | peticiones por mes |
| ---------------------------- | ---------------------- | --------------------- | ------------------ |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -123,11 +124,11 @@ La siguiente tabla proporciona una cantidad estimada de peticiones basada en los
| 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 | - | - | - |
Las estimaciones se basan en los patrones de peticiones observados:
- Grok 4.51,100 tokens de entrada, 71,500 en caché, 220 tokens de salida por petición
- Grok 4.6390 tokens de entrada, 32,500 en caché, 120 tokens de salida por petición
- GLM-5.3-Flash — 1,000 tokens de entrada, 55,000 en caché, 200 tokens de salida por petición
- GLM-5.3/5.2/5.1 — 700 tokens de entrada, 52,000 en caché, 150 tokens de salida por petición
- GPT 5.6 Luna — 1,000 tokens de entrada, 50,000 en caché, 220 tokens de salida por petición
- Kimi K3 — 1,050 tokens de entrada, 76,500 en caché, 300 tokens de salida por petición
@@ -151,9 +152,11 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en
| Modelo | Entrada | Salida | Lectura en caché | Escritura en caché | Uso |
| --------------------------------------- | ------- | ------ | ---------------- | ------------------ | --- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -180,14 +183,11 @@ Las estimaciones también se basan en los siguientes precios por 1M tokens y en
| 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:** Las horas Peak son 01:00-04:00 y 06:00-10:00 UTC, de lunes a viernes; todas las demás horas, incluidos los fines de semana, son Off-Peak. [Más información](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Las imágenes se convierten en tokens según sus dimensiones y se facturan como tokens de entrada junto con los tokens de texto. [Más información](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Gratis por tiempo limitado.
Puedes realizar un seguimiento de tu uso actual en la **<a href={console}>consola</a>**.
:::tip
@@ -224,8 +224,9 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint
| Modelo | ID del modelo | Endpoint | Paquete de AI SDK |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +248,6 @@ También puedes acceder a los modelos de Go a través de los siguientes endpoint
| 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` |
El [ID del modelo](/docs/config/#models) en tu configuración de OpenCode
usa el formato `opencode-go/<model-id>`. Por ejemplo, para Kimi K3, usarías
@@ -269,8 +269,9 @@ https://opencode.ai/zen/go/v1/models
| Modelo | Entrenamiento del modelo | Retención de datos |
| ---------------------------- | ------------------------ | ------------------ |
| Grok 4.5 | No utilizado | 30 días |
| Grok 4.6 | No utilizado | 30 días |
| GPT 5.6 Luna | No utilizado | 30 días |
| GLM-5.3-Flash | No utilizado | 0 días |
| GLM-5.3 | No utilizado | 0 días |
| GLM-5.2 | No utilizado | 0 días |
| GLM-5.1 | No utilizado | 0 días |
@@ -291,9 +292,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | No utilizado | 0 días |
| DeepSeek V4 Flash Vision Exp | No utilizado | 0 días |
| Hy3 | No utilizado | 0 días |
| Ox Alpha Free | No utilizado | 0 días |
- **Grok 4.5:** ZDR deshabilita funciones importantes de la API que dependen de datos almacenados, incluidas la Responses API con estado, Files and Collections y la Batch API. [Más información](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR deshabilita funciones importantes de la API que dependen de datos almacenados, incluidas la Responses API con estado, Files and Collections y la Batch API. [Más información](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Se generan registros de supervisión de abusos para todo el uso de funciones de la API y se conservan durante un máximo de 30 días. [Más información](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Precios de tokens muy reducidos a cambio de permitir que tus prompts y las respuestas generadas se utilicen para entrenar futuros modelos de Meta. La disponibilidad está limitada a las regiones permitidas por la [Política de uso geográfico](https://ai.developer.meta.com/legal/geographic-use-policy) de Meta. [Más información](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** El acuerdo de ZDR se renueva mensualmente. El acuerdo actual es válido hasta el 31 de agosto de 2026.
-3
View File
@@ -117,7 +117,6 @@ También puedes acceder a nuestros modelos a través de los siguientes endpoints
| 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 @@ Admitimos un modelo de pago por uso. A continuación se muestran los precios **p
| Modelo | Entrada | Salida | Lectura en caché | Escritura en caché |
| --------------------------------- | ------- | ------- | ---------------- | ------------------ |
| 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 @@ Los modelos gratuitos:
- Nemotron 3 Ultra Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- Nemotron 3.5 Lightning Free está disponible en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- Big Pickle es un modelo stealth que es gratuito en OpenCode por tiempo limitado. El equipo está usando este tiempo para recopilar comentarios y mejorar el modelo.
- Ox Alpha Free es un modelo stealth que es gratuito en OpenCode por tiempo limitado. Su proveedor sigue una política de retención cero y no utiliza tus datos para entrenar modelos.
- Muse Spark 1.2 Contributor Free está disponible en OpenCode por tiempo limitado. El equipo está aprovechando este período para recopilar comentarios y mejorar el modelo.
<a href={email}>Contáctanos</a> si tienes alguna pregunta.
+14 -14
View File
@@ -49,7 +49,8 @@ Un seul membre par espace de travail peut s'abonner à OpenCode Go.
La liste actuelle des modèles comprend :
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -71,7 +72,6 @@ La liste actuelle des modèles comprend :
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (pour une durée limitée)
La liste des modèles peut changer au fur et à mesure que nous en testons et en ajoutons de nouveaux.
@@ -91,8 +91,9 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d
| Model | requêtes par 5 heures | requêtes par semaine | requêtes par mois |
| ---------------------------- | --------------------- | -------------------- | ----------------- |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -113,11 +114,11 @@ Le tableau ci-dessous fournit une estimation du nombre de requêtes basée sur d
| 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 | - | - | - |
Les estimations sont basées sur les schémas de requêtes observés :
- Grok 4.51,100 tokens en entrée, 71,500 en cache, 220 tokens en sortie par requête
- Grok 4.6390 tokens en entrée, 32,500 en cache, 120 tokens en sortie par requête
- GLM-5.3-Flash — 1,000 tokens en entrée, 55,000 en cache, 200 tokens en sortie par requête
- GLM-5.3/5.2/5.1 — 700 tokens en entrée, 52,000 en cache, 150 tokens en sortie par requête
- GPT 5.6 Luna — 1,000 tokens en entrée, 50,000 en cache, 220 tokens en sortie par requête
- Kimi K3 — 1,050 tokens en entrée, 76,500 en cache, 300 tokens en sortie par requête
@@ -141,9 +142,11 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s
| Modèle | Input | Output | Cached Read | Cached Write | Utilisation |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ----------- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -170,14 +173,11 @@ Les estimations sont également basées sur les prix suivants par 1M tokens et s
| 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:** Les heures Peak sont 01:00-04:00 et 06:00-10:00 UTC, du lundi au vendredi ; toutes les autres heures, y compris le week-end, sont Off-Peak. [En savoir plus](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Les images sont converties en tokens selon leurs dimensions et facturées comme tokens dentrée avec les tokens de texte. [En savoir plus](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Gratuit pour une durée limitée.
Vous pouvez suivre votre utilisation actuelle dans la **<a href={console}>console</a>**.
:::tip
@@ -212,8 +212,9 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d'
| Modèle | ID de modèle | Point de terminaison | Package AI SDK |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -235,7 +236,6 @@ Vous pouvez également accéder aux modèles Go via les points de terminaison d'
| 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` |
L'[ID de modèle](/docs/config/#models) dans votre configuration OpenCode utilise le format `opencode-go/<model-id>`. Par exemple, pour Kimi K3, vous utiliseriez `opencode-go/kimi-k3` dans votre configuration.
@@ -255,8 +255,9 @@ https://opencode.ai/zen/go/v1/models
| Modèle | Entraînement des modèles | Conservation des données |
| ---------------------------- | ------------------------ | ------------------------ |
| Grok 4.5 | Non utilisé | 30 jours |
| Grok 4.6 | Non utilisé | 30 jours |
| GPT 5.6 Luna | Non utilisé | 30 jours |
| GLM-5.3-Flash | Non utilisé | 0 jour |
| GLM-5.3 | Non utilisé | 0 jour |
| GLM-5.2 | Non utilisé | 0 jour |
| GLM-5.1 | Non utilisé | 0 jour |
@@ -277,9 +278,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Non utilisé | 0 jour |
| DeepSeek V4 Flash Vision Exp | Non utilisé | 0 jour |
| Hy3 | Non utilisé | 0 jour |
| Ox Alpha Free | Non utilisé | 0 jour |
- **Grok 4.5:** Le ZDR désactive dimportantes fonctionnalités API qui dépendent des données stockées, notamment Responses API avec état, Files and Collections et Batch API. [En savoir plus](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** Le ZDR désactive dimportantes fonctionnalités API qui dépendent des données stockées, notamment Responses API avec état, Files and Collections et Batch API. [En savoir plus](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Des journaux de surveillance des abus sont générés pour toute utilisation des fonctionnalités API et conservés pendant un maximum de 30 jours. [En savoir plus](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Des tarifs de tokens fortement réduits en échange de lautorisation dutiliser vos prompts et vos complétions pour entraîner de futurs modèles Meta. La disponibilité est limitée aux régions autorisées par la [Politique dutilisation géographique](https://ai.developer.meta.com/legal/geographic-use-policy) de Meta. [En savoir plus](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** Laccord ZDR est renouvelé chaque mois. Laccord actuel est valable jusquau 31 août 2026.
-3
View File
@@ -108,7 +108,6 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP
| 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` |
@@ -136,7 +135,6 @@ Nous prenons en charge un modèle de paiement à l'utilisation. Vous trouverez c
| Modèle | 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 | - |
@@ -227,7 +225,6 @@ Les modèles gratuits :
- Nemotron 3 Ultra Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- Nemotron 3.5 Lightning Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- Big Pickle est un modèle stealth gratuit sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
- Ox Alpha Free est un modèle stealth gratuit sur OpenCode pour une durée limitée. Son fournisseur applique une politique de conservation nulle et n'utilise pas vos données pour entraîner des modèles.
- Muse Spark 1.2 Contributor Free est disponible sur OpenCode pour une durée limitée. L'équipe utilise cette période pour recueillir des retours et améliorer le modèle.
<a href={email}>Contactez-nous</a> si vous avez des questions.
+14 -14
View File
@@ -59,7 +59,8 @@ Only one member per workspace can subscribe to OpenCode Go.
The current list of models includes:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +82,6 @@ The current list of models includes:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (limited time)
The list of models may change as we test and add new ones.
@@ -101,8 +101,9 @@ The table below provides an estimated request count based on typical Go usage pa
| Model | requests per 5 hour | requests per week | requests per month |
| ---------------------------- | ------------------- | ----------------- | ------------------ |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -123,11 +124,11 @@ The table below provides an estimated request count based on typical Go usage pa
| 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 | - | - | - |
The estimates are based on observed request patterns:
- Grok 4.51,100 input, 71,500 cached, 220 output tokens per request
- Grok 4.6390 input, 32,500 cached, 120 output tokens per request
- GLM-5.3-Flash — 1,000 input, 55,000 cached, 200 output tokens per request
- GLM-5.3/5.2/5.1 — 700 input, 52,000 cached, 150 output tokens per request
- GPT 5.6 Luna — 1,000 input, 50,000 cached, 220 output tokens per request
- Kimi K3 — 1,050 input, 76,500 cached, 300 output tokens per request
@@ -151,9 +152,11 @@ The estimates are also based on the following prices per 1M tokens and the month
| Model | Input | Output | Cached Read | Cached Write | Usage |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ----- |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -180,14 +183,11 @@ The estimates are also based on the following prices per 1M tokens and the month
| 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 hours are 01:00-04:00 and 06:00-10:00 UTC, Monday through Friday; all other hours, including weekends, are Off-Peak. [Learn more](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Images are converted into tokens based on their dimensions and billed as input tokens alongside text tokens. [Learn more](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Free for a limited time.
You can track your current usage in the **<a href={console}>console</a>**.
:::tip
@@ -224,8 +224,9 @@ You can also access Go models through the following API endpoints.
| Model | Model ID | Endpoint | AI SDK Package |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +248,6 @@ You can also access Go models through the following API endpoints.
| 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` |
The [model id](/docs/config/#models) in your OpenCode config
uses the format `opencode-go/<model-id>`. For example, for Kimi K3, you would
@@ -269,8 +269,9 @@ https://opencode.ai/zen/go/v1/models
| Model | Model training | Data retention |
| ---------------------------- | -------------- | -------------- |
| Grok 4.5 | Not used | 30 days |
| Grok 4.6 | Not used | 30 days |
| GPT 5.6 Luna | Not used | 30 days |
| GLM-5.3-Flash | Not used | 0 days |
| GLM-5.3 | Not used | 0 days |
| GLM-5.2 | Not used | 0 days |
| GLM-5.1 | Not used | 0 days |
@@ -291,9 +292,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Not used | 0 days\* |
| DeepSeek V4 Flash Vision Exp | Not used | 0 days\* |
| Hy3 | Not used | 0 days |
| Ox Alpha Free | Not used | 0 days |
- **Grok 4.5:** ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API. [Learn more](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API. [Learn more](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days. [Learn more](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Heavily discounted token pricing in exchange for permission to use your prompts and completions to train future Meta models. Availability is limited to regions permitted by Meta's [Geographic Use Policy](https://ai.developer.meta.com/legal/geographic-use-policy). [Learn more](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek:** ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026.
+14 -14
View File
@@ -57,7 +57,8 @@ Solo un membro per workspace può abbonarsi a OpenCode Go.
L'elenco attuale dei modelli include:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -79,7 +80,6 @@ L'elenco attuale dei modelli include:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (per un periodo limitato)
L'elenco dei modelli potrebbe cambiare man mano che ne testiamo e aggiungiamo di nuovi.
@@ -99,8 +99,9 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p
| Model | richieste ogni 5 ore | richieste a settimana | richieste al mese |
| ---------------------------- | -------------------- | --------------------- | ----------------- |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -121,11 +122,11 @@ La tabella seguente fornisce una stima del conteggio delle richieste in base a p
| 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 | - | - | - |
Le stime si basano sui pattern di richieste osservati:
- Grok 4.51.100 di input, 71.500 in cache, 220 token di output per richiesta
- Grok 4.6390 di input, 32.500 in cache, 120 token di output per richiesta
- GLM-5.3-Flash — 1.000 di input, 55.000 in cache, 200 token di output per richiesta
- GLM-5.3/5.2/5.1 — 700 di input, 52.000 in cache, 150 token di output per richiesta
- GPT 5.6 Luna — 1.000 token di input, 50.000 in cache, 220 token di output per richiesta
- Kimi K3 — 1.050 di input, 76.500 in cache, 300 token di output per richiesta
@@ -149,9 +150,11 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil
| Modello | Input | Output | Cached Read | Cached Write | Utilizzo |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | -------- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -178,14 +181,11 @@ Le stime si basano anche sui seguenti prezzi per 1M token e sull'utilizzo mensil
| 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:** Gli orari Peak sono 01:00-04:00 e 06:00-10:00 UTC, dal lunedì al venerdì; tutti gli altri orari, inclusi i fine settimana, sono Off-Peak. [Scopri di più](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Le immagini vengono convertite in token in base alle loro dimensioni e fatturate come token di input insieme ai token di testo. [Scopri di più](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Gratis per un periodo limitato.
Puoi monitorare il tuo utilizzo attuale nella **<a href={console}>console</a>**.
:::tip
@@ -222,8 +222,9 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API.
| Modello | ID Modello | Endpoint | Pacchetto AI SDK |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -245,7 +246,6 @@ Puoi anche accedere ai modelli Go tramite i seguenti endpoint API.
| 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` |
Il [model id](/docs/config/#models) nella tua OpenCode config
utilizza il formato `opencode-go/<model-id>`. Ad esempio, per Kimi K3, useresti
@@ -267,8 +267,9 @@ https://opencode.ai/zen/go/v1/models
| Modello | Addestramento del modello | Conservazione dei dati |
| ---------------------------- | ------------------------- | ---------------------- |
| Grok 4.5 | Non utilizzato | 30 giorni |
| Grok 4.6 | Non utilizzato | 30 giorni |
| GPT 5.6 Luna | Non utilizzato | 30 giorni |
| GLM-5.3-Flash | Non utilizzato | 0 giorni |
| GLM-5.3 | Non utilizzato | 0 giorni |
| GLM-5.2 | Non utilizzato | 0 giorni |
| GLM-5.1 | Non utilizzato | 0 giorni |
@@ -289,9 +290,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Non utilizzato | 0 giorni |
| DeepSeek V4 Flash Vision Exp | Non utilizzato | 0 giorni |
| Hy3 | Non utilizzato | 0 giorni |
| Ox Alpha Free | Non utilizzato | 0 giorni |
- **Grok 4.5:** ZDR disabilita importanti funzionalità API che dipendono dai dati archiviati, tra cui la Responses API con stato, Files and Collections e Batch API. [Scopri di più](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR disabilita importanti funzionalità API che dipendono dai dati archiviati, tra cui la Responses API con stato, Files and Collections e Batch API. [Scopri di più](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** I log di monitoraggio degli abusi vengono generati per l'utilizzo di tutte le funzionalità API e conservati per un massimo di 30 giorni. [Scopri di più](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Prezzi dei token fortemente scontati in cambio dell'autorizzazione a utilizzare i tuoi prompt e completamenti per addestrare futuri modelli Meta. La disponibilità è limitata alle regioni consentite dalla [Politica sull'uso geografico](https://ai.developer.meta.com/legal/geographic-use-policy) di Meta. [Scopri di più](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** L'accordo ZDR viene rinnovato mensilmente. L'accordo attuale è valido fino al 31 agosto 2026.
-3
View File
@@ -117,7 +117,6 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API.
| 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 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
| Modello | 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 @@ I modelli gratuiti:
- Nemotron 3 Ultra Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- Nemotron 3.5 Lightning Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- Big Pickle è un modello stealth che è gratuito su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
- Ox Alpha Free è un modello stealth gratuito su OpenCode per un periodo limitato. Il suo provider segue una politica di conservazione zero e non usa i tuoi dati per addestrare modelli.
- Muse Spark 1.2 Contributor Free è disponibile su OpenCode per un periodo limitato. Il team usa questo periodo per raccogliere feedback e migliorare il modello.
<a href={email}>Contattaci</a> se hai domande.
+14 -14
View File
@@ -49,7 +49,8 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー
現在のモデルリストには以下が含まれます:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -71,7 +72,6 @@ OpenCode Goをサブスクライブできるのは、1つのワークスペー
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (期間限定)
新しいモデルをテストして追加するにつれて、モデルのリストは変更される場合があります。
@@ -91,8 +91,9 @@ OpenCode Goには以下の制限が含まれています:
| 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-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -113,11 +114,11 @@ OpenCode Goには以下の制限が含まれています:
| 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.5 — リクエストあたり 入力 1,100トークン、キャッシュ 71,500トークン、出力 220トークン
- Grok 4.6 — リクエストあたり 入力 390トークン、キャッシュ 32,500トークン、出力 120トークン
- GLM-5.3-Flash — リクエストあたり 入力 1,000トークン、キャッシュ 55,000トークン、出力 200トークン
- GLM-5.3/5.2/5.1 — リクエストあたり 入力 700トークン、キャッシュ 52,000トークン、出力 150トークン
- GPT 5.6 Luna — リクエストあたり 入力 1,000トークン、キャッシュ 50,000トークン、出力 220トークン
- Kimi K3 — リクエストあたり 入力 1,050トークン、キャッシュ 76,500トークン、出力 300トークン
@@ -141,9 +142,11 @@ OpenCode Goには以下の制限が含まれています:
| Model | Input | Output | Cached Read | Cached Write | Usage |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ----- |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -170,14 +173,11 @@ OpenCode Goには以下の制限が含まれています:
| 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}>コンソール</a>**で追跡できます。
:::tip
@@ -212,8 +212,9 @@ Goでは月額$10を支払い、その6倍の利用枠を提供することを
| 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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -235,7 +236,6 @@ Goでは月額$10を支払い、その6倍の利用枠を提供することを
| 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` |
OpenCode設定の[model id](/docs/config/#models)は、`opencode-go/<model-id>`という形式を使用します。たとえば、Kimi K3の場合は、設定で`opencode-go/kimi-k3`を使用します。
@@ -255,8 +255,9 @@ https://opencode.ai/zen/go/v1/models
| モデル | モデルのトレーニング | データ保持 |
| ---------------------------- | -------------------- | ----------- |
| Grok 4.5 | 使用なし | 30日 |
| Grok 4.6 | 使用なし | 30日 |
| GPT 5.6 Luna | 使用なし | 30日 |
| GLM-5.3-Flash | 使用なし | 0日 |
| GLM-5.3 | 使用なし | 0日 |
| GLM-5.2 | 使用なし | 0日 |
| GLM-5.1 | 使用なし | 0日 |
@@ -277,9 +278,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モデルのトレーニングにプロンプトと生成結果を使用する許可と引き換えに、トークン料金が大幅に割引されます。利用できるのは、Metaの[地域別利用ポリシー](https://ai.developer.meta.com/legal/geographic-use-policy)で許可されている地域に限られます。[詳しく見る](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier)。
- **DeepSeek V4 Flash:** ZDR契約は毎月更新されます。現在の契約は2026年8月31日まで有効です。
-3
View File
@@ -108,7 +108,6 @@ OpenCode Zen は、OpenCode のほかのプロバイダーと同じように動
| 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` |
@@ -136,7 +135,6 @@ https://opencode.ai/zen/v1/models
| 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 | - |
@@ -227,7 +225,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>。
+14 -14
View File
@@ -49,7 +49,8 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다.
현재 모델 목록에는 다음이 포함됩니다.
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -71,7 +72,6 @@ workspace당 한 명의 멤버만 OpenCode Go를 구독할 수 있습니다.
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (한정된 기간)
새로운 모델을 테스트하고 추가함에 따라 이 목록은 변경될 수 있습니다.
@@ -91,8 +91,9 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| 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-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -113,11 +114,11 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| 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.5 — 요청당 입력 1,100, 캐시 71,500, 출력 토큰 220
- Grok 4.6 — 요청당 입력 390, 캐시 32,500, 출력 토큰 120
- GLM-5.3-Flash — 요청당 입력 1,000, 캐시 55,000, 출력 토큰 200
- GLM-5.3/5.2/5.1 — 요청당 입력 700, 캐시 52,000, 출력 토큰 150
- GPT 5.6 Luna — 요청당 입력 토큰 1,000개, 캐시 토큰 50,000개, 출력 토큰 220개
- Kimi K3 — 요청당 입력 1,050, 캐시 76,500, 출력 토큰 300
@@ -141,9 +142,11 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| Model | Input | Output | Cached Read | Cached Write | Usage |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ----- |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -170,14 +173,11 @@ OpenCode Go에는 다음과 같은 한도가 포함됩니다.
| 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,8 +212,9 @@ Go에서는 월 $10를 지불하며, 저희는 그 6배의 사용량을 제공
| 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| 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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -235,7 +236,6 @@ Go에서는 월 $10를 지불하며, 저희는 그 6배의 사용량을 제공
| 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` |
OpenCode config의 [model id](/docs/config/#models)는 `opencode-go/<model-id>` 형식을 사용합니다. 예를 들어 Kimi K3의 경우 config에서 `opencode-go/kimi-k3`를 사용하면 됩니다.
@@ -255,8 +255,9 @@ https://opencode.ai/zen/go/v1/models
| 모델 | 모델 학습 | 데이터 보존 |
| ---------------------------- | ------------- | ----------- |
| Grok 4.5 | 사용되지 않음 | 30일 |
| Grok 4.6 | 사용되지 않음 | 30일 |
| GPT 5.6 Luna | 사용되지 않음 | 30일 |
| GLM-5.3-Flash | 사용되지 않음 | 0일 |
| GLM-5.3 | 사용되지 않음 | 0일 |
| GLM-5.2 | 사용되지 않음 | 0일 |
| GLM-5.1 | 사용되지 않음 | 0일 |
@@ -277,9 +278,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 모델 학습에 사용자의 프롬프트와 생성 결과를 사용할 수 있도록 허용하는 대신 토큰 가격이 대폭 할인됩니다. Meta의 [지역별 사용 정책](https://ai.developer.meta.com/legal/geographic-use-policy)에서 허용하는 지역에서만 이용할 수 있습니다. [자세히 알아보기](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** ZDR 계약은 매월 갱신됩니다. 현재 계약은 2026년 8월 31일까지 유효합니다.
-3
View File
@@ -108,7 +108,6 @@ OpenCode Zen은 OpenCode의 다른 provider와 똑같이 작동합니다.
| 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` |
@@ -136,7 +135,6 @@ https://opencode.ai/zen/v1/models
| 모델 | 입력 | 출력 | 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 | - |
@@ -227,7 +225,6 @@ https://opencode.ai/zen/v1/models
- Nemotron 3 Ultra Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- Nemotron 3.5 Lightning Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- Big Pickle은 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 팀은 이 기간에 피드백을 수집하고 모델을 개선합니다.
- Ox Alpha Free는 한정된 기간 동안 OpenCode에서 무료로 제공되는 stealth model입니다. 제공업체는 데이터 미보관 정책을 따르며 사용자의 데이터를 모델 학습에 사용하지 않습니다.
- Muse Spark 1.2 Contributor Free는 한정된 기간 동안 OpenCode에서 제공됩니다. 팀은 이 기간을 활용해 피드백을 수집하고 모델을 개선하고 있습니다.
궁금한 점이 있으면 <a href={email}>Contact us</a>로 문의해 주세요.
+14 -14
View File
@@ -59,7 +59,8 @@ Kun ett medlem per arbeidsområde kan abonnere på OpenCode Go.
Den nåværende listen over modeller inkluderer:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +82,6 @@ Den nåværende listen over modeller inkluderer:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (i en begrenset periode)
Listen over modeller kan endres etter hvert som vi tester og legger til nye.
@@ -101,8 +101,9 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm
| Model | forespørsler per 5 timer | forespørsler per uke | forespørsler per måned |
| ---------------------------- | ------------------------ | -------------------- | ---------------------- |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -123,11 +124,11 @@ Tabellen nedenfor gir et estimert antall forespørsler basert på typiske bruksm
| 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 | - | - | - |
Estimatene er basert på observerte forespørselsmønstre:
- Grok 4.51 100 input, 71 500 bufret, 220 output-tokens per forespørsel
- Grok 4.6390 input, 32 500 bufret, 120 output-tokens per forespørsel
- GLM-5.3-Flash — 1 000 input, 55 000 bufret, 200 output-tokens per forespørsel
- GLM-5.3/5.2/5.1 — 700 input, 52 000 bufret, 150 output-tokens per forespørsel
- GPT 5.6 Luna — 1 000 input, 50 000 bufret, 220 output-tokens per forespørsel
- Kimi K3 — 1 050 input, 76 500 bufret, 300 output-tokens per forespørsel
@@ -151,9 +152,11 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige
| Model | Input | Output | Cached Read | Cached Write | Bruk |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ---- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -180,14 +183,11 @@ Estimatene er også basert på følgende priser per 1M tokens og den månedlige
| 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-tidene er 01:00-04:00 og 06:00-10:00 UTC fra mandag til fredag; alle andre tider, inkludert helger, er Off-Peak. [Les mer](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Bilder konverteres til tokens basert på dimensjonene og faktureres som input-tokens sammen med tekst-tokens. [Les mer](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Gratis i en begrenset periode.
Du kan spore din nåværende bruk i **<a href={console}>konsollen</a>**.
:::tip
@@ -224,8 +224,9 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter.
| Modell | Modell-ID | Endepunkt | AI SDK Package |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +248,6 @@ Du kan også få tilgang til Go-modeller gjennom følgende API-endepunkter.
| 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` |
[Modell-ID-en](/docs/config/#models) i din OpenCode-konfigurasjon
bruker formatet `opencode-go/<model-id>`. For eksempel, for Kimi K3, vil du
@@ -269,8 +269,9 @@ https://opencode.ai/zen/go/v1/models
| Modell | Modelltrening | Dataoppbevaring |
| ---------------------------- | ------------- | --------------- |
| Grok 4.5 | Brukes ikke | 30 dager |
| Grok 4.6 | Brukes ikke | 30 dager |
| GPT 5.6 Luna | Brukes ikke | 30 dager |
| GLM-5.3-Flash | Brukes ikke | 0 dager |
| GLM-5.3 | Brukes ikke | 0 dager |
| GLM-5.2 | Brukes ikke | 0 dager |
| GLM-5.1 | Brukes ikke | 0 dager |
@@ -291,9 +292,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Brukes ikke | 0 dager |
| DeepSeek V4 Flash Vision Exp | Brukes ikke | 0 dager |
| Hy3 | Brukes ikke | 0 dager |
| Ox Alpha Free | Brukes ikke | 0 dager |
- **Grok 4.5:** ZDR deaktiverer viktige API-funksjoner som er avhengige av lagrede data, inkludert den tilstandsbaserte Responses API, Files and Collections og Batch API. [Les mer](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR deaktiverer viktige API-funksjoner som er avhengige av lagrede data, inkludert den tilstandsbaserte Responses API, Files and Collections og Batch API. [Les mer](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Logger for overvåking av misbruk genereres for all bruk av API-funksjoner og oppbevares i opptil 30 dager. [Les mer](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Kraftig rabatterte tokenpriser i bytte mot tillatelse til å bruke ledetekstene og fullføringene dine til å trene fremtidige Meta-modeller. Tilgjengeligheten er begrenset til regioner som er tillatt i henhold til [retningslinjene for geografisk bruk](https://ai.developer.meta.com/legal/geographic-use-policy) fra Meta. [Les mer](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** ZDR-avtalen fornyes månedlig. Den gjeldende avtalen er gyldig til og med 31. august 2026.
-3
View File
@@ -117,7 +117,6 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter.
| 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 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1M tokens**.
| Modell | Inndata | Utdata | Bufret lesing | Bufret skriving |
| --------------------------------- | ------- | ------- | ------------- | --------------- |
| 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 @@ Gratis-modellene:
- Nemotron 3 Ultra Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- Nemotron 3.5 Lightning Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- Big Pickle er en stealth-modell som er gratis på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
- Ox Alpha Free er en stealth-modell som er gratis på OpenCode i en begrenset periode. Leverandøren følger en nulloppbevaringspolicy og bruker ikke dataene dine til å trene modeller.
- Muse Spark 1.2 Contributor Free er tilgjengelig på OpenCode i en begrenset periode. Teamet bruker denne tiden til å samle inn tilbakemeldinger og forbedre modellen.
<a href={email}>Kontakt oss</a> hvis du har spørsmål.
+14 -14
View File
@@ -53,7 +53,8 @@ Tylko jeden członek na obszar roboczy (workspace) może zasubskrybować OpenCod
Obecna lista modeli obejmuje:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -75,7 +76,6 @@ Obecna lista modeli obejmuje:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (przez ograniczony czas)
Lista modeli może ulec zmianie w miarę testowania i dodawania nowych.
@@ -95,8 +95,9 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych
| Model | żądania na 5 godzin | żądania na tydzień | żądania na miesiąc |
| ---------------------------- | ------------------- | ------------------ | ------------------ |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -117,11 +118,11 @@ Poniższa tabela przedstawia szacunkową liczbę żądań na podstawie typowych
| 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 | - | - | - |
Szacunki te opierają się na zaobserwowanych wzorcach żądań:
- Grok 4.51 100 tokenów wejściowych, 71 500 w pamięci podręcznej, 220 tokenów wyjściowych na żądanie
- Grok 4.6390 tokenów wejściowych, 32 500 w pamięci podręcznej, 120 tokenów wyjściowych na żądanie
- GLM-5.3-Flash — 1 000 tokenów wejściowych, 55 000 w pamięci podręcznej, 200 tokenów wyjściowych na żądanie
- GLM-5.3/5.2/5.1 — 700 tokenów wejściowych, 52 000 w pamięci podręcznej, 150 tokenów wyjściowych na żądanie
- GPT 5.6 Luna — 1 000 tokenów wejściowych, 50 000 w pamięci podręcznej, 220 tokenów wyjściowych na żądanie
- Kimi K3 — 1 050 tokenów wejściowych, 76 500 w pamięci podręcznej, 300 tokenów wyjściowych na żądanie
@@ -145,9 +146,11 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz
| Model | Wejście | Wyjście | Odczyt z cache | Zapis do cache | Użycie |
| --------------------------------------- | ------- | ------- | -------------- | -------------- | ------ |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -174,14 +177,11 @@ Szacunki opierają się również na następujących cenach za 1M tokenów oraz
| 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:** Godziny Peak to 01:00-04:00 i 06:00-10:00 UTC od poniedziałku do piątku; wszystkie pozostałe godziny, w tym weekendy, to Off-Peak. [Dowiedz się więcej](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** Obrazy są przeliczane na tokeny na podstawie ich wymiarów i rozliczane jako tokeny wejściowe razem z tokenami tekstowymi. [Dowiedz się więcej](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Bezpłatny przez ograniczony czas.
Możesz śledzić swoje bieżące zużycie w **<a href={console}>konsoli</a>**.
:::tip
@@ -216,8 +216,9 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc
| Model | ID modelu | Punkt końcowy | Pakiet AI SDK |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -239,7 +240,6 @@ Możesz również uzyskać dostęp do modeli Go za pośrednictwem następującyc
| 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` |
[ID modelu](/docs/config/#models) w Twojej konfiguracji OpenCode
używa formatu `opencode-go/<model-id>`. Na przykład dla Kimi K3 należy użyć
@@ -261,8 +261,9 @@ https://opencode.ai/zen/go/v1/models
| Model | Trenowanie modelu | Retencja danych |
| ---------------------------- | ----------------- | --------------- |
| Grok 4.5 | Niewykorzystywane | 30 dni |
| Grok 4.6 | Niewykorzystywane | 30 dni |
| GPT 5.6 Luna | Niewykorzystywane | 30 dni |
| GLM-5.3-Flash | Niewykorzystywane | 0 dni |
| GLM-5.3 | Niewykorzystywane | 0 dni |
| GLM-5.2 | Niewykorzystywane | 0 dni |
| GLM-5.1 | Niewykorzystywane | 0 dni |
@@ -283,9 +284,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Niewykorzystywane | 0 dni |
| DeepSeek V4 Flash Vision Exp | Niewykorzystywane | 0 dni |
| Hy3 | Niewykorzystywane | 0 dni |
| Ox Alpha Free | Niewykorzystywane | 0 dni |
- **Grok 4.5:** ZDR wyłącza ważne funkcje API zależne od przechowywanych danych, w tym stanowy Responses API, Files and Collections oraz Batch API. [Dowiedz się więcej](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** ZDR wyłącza ważne funkcje API zależne od przechowywanych danych, w tym stanowy Responses API, Files and Collections oraz Batch API. [Dowiedz się więcej](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Dzienniki monitorowania nadużyć są generowane dla każdego użycia funkcji API i przechowywane przez maksymalnie 30 dni. [Dowiedz się więcej](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Znacznie obniżone ceny tokenów w zamian za zgodę na wykorzystanie Twoich promptów i odpowiedzi do trenowania przyszłych modeli Meta. Dostępność jest ograniczona do regionów dozwolonych przez [Zasady korzystania w poszczególnych regionach geograficznych](https://ai.developer.meta.com/legal/geographic-use-policy) firmy Meta. [Dowiedz się więcej](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** Umowa ZDR jest odnawiana co miesiąc. Obecna umowa obowiązuje do 31 sierpnia 2026 r.
-3
View File
@@ -117,7 +117,6 @@ Możesz też uzyskać dostęp do naszych modeli przez poniższe endpointy API.
| 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 @@ Obsługujemy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów*
| Model | Wejście | Wyjście | Odczyt z cache | Zapis do cache |
| --------------------------------- | ------- | ------- | -------------- | -------------- |
| 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 @@ Darmowe modele:
- Nemotron 3 Ultra Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- Nemotron 3.5 Lightning Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- Big Pickle to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
- Ox Alpha Free to stealth model, który jest darmowy w OpenCode przez ograniczony czas. Dostawca stosuje zasadę zerowego przechowywania i nie używa twoich danych do trenowania modeli.
- Muse Spark 1.2 Contributor Free jest dostępny w OpenCode przez ograniczony czas. Zespół wykorzystuje ten czas do zbierania opinii i ulepszania modelu.
<a href={email}>Skontaktuj się z nami</a>, jeśli masz pytania.
+14 -14
View File
@@ -59,7 +59,8 @@ Apenas um membro por workspace pode assinar o OpenCode Go.
A lista atual de modelos inclui:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +82,6 @@ A lista atual de modelos inclui:
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (por tempo limitado)
A lista de modelos pode mudar conforme testamos e adicionamos novos.
@@ -101,8 +101,9 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr
| Model | requisições por 5 horas | requisições por semana | requisições por mês |
| ---------------------------- | ----------------------- | ---------------------- | ------------------- |
| Grok 4.5 | 120 | 300 | 600 |
| Grok 4.6 | 169 | 423 | 845 |
| GPT 5.6 Luna | 2,050 | 5,100 | 10,250 |
| GLM-5.3-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -123,11 +124,11 @@ A tabela abaixo fornece uma contagem estimada de requisições com base nos padr
| 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 | - | - | - |
As estimativas se baseiam nos padrões de requisições observados:
- Grok 4.51.100 tokens de entrada, 71.500 em cache, 220 tokens de saída por requisição
- Grok 4.6390 tokens de entrada, 32.500 em cache, 120 tokens de saída por requisição
- GLM-5.3-Flash — 1.000 tokens de entrada, 55.000 em cache, 200 tokens de saída por requisição
- GLM-5.3/5.2/5.1 — 700 tokens de entrada, 52.000 em cache, 150 tokens de saída por requisição
- GPT 5.6 Luna — 1.000 tokens de entrada, 50.000 em cache, 220 tokens de saída por requisição
- Kimi K3 — 1.050 tokens de entrada, 76.500 em cache, 300 tokens de saída por requisição
@@ -151,9 +152,11 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m
| Modelo | Entrada | Saída | Leitura em cache | Escrita em cache | Uso |
| --------------------------------------- | ------- | ------ | ---------------- | ---------------- | --- |
| Grok 4.5 | $2.00 | $6.00 | $0.30 | - | $15 |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -180,14 +183,11 @@ As estimativas também se baseiam nos seguintes preços por 1M tokens e no uso m
| 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:** Os horários Peak são 01:00-04:00 e 06:00-10:00 UTC, de segunda a sexta-feira; todos os demais horários, incluindo os fins de semana, são Off-Peak. [Saiba mais](https://api-docs.deepseek.com/quick_start/pricing/).
**DeepSeek V4 Flash Vision Exp:** As imagens são convertidas em tokens com base em suas dimensões e cobradas como tokens de entrada junto com os tokens de texto. [Saiba mais](https://api-docs.deepseek.com/quick_start/pricing/).
**Ox Alpha Free:** Gratuito por tempo limitado.
Você pode acompanhar o seu uso atual no **<a href={console}>console</a>**.
:::tip
@@ -224,8 +224,9 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de
| Modelo | ID do Modelo | Endpoint | Pacote do AI SDK |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| Grok 4.5 | grok-4.5 | `https://opencode.ai/zen/go/v1/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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +248,6 @@ Você também pode acessar os modelos do Go através dos seguintes endpoints de
| 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` |
O [ID do modelo](/docs/config/#models) na sua configuração do OpenCode
usa o formato `opencode-go/<model-id>`. Por exemplo, para o Kimi K3, você usaria
@@ -269,8 +269,9 @@ https://opencode.ai/zen/go/v1/models
| Modelo | Treinamento de modelos | Retenção de dados |
| ---------------------------- | ---------------------- | ----------------- |
| Grok 4.5 | Não usado | 30 dias |
| Grok 4.6 | Não usado | 30 dias |
| GPT 5.6 Luna | Não usado | 30 dias |
| GLM-5.3-Flash | Não usado | 0 dias |
| GLM-5.3 | Não usado | 0 dias |
| GLM-5.2 | Não usado | 0 dias |
| GLM-5.1 | Não usado | 0 dias |
@@ -291,9 +292,8 @@ https://opencode.ai/zen/go/v1/models
| DeepSeek V4 Flash | Não usado | 0 dias |
| DeepSeek V4 Flash Vision Exp | Não usado | 0 dias |
| Hy3 | Não usado | 0 dias |
| Ox Alpha Free | Não usado | 0 dias |
- **Grok 4.5:** O ZDR desativa recursos importantes da API que dependem de dados armazenados, incluindo a Responses API com estado, Files and Collections e a Batch API. [Saiba mais](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **Grok 4.6:** O ZDR desativa recursos importantes da API que dependem de dados armazenados, incluindo a Responses API com estado, Files and Collections e a Batch API. [Saiba mais](https://docs.x.ai/developers/faq/security#what-is-zero-data-retention-zdr).
- **GPT 5.6 Luna:** Logs de monitoramento de abuso são gerados para todo uso de recursos da API e retidos por até 30 dias. [Saiba mais](https://developers.openai.com/api/docs/guides/your-data#data-retention-controls-for-abuse-monitoring).
- **Muse Spark 1.2 Contributor:** Preços de tokens com grandes descontos em troca da permissão para usar seus prompts e respostas geradas para treinar futuros modelos da Meta. A disponibilidade é limitada às regiões permitidas pela [Política de Uso Geográfico](https://ai.developer.meta.com/legal/geographic-use-policy) da Meta. [Saiba mais](https://dev.meta.ai/docs/pricing-rate-limits#contributor-tier).
- **DeepSeek V4 Flash:** O acordo de ZDR é renovado mensalmente. O acordo atual é válido até 31 de agosto de 2026.
@@ -108,7 +108,6 @@ Você também pode acessar nossos modelos pelos seguintes endpoints de API.
| 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` |
@@ -136,7 +135,6 @@ Oferecemos um modelo pay-as-you-go. Abaixo estão os preços **por 1M tokens**.
| Modelo | Entrada | Saída | Leitura em cache | Escrita em cache |
| --------------------------------- | ------- | ------- | ---------------- | ---------------- |
| 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 | - |
@@ -227,7 +225,6 @@ Os modelos gratuitos:
- Nemotron 3 Ultra Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- Nemotron 3.5 Lightning Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- Big Pickle é um modelo stealth que está gratuito no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
- Ox Alpha Free é um modelo stealth gratuito no OpenCode por tempo limitado. Seu provedor segue uma política de retenção zero e não usa seus dados para treinar modelos.
- Muse Spark 1.2 Contributor Free está disponível no OpenCode por tempo limitado. A equipe está usando esse período para coletar feedback e melhorar o modelo.
<a href={email}>Entre em contato</a> se você tiver alguma dúvida.
+14 -14
View File
@@ -59,7 +59,8 @@ OpenCode Go работает так же, как и любой другой пр
Текущий список моделей включает:
- **Grok 4.5**
- **Grok 4.6**
- **GLM-5.3-Flash**
- **GLM-5.3**
- **GLM-5.2**
- **GLM-5.1**
@@ -81,7 +82,6 @@ OpenCode Go работает так же, как и любой другой пр
- **DeepSeek V4 Flash**
- **DeepSeek V4 Flash Vision Exp**
- **Hy3**
- **Ox Alpha Free** (ограниченное время)
Список моделей может меняться по мере того, как мы тестируем и добавляем новые.
@@ -101,8 +101,9 @@ OpenCode Go включает следующие лимиты:
| 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-Flash | 1,580 | 3,950 | 7,900 |
| GLM-5.3 | 220 | 540 | 1,080 |
| GLM-5.2 | 880 | 2,150 | 4,300 |
| GLM-5.1 | 880 | 2,150 | 4,300 |
@@ -123,11 +124,11 @@ OpenCode Go включает следующие лимиты:
| 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 входных, 71,500 кешированных, 220 выходных токенов на запрос
- Grok 4.6390 входных, 32,500 кешированных, 120 выходных токенов на запрос
- GLM-5.3-Flash — 1,000 входных, 55,000 кешированных, 200 выходных токенов на запрос
- GLM-5.3/5.2/5.1 — 700 входных, 52,000 кешированных, 150 выходных токенов на запрос
- GPT 5.6 Luna — 1,000 входных, 50,000 кешированных, 220 выходных токенов на запрос
- Kimi K3 — 1,050 входных, 76,500 кешированных, 300 выходных токенов на запрос
@@ -151,9 +152,11 @@ OpenCode Go включает следующие лимиты:
| Model | Input | Output | Cached Read | Cached Write | Использование |
| --------------------------------------- | ------ | ------ | ----------- | ------------ | ------------- |
| 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-Flash | $0.15 | $0.50 | $0.03 | - | $15 |
| GLM-5.3 | $1.40 | $4.40 | $0.26 | - | $15 |
| GLM-5.2 | $1.40 | $4.40 | $0.26 | - | $60 |
| GLM-5.1 | $1.40 | $4.40 | $0.26 | - | $60 |
@@ -180,14 +183,11 @@ OpenCode Go включает следующие лимиты:
| 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}>консоли</a>**.
:::tip
@@ -224,8 +224,9 @@ OpenCode Go включает следующие лимиты:
| Модель | ID модели | Эндпоинт | Пакет AI SDK |
| ---------------------------- | ---------------------------- | ------------------------------------------------ | --------------------------- |
| 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-Flash | glm-5.3-flash | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| 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` |
| GLM-5.1 | glm-5.1 | `https://opencode.ai/zen/go/v1/chat/completions` | `@ai-sdk/openai-compatible` |
@@ -247,7 +248,6 @@ OpenCode Go включает следующие лимиты:
| 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` |
[ID модели](/docs/config/#models) в вашем конфиге OpenCode
использует формат `opencode-go/<model-id>`. Например, для Kimi K3 вам нужно
@@ -269,8 +269,9 @@ https://opencode.ai/zen/go/v1/models
| Модель | Обучение моделей | Хранение данных |
| ---------------------------- | ---------------- | --------------- |
| Grok 4.5 | Не используется | 30 дней |
| Grok 4.6 | Не используется | 30 дней |
| GPT 5.6 Luna | Не используется | 30 дней |
| GLM-5.3-Flash | Не используется | 0 дней |
| GLM-5.3 | Не используется | 0 дней |
| GLM-5.2 | Не используется | 0 дней |
| GLM-5.1 | Не используется | 0 дней |
@@ -291,9 +292,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 года.

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