From 6ef57a694ea66680bd99a277e0ff76de98019c4b Mon Sep 17 00:00:00 2001 From: Johnny Eric Amancio Date: Mon, 24 Aug 2026 14:04:20 +0200 Subject: [PATCH] refactor: kilo compat for v1.18.18 --- .opencode-version | 2 +- .opencode/tool/github-triage.ts | 2 +- bun.lock | 57 +- nix/hashes.json | 8 +- package.json | 15 +- packages/app/package.json | 97 -- packages/app/src/i18n/zh.ts | 1227 ---------------- packages/cli/package.json | 36 - packages/codemode/package.json | 8 +- packages/console/app/package.json | 48 - packages/console/app/src/i18n/en.ts | 848 ----------- .../app/src/routes/zen/util/handler.ts | 1297 ----------------- packages/console/core/package.json | 52 - packages/core/package.json | 2 +- packages/core/src/plugin/agent.ts | 8 +- packages/core/src/session/compaction.ts | 44 +- packages/core/src/v1/config/config.ts | 2 +- packages/core/test/provider-groq.test.ts | 28 + packages/core/test/provider-mistral.test.ts | 26 + .../core/test/provider-xai-responses.test.ts | 53 + packages/core/test/session-compaction.test.ts | 20 + packages/core/test/session-runner.test.ts | 63 +- packages/effect-drizzle-sqlite/package.json | 2 +- packages/effect-sqlite-node/package.json | 2 +- packages/http-recorder/package.json | 2 +- packages/llm/package.json | 2 +- packages/opencode/package.json | 3 +- .../opencode/src/agent/prompt/compaction.txt | 8 +- packages/opencode/src/config/parse.ts | 28 +- .../src/plugin/github-copilot/models.ts | 5 +- packages/opencode/src/provider/transform.ts | 9 + packages/opencode/src/session/compaction.ts | 41 +- packages/opencode/src/session/prompt/meta.txt | 8 +- packages/opencode/src/session/retry.ts | 14 +- packages/opencode/src/session/system.ts | 11 +- packages/opencode/test/config/config.test.ts | 15 +- .../test/plugin/github-copilot-models.test.ts | 68 + .../opencode/test/provider/provider.test.ts | 27 + .../opencode/test/provider/transform.test.ts | 51 + .../server/httpapi-instance-context.test.ts | 15 + .../opencode/test/session/compaction.test.ts | 73 +- packages/opencode/test/session/retry.test.ts | 39 +- packages/opencode/test/session/system.test.ts | 25 +- packages/plugin/package.json | 2 +- packages/sdk/js/package.json | 2 +- packages/server/package.json | 2 +- packages/session-ui/package.json | 2 +- packages/session-ui/src/context/data.tsx | 4 + packages/tui/package.json | 2 +- packages/ui/package.json | 2 +- packages/ui/src/theme/themes/oc-2.json | 2 +- packages/ui/src/v2/components/button-v2.css | 4 +- patches/@ai-sdk%2Fgroq@3.0.31.patch | 79 + patches/@ai-sdk%2Fmistral@3.0.51.patch | 27 +- patches/@ai-sdk%2Fxai@3.0.102.patch | 122 +- script/beta.ts | 36 +- 56 files changed, 875 insertions(+), 3802 deletions(-) delete mode 100644 packages/app/package.json delete mode 100644 packages/app/src/i18n/zh.ts delete mode 100644 packages/cli/package.json delete mode 100644 packages/console/app/package.json delete mode 100644 packages/console/app/src/i18n/en.ts delete mode 100644 packages/console/app/src/routes/zen/util/handler.ts delete mode 100644 packages/console/core/package.json create mode 100644 packages/core/test/provider-groq.test.ts create mode 100644 patches/@ai-sdk%2Fgroq@3.0.31.patch diff --git a/.opencode-version b/.opencode-version index bc8d3f528ff..e29fb18b22e 100644 --- a/.opencode-version +++ b/.opencode-version @@ -1 +1 @@ -v1.18.15 +v1.18.18 diff --git a/.opencode/tool/github-triage.ts b/.opencode/tool/github-triage.ts index e861e1e467b..d610a81e497 100644 --- a/.opencode/tool/github-triage.ts +++ b/.opencode/tool/github-triage.ts @@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin" const TEAM = { tui: ["kommander", "simonklee"], desktop_web: ["Hona", "Brendonovich"], - core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"], + core: ["jlongster", "rekram1-node", "neriousy", "nexxeln", "kitlangton"], inference: ["fwang", "MrMushrooooom", "starptech"], windows: ["Hona"], } as const diff --git a/bun.lock b/bun.lock index 1e3a6425719..04b5bcf35b8 100644 --- a/bun.lock +++ b/bun.lock @@ -29,7 +29,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@corvu/drawer": "catalog:", "@dnd-kit/abstract": "0.5.0", @@ -96,7 +96,7 @@ }, "packages/cli": { "name": "@opencode-ai/cli", - "version": "1.18.15", + "version": "1.18.18", "bin": { "lildax": "./bin/lildax.cjs", }, @@ -144,7 +144,7 @@ }, "packages/codemode": { "name": "@opencode-ai/codemode", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "acorn": "8.15.0", "effect": "catalog:", @@ -158,7 +158,7 @@ }, "packages/console/app": { "name": "@opencode-ai/console-app", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@ibm/plex": "6.4.1", @@ -194,7 +194,7 @@ }, "packages/console/core": { "name": "@opencode-ai/console-core", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@aws-sdk/client-sts": "3.782.0", "@jsx-email/render": "1.1.1", @@ -221,7 +221,7 @@ }, "packages/console/function": { "name": "@opencode-ai/console-function", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@ai-sdk/anthropic": "3.0.82", "@ai-sdk/openai": "3.0.48", @@ -243,7 +243,7 @@ }, "packages/console/mail": { "name": "@opencode-ai/console-mail", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@jsx-email/all": "2.2.3", "@jsx-email/cli": "1.4.3", @@ -267,7 +267,7 @@ }, "packages/console/support": { "name": "@opencode-ai/console-support", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@cloudflare/vite-plugin": "1.15.2", "@opencode-ai/console-core": "workspace:*", @@ -287,7 +287,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "1.18.15", + "version": "1.18.18", "bin": { "opencode": "./bin/opencode", }, @@ -381,7 +381,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@zip.js/zip.js": "2.7.62", "drizzle-orm": "catalog:", @@ -435,7 +435,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -449,7 +449,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "effect": "catalog:", }, @@ -461,7 +461,7 @@ }, "packages/enterprise": { "name": "@opencode-ai/enterprise", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@hono/standard-validator": "catalog:", "@opencode-ai/core": "workspace:*", @@ -493,7 +493,7 @@ }, "packages/function": { "name": "@opencode-ai/function", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@octokit/auth-app": "8.0.1", "@octokit/rest": "catalog:", @@ -509,7 +509,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@effect/platform-node": "4.0.0-beta.83", "@effect/platform-node-shared": "4.0.0-beta.83", @@ -540,7 +540,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@opencode-ai/schema": "workspace:*", "@smithy/eventstream-codec": "4.2.14", @@ -559,7 +559,7 @@ }, "packages/opencode": { "name": "opencode", - "version": "1.18.15", + "version": "1.18.18", "bin": { "opencode": "./bin/opencode", }, @@ -690,7 +690,7 @@ }, "packages/plugin": { "name": "@opencode-ai/plugin", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode-ai/sdk": "workspace:*", @@ -766,7 +766,7 @@ }, "packages/sdk/js": { "name": "@opencode-ai/sdk", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "cross-spawn": "catalog:", }, @@ -781,7 +781,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/protocol": "workspace:*", @@ -796,7 +796,7 @@ }, "packages/session-ui": { "name": "@opencode-ai/session-ui", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@kobalte/core": "catalog:", "@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz", @@ -836,7 +836,7 @@ }, "packages/slack": { "name": "@opencode-ai/slack", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@opencode-ai/sdk": "workspace:*", "@slack/bolt": "^3.17.1", @@ -849,7 +849,7 @@ }, "packages/stats/app": { "name": "@opencode-ai/stats-app", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@ibm/plex": "6.4.1", "@kobalte/core": "catalog:", @@ -883,7 +883,7 @@ }, "packages/stats/core": { "name": "@opencode-ai/stats-core", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@aws-sdk/client-athena": "3.933.0", "@planetscale/database": "1.19.0", @@ -902,7 +902,7 @@ }, "packages/stats/server": { "name": "@opencode-ai/stats-server", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@aws-sdk/client-firehose": "3.933.0", "@effect/platform-node": "catalog:", @@ -944,7 +944,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@opencode-ai/core": "workspace:*", "@opencode-ai/plugin": "workspace:*", @@ -971,7 +971,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@kobalte/core": "catalog:", "@pierre/diffs": "catalog:", @@ -1022,7 +1022,7 @@ }, "packages/web": { "name": "@opencode-ai/web", - "version": "1.18.15", + "version": "1.18.18", "dependencies": { "@astrojs/cloudflare": "12.6.3", "@astrojs/markdown-remark": "6.3.1", @@ -1075,6 +1075,7 @@ "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", + "@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", diff --git a/nix/hashes.json b/nix/hashes.json index 6f321d88bf2..0864cb930d1 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=", - "aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=", - "aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=", - "x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU=" + "x86_64-linux": "sha256-TNwKfqxD83UpZuCKN8FdEWN+CcQUP9CkCQSLGNqR/sA=", + "aarch64-linux": "sha256-qzvOJZzmq2QhlauElw8GwgQnCPHdhexI52L0md5zrxQ=", + "aarch64-darwin": "sha256-ZzoyLayOFfcYUAg35ZbZ2WapxDdd9IUWqy2xkxZH4QM=", + "x86_64-darwin": "sha256-maP/qLeaC3q8VcmNIPyIKlnplxFXJ7ULho3v21/16Mw=" } } diff --git a/package.json b/package.json index caea6e58714..b31df88c2a8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "packageManager": "bun@1.3.14", "scripts": { - "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", + "dev": "KILO_CLIENT=cli bun run --cwd packages/opencode --conditions=node src/index.ts", "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", @@ -21,7 +21,8 @@ "extension:isolated": "bun --cwd packages/kilo-vscode script/launch.ts --isolated", "extension:isolated:clean": "bun --cwd packages/kilo-vscode script/launch.ts --isolated --clean", "dev-setup": "bun run --cwd packages/opencode --conditions=browser src/index.ts dev-setup", - "dev:local": "bun run packages/opencode/script/dev-local.ts" + "dev:local": "bun run packages/opencode/script/dev-local.ts", + "test:script:ci": "mkdir -p .artifacts/unit && bun test ./script --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" }, "workspaces": { "packages": [ @@ -162,7 +163,6 @@ "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch", "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", @@ -170,14 +170,15 @@ "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch", + "@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch", "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", - "@tanstack/solid-virtual@3.13.28": "patches/@tanstack%2Fsolid-virtual@3.13.28.patch", - "@tanstack/virtual-core@3.17.0": "patches/@tanstack%2Fvirtual-core@3.17.0.patch", + "@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch", "virtua@0.49.1": "patches/virtua@0.49.1.patch", "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", "pacote@21.5.1": "patches/pacote@21.5.1.patch", - "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" + "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", + "solid-js@1.9.12": "patches/solid-js@1.9.12.patch" }, - "version": "7.4.20", + "version": "7.4.23", "peerDependencies": {} } diff --git a/packages/app/package.json b/packages/app/package.json deleted file mode 100644 index 3cee5122ff8..00000000000 --- a/packages/app/package.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "name": "@opencode-ai/app", - "version": "7.4.20", - "description": "", - "type": "module", - "exports": { - ".": "./src/index.ts", - "./desktop-menu": "./src/desktop-menu.ts", - "./i18n/desktop-native": "./src/i18n/desktop-native.ts", - "./updater": "./src/updater.ts", - "./wsl/types": "./src/wsl/types.ts", - "./vite": "./vite.js", - "./index.css": "./src/index.css" - }, - "scripts": { - "typecheck": "tsgo -b", - "typecheck:e2e": "tsgo -p e2e/tsconfig.json", - "start": "vite", - "dev": "vite", - "build": "vite build", - "serve": "vite preview", - "test": "bun run test:unit && bun run test:browser", - "test:unit": "bun test --conditions=solid --only-failures --preload ./happydom.ts ./src", - "test:browser": "bun test --conditions=browser --preload ./happydom.ts ./test-browser", - "test:unit:watch": "bun test --conditions=solid --watch --preload ./happydom.ts ./src", - "test:e2e": "playwright test", - "test:e2e:local": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:report": "playwright show-report e2e/playwright-report", - "test:stability": "bun test ./e2e/performance/unit/visual-stability.test.ts && playwright test --config e2e/performance/timeline-stability/playwright.config.ts", - "test:bench": "bun test ./e2e/performance/unit && playwright test --config e2e/performance/playwright.config.ts" - }, - "license": "MIT", - "devDependencies": { - "@happy-dom/global-registrator": "20.0.11", - "@playwright/test": "catalog:", - "@sentry/vite-plugin": "catalog:", - "@tailwindcss/vite": "catalog:", - "@tsconfig/bun": "1.0.9", - "@types/bun": "catalog:", - "@types/luxon": "catalog:", - "@types/node": "catalog:", - "@typescript/native-preview": "catalog:", - "tw-animate-css": "1.4.0", - "typescript": "catalog:", - "vite": "catalog:", - "vite-plugin-icons-spritesheet": "3.0.1", - "vite-plugin-solid": "catalog:" - }, - "dependencies": { - "@corvu/drawer": "catalog:", - "@dnd-kit/abstract": "0.5.0", - "@dnd-kit/dom": "0.5.0", - "@dnd-kit/helpers": "0.5.0", - "@dnd-kit/solid": "0.5.0", - "@kobalte/core": "catalog:", - "@opencode-ai/client": "file:vendor/opencode-ai-client-1.17.13-v2.tgz", - "@opencode-ai/core": "workspace:*", - "@opencode-ai/schema": "workspace:*", - "@kilocode/sdk": "workspace:*", - "@opencode-ai/session-ui": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@pierre/trees": "1.0.0-beta.4", - "@sentry/solid": "catalog:", - "@shikijs/transformers": "3.9.2", - "@solid-primitives/active-element": "2.1.3", - "@solid-primitives/audio": "1.4.2", - "@solid-primitives/event-bus": "1.1.2", - "@solid-primitives/event-listener": "2.4.5", - "@solid-primitives/i18n": "2.2.1", - "@solid-primitives/media": "2.3.3", - "@solid-primitives/resize-observer": "2.1.5", - "@solid-primitives/scheduled": "1.5.3", - "@solid-primitives/scroll": "2.1.3", - "@solid-primitives/storage": "catalog:", - "@solid-primitives/timer": "1.4.4", - "@solid-primitives/websocket": "1.3.1", - "@solidjs/meta": "catalog:", - "@solidjs/router": "catalog:", - "@tanstack/solid-query": "5.91.4", - "@tanstack/solid-virtual": "catalog:", - "@thisbeyond/solid-dnd": "0.7.5", - "diff": "catalog:", - "effect": "catalog:", - "fuzzysort": "catalog:", - "ghostty-web": "github:anomalyco/ghostty-web#83c0a07b8628b748aed073b232cb4b52a6ca11c1", - "luxon": "catalog:", - "marked": "catalog:", - "marked-shiki": "catalog:", - "remeda": "catalog:", - "shiki": "catalog:", - "solid-js": "catalog:", - "solid-list": "catalog:", - "solid-presence": "0.2.0", - "tailwindcss": "catalog:" - } -} diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts deleted file mode 100644 index b6fbfa5a90c..00000000000 --- a/packages/app/src/i18n/zh.ts +++ /dev/null @@ -1,1227 +0,0 @@ -import { dict as en } from "./en" - -type Keys = keyof typeof en - -export const dict = { - "desktop.menu.app": "Kilo", // kilocode_change - "desktop.menu.file": "文件", - "desktop.menu.edit": "编辑", - "desktop.menu.view": "视图", - "desktop.menu.go": "转到", - "desktop.menu.window": "窗口", - "desktop.menu.help": "帮助", - "desktop.menu.checkForUpdates": "检查更新...", - "desktop.menu.settings": "设置", - "desktop.menu.reloadWebview": "重新加载 Webview", - "desktop.menu.restart": "重启", - "desktop.menu.exportLogs": "导出日志...", - "desktop.menu.newSession": "新建会话", - "desktop.menu.openProject": "打开项目...", - "desktop.menu.newWindow": "新建窗口", - "desktop.menu.closeWindow": "关闭窗口", - "desktop.menu.undo": "撤销", - "desktop.menu.redo": "重做", - "desktop.menu.cut": "剪切", - "desktop.menu.copy": "复制", - "desktop.menu.paste": "粘贴", - "desktop.menu.delete": "删除", - "desktop.menu.selectAll": "全选", - "desktop.menu.toggleSidebar": "切换侧边栏", - "desktop.menu.toggleTerminal": "切换终端", - "desktop.menu.toggleFileTree": "切换文件树", - "desktop.menu.reload": "重新加载", - "desktop.menu.toggleDeveloperTools": "切换开发人员工具", - "desktop.menu.actualSize": "实际大小", - "desktop.menu.zoomIn": "放大", - "desktop.menu.zoomOut": "缩小", - "desktop.menu.toggleFullScreen": "切换全屏显示", - "desktop.menu.back": "后退", - "desktop.menu.forward": "前进", - "desktop.menu.previousSession": "上一个会话", - "desktop.menu.nextSession": "下一个会话", - "desktop.menu.previousProject": "上一个项目", - "desktop.menu.nextProject": "下一个项目", - "desktop.menu.minimize": "最小化", - "desktop.menu.maximize": "最大化", - "desktop.menu.documentation": "Kilo 文档", // kilocode_change - "desktop.menu.supportForum": "支持论坛", - "desktop.menu.shareFeedback": "提供反馈", - "desktop.menu.reportBug": "报告错误", - "desktop.menu.ariaLabel": "Kilo 菜单", // kilocode_change - - "desktop.updater.dialog.checkFailed.message": "检查更新失败。", - "desktop.updater.dialog.checkFailed.title": "更新错误", - "desktop.updater.dialog.upToDate.message": "你使用的是最新版本。", - "desktop.updater.dialog.upToDate.title": "无可用更新", - "desktop.updater.dialog.ready.message": "已下载更新 {{version}}。是否立即重启?", - "desktop.updater.dialog.ready.title": "更新已就绪", - "desktop.updater.dialog.restart": "重启", - "desktop.updater.dialog.later": "稍后", - - "desktop.recovery.action.relaunch": "重新启动", - "desktop.recovery.action.exportLogs": "导出日志", - "desktop.recovery.action.keepWaiting": "继续等待", - "desktop.recovery.action.quit": "退出", - "desktop.recovery.loadFailed": "Kilo 加载失败", // kilocode_change - "desktop.recovery.terminated": "Kilo 窗口意外终止", // kilocode_change - "desktop.recovery.unresponsive": "Kilo 未响应", // kilocode_change - "desktop.recovery.unresponsive.detail": "你可以重新启动应用、打开日志或继续等待。", - "desktop.recovery.loadFailed.detail": "窗口:{{window}}\nURL:{{url}}\n错误:{{code}} {{description}}", - "desktop.recovery.terminated.detail": "窗口:{{window}}\n原因:{{reason}}\n代码:{{code}}", - "desktop.recovery.unknown": "<未知>", - - "desktop.dialog.chooseFolder": "选择文件夹", - "desktop.dialog.chooseFile": "选择文件", - "desktop.dialog.saveFile": "保存文件", - "desktop.dialog.files": "文件", - "desktop.server.local": "本地服务器", - - "desktop.wsl.error.windowsOnly": "WSL 仅在 Windows 上可用", - "desktop.wsl.error.unavailable": "WSL 不可用", - "desktop.wsl.error.listInstalled": "无法列出已安装的 WSL 发行版", - "desktop.wsl.error.listOnline": "无法列出在线 WSL 发行版", - "desktop.wsl.error.executeDistro": "无法在发行版中执行命令", - "desktop.wsl.error.installWsl": "WSL 安装失败", - "desktop.wsl.error.installDistro": "无法安装发行版:{{distro}}", - "desktop.wsl.error.installOpencode": "Kilo 安装失败", // kilocode_change - "desktop.wsl.error.alreadyAdded": "已添加 {{distro}}", - "desktop.wsl.error.opencodeMissing": "此发行版中未安装 opencode", - "desktop.wsl.error.opencodeCannotRun": "已安装 opencode,但无法运行", - "desktop.wsl.error.opencodeNotInstalled": "{{distro}} 中未安装 Kilo", // kilocode_change - "desktop.wsl.error.updateVersion": - "Kilo 更新已完成,但 {{distro}} 仍报告版本 {{installed}};预期版本为 {{expected}}", // kilocode_change - "desktop.wsl.error.noVersion": "无版本信息", - "desktop.wsl.error.serverExited": "WSL 服务器启动后退出(code={{code}} signal={{signal}})", - "desktop.wsl.error.serverExitedBeforeHealthy": - "WSL 服务器在进入正常状态前退出(code={{code}} signal={{signal}}){{output}}", - "desktop.wsl.error.healthTimeout": "{{distro}} 的 Sidecar 运行状况检查在 {{timeout}}ms 后超时", - "desktop.wsl.error.commandTimeout": "{{command}} {{args}} 在 {{timeout}}ms 后超时", - "desktop.wsl.error.failedPort": "无法获取端口", - - "desktop.picker.error.notSelected": "文件选择器未选择文件", - "desktop.picker.error.sizeLimit": "所选附件超过 {{limit}} MB 的限制", - - "command.category.suggested": "建议", - "command.category.view": "视图", - "command.category.project": "项目", - "command.category.provider": "提供商", - "command.category.server": "服务器", - "command.category.session": "会话", - "command.category.theme": "主题", - "command.category.language": "语言", - "command.category.file": "文件", - "command.category.context": "上下文", - "command.category.terminal": "终端", - "command.category.model": "模型", - "command.category.mcp": "MCP", - "command.category.agent": "智能体", - "command.category.permissions": "权限", - "command.category.workspace": "工作区", - "command.category.settings": "设置", - "command.logs.export": "导出日志", - - "theme.scheme.system": "系统", - "theme.scheme.light": "浅色", - "theme.scheme.dark": "深色", - - "command.sidebar.toggle": "切换侧边栏", - - "command.project.open": "打开项目", - "command.project.previous": "上一个项目", - "command.project.next": "下一个项目", - "command.project.index": "切换到项目 {{index}}", - - "command.provider.connect": "连接提供商", - - "command.server.switch": "切换服务器", - - "command.settings.open": "打开设置", - - "command.session.previous": "上一个会话", - "command.session.next": "下一个会话", - "command.session.previous.unseen": "上一个未读会话", - "command.session.next.unseen": "下一个未读会话", - "command.session.archive": "归档会话", - - "command.palette": "命令面板", - - "command.theme.cycle": "切换主题", - "command.theme.set": "使用主题:{{theme}}", - "command.theme.scheme.cycle": "切换配色方案", - "command.theme.scheme.set": "使用配色方案:{{scheme}}", - - "command.language.cycle": "切换语言", - "command.language.set": "使用语言:{{language}}", - - "command.session.new": "新建会话", - - "command.file.open": "打开文件", - - "command.tab.close": "关闭标签页", - "command.tab.reopenClosed": "重新打开已关闭的标签页", - - "command.context.addSelection": "将所选内容添加到上下文", - "command.context.addSelection.description": "添加当前文件中选中的行", - - "command.input.focus": "聚焦输入框", - - "command.terminal.toggle": "切换终端", - - "command.fileTree.toggle": "切换文件树", - - "command.review.toggle": "切换审查", - - "command.terminal.new": "新建终端", - "command.terminal.new.description": "创建新的终端标签页", - - "command.steps.toggle": "切换步骤", - "command.steps.toggle.description": "显示或隐藏当前消息的步骤", - - "command.message.previous": "上一条消息", - "command.message.previous.description": "跳转到上一条用户消息", - "command.message.next": "下一条消息", - "command.message.next.description": "跳转到下一条用户消息", - - "command.model.choose": "选择模型", - "command.model.choose.description": "选择不同的模型", - - "command.mcp.toggle": "启用或禁用 MCP 服务器", - "command.mcp.toggle.description": "启用或禁用 MCP 服务器", - - "command.agent.cycle": "切换智能体", - "command.agent.cycle.description": "切换到下一个智能体", - "command.agent.cycle.reverse": "反向切换智能体", - "command.agent.cycle.reverse.description": "切换到上一个智能体", - - "command.model.variant.cycle": "切换思考强度", - "command.model.variant.cycle.description": "切换到下一个强度等级", - - "command.prompt.mode.shell": "Shell", - "command.prompt.mode.normal": "Prompt", - - "command.permissions.autoaccept.enable": "自动接受权限", - "command.permissions.autoaccept.disable": "停止自动接受权限", - - "command.workspace.toggle": "切换工作区", - "command.workspace.toggle.description": "在侧边栏启用或禁用多个工作区", - - "command.session.undo": "撤销", - "command.session.undo.description": "撤销上一条消息", - "command.session.redo": "重做", - "command.session.redo.description": "重做上一条撤销的消息", - "command.session.compact": "压缩会话", - "command.session.compact.description": "总结会话以减少上下文大小", - "command.session.fork": "从消息创建新会话", - "command.session.fork.description": "从之前的消息创建新会话", - "command.session.share": "分享会话", - "command.session.share.description": "分享此会话并将链接复制到剪贴板", - "command.session.unshare": "取消分享会话", - "command.session.unshare.description": "停止分享此会话", - - "command.session.export": "导出会话", - "command.session.export.description": "将完整会话记录导出为 JSON", - - "palette.search.placeholder": "搜索文件、命令和会话", - "palette.search.placeholder.home": "搜索命令和会话", - "palette.empty": "未找到结果", - "palette.group.commands": "命令", - "palette.group.files": "文件", - - "dialog.provider.search.placeholder": "搜索提供商", - "dialog.provider.empty": "未找到提供商", - "dialog.provider.group.popular": "热门", - "dialog.provider.group.other": "其他", - "dialog.provider.custom.label": "自定义 OpenAI 兼容提供商", - "dialog.provider.tag.recommended": "推荐", - "dialog.provider.opencode.note": "精选模型,包括 Claude、GPT、Gemini 等", - "dialog.provider.opencode.tagline": "可靠的优化模型", - "dialog.provider.opencodeGo.tagline": "适合所有人的低成本订阅", - "dialog.provider.anthropic.note": "直接使用 Claude 模型,包括 Pro 和 Max", - "dialog.provider.copilot.note": "通过 GitHub Copilot 使用辅助编程 AI 模型", - "dialog.provider.openai.note": "适合快速处理各类 AI 任务的 GPT 模型", - "dialog.provider.google.note": "可快速生成结构化回复的 Gemini 模型", - "dialog.provider.openrouter.note": "通过一个提供商使用所有受支持的模型", - "dialog.provider.vercel.note": "通过智能路由统一使用 AI 模型", - - "dialog.model.select.title": "选择模型", - "dialog.model.search.placeholder": "搜索模型", - "dialog.model.empty": "未找到模型", - "dialog.model.manage": "管理模型", - "dialog.model.manage.description": "自定义模型选择器中显示的模型。", - "dialog.model.manage.provider.toggle": "切换所有 {{provider}} 模型", - "dialog.model.unpaid.freeModels.title": "Kilo 提供的免费模型", // kilocode_change - "dialog.model.unpaid.addMore.title": "从热门提供商添加更多模型", - "dialog.model.unpaid.viewMoreProviders": "查看另外 70 多个提供商", - - "dialog.provider.viewAll": "查看更多提供商", - - "provider.connect.title": "连接 {{provider}}", - "provider.connect.title.anthropicProMax": "使用 Claude Pro/Max 登录", - "provider.connect.selectMethod": "选择 {{provider}} 的登录方式。", - "provider.connect.method.apiKey": "API 密钥", - "provider.connect.method.browser": "浏览器", - "provider.connect.method.headless": "无头模式", - "provider.connect.status.inProgress": "正在授权...", - "provider.connect.status.waiting": "等待授权...", - "provider.connect.status.failed": "授权失败:{{error}}", - "provider.connect.apiKey.description": - "输入你的 {{provider}} API 密钥以连接账户,并在 Kilo 中使用 {{provider}} 模型。", // kilocode_change - "provider.connect.apiKey.label": "{{provider}} API 密钥", - "provider.connect.apiKey.placeholder": "API 密钥", - "provider.connect.apiKey.required": "API 密钥为必填项", - "provider.connect.opencodeZen.line1": "OpenCode Zen 为你提供一组精选的可靠优化模型,用于代码智能体。", - "provider.connect.opencodeZen.line2": "只需一个 API 密钥,你就能使用 Claude、GPT、Gemini、GLM 等模型。", - "provider.connect.opencodeZen.visit.prefix": "访问 ", - "provider.connect.opencodeZen.visit.link": "opencode.ai/zen", - "provider.connect.opencodeZen.visit.suffix": " 获取你的 API 密钥。", - "provider.connect.oauth.code.visit.prefix": "访问 ", - "provider.connect.oauth.code.visit.link": "此链接", - "provider.connect.oauth.code.visit.suffix": " 获取授权码,以连接你的账户并在 Kilo 中使用 {{provider}} 模型。", // kilocode_change - "provider.connect.oauth.code.label": "{{method}} 授权码", - "provider.connect.oauth.code.placeholder": "授权码", - "provider.connect.oauth.code.required": "授权码为必填项", - "provider.connect.oauth.code.invalid": "授权码无效", - "provider.connect.oauth.auto.visit.prefix": "访问 ", - "provider.connect.oauth.auto.visit.link": "此链接", - "provider.connect.oauth.auto.visit.suffix": " 并输入以下代码,以连接你的账户并在 Kilo 中使用 {{provider}} 模型。", // kilocode_change - "provider.connect.oauth.auto.confirmationCode": "确认码", - "provider.connect.toast.connected.title": "{{provider}} 已连接", - "provider.connect.toast.connected.description": "现在可以使用 {{provider}} 模型了。", - - "provider.custom.title": "自定义提供商", - "provider.custom.unavailable": "此服务器上无法使用自定义提供商", - "provider.custom.description.prefix": "配置与 OpenAI 兼容的提供商。请查看", - "provider.custom.description.link": "提供商配置文档", - "provider.custom.description.suffix": "。", - "provider.custom.field.providerID.label": "提供商 ID", - "provider.custom.field.providerID.placeholder": "myprovider", - "provider.custom.field.providerID.description": "使用小写字母、数字、连字符或下划线", - "provider.custom.field.name.label": "显示名称", - "provider.custom.field.name.placeholder": "我的 AI 提供商", - "provider.custom.field.baseURL.label": "基础 URL", - "provider.custom.field.baseURL.placeholder": "https://api.myprovider.com/v1", - "provider.custom.field.apiKey.label": "API 密钥", - "provider.custom.field.apiKey.placeholder": "API 密钥", - "provider.custom.field.apiKey.description": "可选。如果你通过请求头管理认证,可留空。", - "provider.custom.models.label": "模型", - "provider.custom.models.id.label": "ID", - "provider.custom.models.id.placeholder": "model-id", - "provider.custom.models.name.label": "名称", - "provider.custom.models.name.placeholder": "显示名称", - "provider.custom.models.remove": "移除模型", - "provider.custom.models.add": "添加模型", - "provider.custom.headers.label": "请求头(可选)", - "provider.custom.headers.key.label": "请求头", - "provider.custom.headers.key.placeholder": "Header-Name", - "provider.custom.headers.value.label": "值", - "provider.custom.headers.value.placeholder": "value", - "provider.custom.headers.remove": "移除请求头", - "provider.custom.headers.add": "添加请求头", - "provider.custom.error.providerID.required": "提供商 ID 为必填项", - "provider.custom.error.providerID.format": "请使用小写字母、数字、连字符或下划线", - "provider.custom.error.providerID.exists": "该提供商 ID 已存在", - "provider.custom.error.name.required": "显示名称为必填项", - "provider.custom.error.baseURL.required": "基础 URL 为必填项", - "provider.custom.error.baseURL.format": "必须以 http:// 或 https:// 开头", - "provider.custom.error.required": "必填", - "provider.custom.error.duplicate": "重复", - - "provider.disconnect.toast.disconnected.title": "{{provider}} 已断开连接", - "provider.disconnect.toast.disconnected.description": "{{provider}} 模型已不再可用。", - - "model.tag.free": "免费", - "model.tag.latest": "最新", - "model.provider.anthropic": "Anthropic", - "model.provider.openai": "OpenAI", - "model.provider.google": "Google", - "model.provider.xai": "xAI", - "model.provider.meta": "Meta", - "model.input.text": "文本", - "model.input.image": "图像", - "model.input.audio": "音频", - "model.input.video": "视频", - "model.input.pdf": "PDF", - "model.tooltip.allows": "支持:{{inputs}}", - "model.tooltip.context.label": "上下文", - "model.tooltip.inputs": "输入", - "model.tooltip.model": "模型", - "model.tooltip.provider": "提供商", - "model.tooltip.reasoning": "推理", - "model.tooltip.reasoning.allowed": "支持推理", - "model.tooltip.reasoning.none": "不支持推理", - "model.tooltip.context": "上下文上限 {{limit}}", - - "common.search.placeholder": "搜索", - "common.goBack": "返回", - "common.goForward": "前进", - "common.loading": "加载中", - "common.loading.ellipsis": "...", - "common.cancel": "取消", - "common.clear": "清除", - "common.connect": "连接", - "common.disconnect": "断开连接", - "common.continue": "继续", - "common.submit": "提交", - "common.save": "保存", - "common.saving": "保存中...", - "common.default": "默认", - "common.attachment": "附件", - - "prompt.placeholder.shell": "输入 shell 命令... {{example}}", - "prompt.placeholder.normal": '随便问点什么... "{{example}}"', - "prompt.placeholder.simple": "随便问点什么...", - "prompt.placeholder.summarizeComments": "总结评论…", - "prompt.placeholder.summarizeComment": "总结该评论…", - "prompt.mode.shell": "Shell", - "prompt.mode.normal": "Prompt", - "prompt.mode.shell.exit": "按 esc 退出", - "session.child.promptDisabled": "无法向子智能体会话发送提示词。", - "session.child.backToParent": "返回主会话。", - "prompt.example.1": "修复代码库中的一个 TODO", - "prompt.example.2": "这个项目的技术栈是什么?", - "prompt.example.3": "修复失败的测试", - "prompt.example.4": "解释认证是如何工作的", - "prompt.example.5": "查找并修复安全漏洞", - "prompt.example.6": "为用户服务添加单元测试", - "prompt.example.7": "重构这个函数,让它更易读", - "prompt.example.8": "这个错误是什么意思?", - "prompt.example.9": "帮我调试这个问题", - "prompt.example.10": "生成 API 文档", - "prompt.example.11": "优化数据库查询", - "prompt.example.12": "添加输入校验", - "prompt.example.13": "创建一个新的组件用于...", - "prompt.example.14": "我该如何部署这个项目?", - "prompt.example.15": "审查我的代码并给出最佳实践建议", - "prompt.example.16": "为这个函数添加错误处理", - "prompt.example.17": "解释这个正则表达式", - "prompt.example.18": "把它转换成 TypeScript", - "prompt.example.19": "在整个代码库中添加日志", - "prompt.example.20": "哪些依赖已经过期?", - "prompt.example.21": "帮我写一个迁移脚本", - "prompt.example.22": "为这个接口实现缓存", - "prompt.example.23": "给这个列表添加分页", - "prompt.example.24": "创建一个 CLI 命令用于...", - "prompt.example.25": "这里的环境变量是怎么工作的?", - "prompt.popover.emptyResults": "没有匹配的结果", - "prompt.popover.emptyCommands": "没有匹配的命令", - "prompt.dropzone.label": "将图片、PDF 或文本文件拖放到此处", - "prompt.dropzone.file.label": "拖放以 @提及文件", - "prompt.slash.badge.custom": "自定义", - "prompt.slash.badge.skill": "技能", - "prompt.slash.badge.mcp": "mcp", - "prompt.context.active": "当前", - "prompt.context.includeActiveFile": "包含当前文件", - "prompt.context.removeActiveFile": "从上下文移除活动文件", - "prompt.context.removeFile": "从上下文移除文件", - "prompt.action.attachFile": "附加文件", - "prompt.menu.addImagesAndFiles": "添加文件及更多内容", - "prompt.menu.imagesAndFiles": "图片和文件", - "prompt.menu.commands": "命令", - "prompt.menu.context": "上下文", - "prompt.menu.shellCommand": "shell 命令", - "prompt.attachment.remove": "移除附件", - "prompt.action.send": "发送", - "prompt.action.stop": "停止", - "prompt.toast.pasteUnsupported.title": "不支持的附件", - "prompt.toast.attachmentDuplicate.title": "此文件已上传", - "prompt.toast.pasteUnsupported.description": "此处仅能附加图片、PDF 或文本文件。", - "prompt.toast.modelAgentRequired.title": "请选择智能体和模型", - "prompt.toast.modelAgentRequired.description": "发送提示前请先选择智能体和模型。", - "prompt.toast.worktreeCreateFailed.title": "创建工作区失败", - "prompt.toast.sessionCreateFailed.title": "创建会话失败", - "prompt.toast.shellSendFailed.title": "发送 shell 命令失败", - "prompt.toast.commandSendFailed.title": "发送命令失败", - "prompt.toast.promptSendFailed.title": "发送提示失败", - "prompt.toast.promptSendFailed.description": "无法获取会话", - - "dialog.mcp.title": "MCP 服务器", - "dialog.mcp.description": "已启用 {{enabled}} / {{total}}", - "dialog.mcp.empty": "未配置 MCP 服务器", - - "dialog.lsp.empty": "已根据文件类型自动检测 LSP", - - "dialog.plugins.empty": "在 opencode.json 中配置的插件", - - "mcp.status.connected": "已连接", - "mcp.status.failed": "失败", - "mcp.status.needs_auth": "需要授权", - "mcp.auth.clickToAuthenticate": "点击进行授权", - "mcp.status.disabled": "已禁用", - - "dialog.fork.empty": "没有可用于创建新会话的消息", - - "dialog.directory.search.placeholder": "搜索文件夹", - "dialog.directory.empty": "未找到文件夹", - "dialog.directory.action.selectFile": "选择文件", - "dialog.directory.action.selectFolder": "选择文件夹", - "dialog.directory.root": "根目录", - "dialog.directory.parent": "上级目录", - "dialog.directory.readError": "无法读取此文件夹", - - "dialog.server.title": "服务器", - "dialog.server.description": "切换此应用连接的 Kilo 服务器。", // kilocode_change - "dialog.server.search.placeholder": "搜索服务器", - "dialog.server.empty": "暂无服务器", - "dialog.server.add.title": "添加服务器", - "dialog.server.add.url": "服务器 URL", - "dialog.server.add.placeholder": "http://localhost:4096", - "dialog.server.add.error": "无法连接到服务器", - "dialog.server.add.checking": "检查中...", - "dialog.server.add.button": "添加服务器", - "dialog.server.add.name": "服务器名称(可选)", - "dialog.server.add.namePlaceholder": "Localhost", - "dialog.server.add.username": "用户名(可选)", - "dialog.server.add.password": "密码(可选)", - "dialog.server.edit.title": "编辑服务器", - "dialog.server.default.title": "默认服务器", - "dialog.server.default.description": "应用启动时连接此服务器,而不是启动本地服务器。需要重启。", - "dialog.server.default.none": "未选择服务器", - "dialog.server.default.set": "将当前服务器设为默认", - "dialog.server.default.clear": "清除", - "dialog.server.action.remove": "移除服务器", - "dialog.server.menu.edit": "编辑", - "dialog.server.menu.default": "设为默认", - "dialog.server.menu.defaultRemove": "取消默认", - "dialog.server.menu.delete": "删除", - "dialog.server.current": "当前服务器", - "dialog.server.status.default": "默认", - "wsl.server.add": "添加 WSL 服务器", - "wsl.server.addShort": "添加 WSL", - "wsl.server.label": "WSL", - "wsl.server.menu.label": "WSL 服务器", - "wsl.server.retryStart": "重试启动", - "wsl.server.updating": "正在更新...", - "wsl.onboarding.step.distro": "选择发行版", - "wsl.onboarding.step.opencode": "Kilo", // kilocode_change - "wsl.onboarding.checkingRuntime": "正在检查 WSL...", - "wsl.onboarding.restartRequired": "Windows 需要重启才能完成 WSL 安装。", - "wsl.onboarding.ready": "WSL 已就绪。", - "wsl.onboarding.required": "需要 WSL 才能继续。", - "wsl.onboarding.checkingDistros": "正在检查发行版...", - "wsl.onboarding.installingDistro": "正在安装 {{distro}}...", - "wsl.onboarding.checkingDistro": "正在检查 {{distro}}...", - "wsl.onboarding.listingDistros": "正在列出发行版...", - "wsl.onboarding.distroReady": "{{distro}} 已就绪。", - "wsl.onboarding.distroNotInstalled": "{{distro}} 尚未安装。", - "wsl.onboarding.openDistroOnce": "打开一次 {{distro}} 以完成设置。", - "wsl.onboarding.finishingDistro": "正在完成 {{distro}} 的设置。", - "wsl.onboarding.pickDistro": "选择一个发行版,或在下方安装。", - "wsl.onboarding.checkingOpencode": "正在检查 Kilo...", // kilocode_change - "wsl.onboarding.checkingOpencodeIn": "正在检查 {{distro}} 中的 Kilo...", // kilocode_change - "wsl.onboarding.updatingOpencode": "正在更新 Kilo...", // kilocode_change - "wsl.onboarding.updatingOpencodeIn": "正在更新 {{distro}} 中的 Kilo...", // kilocode_change - "wsl.onboarding.updateOpencodeIn": "更新 {{distro}} 中的 Kilo。", // kilocode_change - "wsl.onboarding.updateOpencode": "更新 Kilo", // kilocode_change - "wsl.onboarding.opencodeReadyIn": "{{distro}} 中的 Kilo 已就绪。", // kilocode_change - "wsl.onboarding.opencodeReady": "Kilo 已就绪。", // kilocode_change - "wsl.onboarding.installOpencodeIn": "在 {{distro}} 中安装 Kilo。", // kilocode_change - "wsl.onboarding.installOpencode": "安装 Kilo", // kilocode_change - "wsl.onboarding.chooseDistroFirst": "请先选择发行版。", - "wsl.onboarding.loadFailed": "加载 WSL 状态失败。", - "wsl.onboarding.loading": "加载中...", - "wsl.onboarding.installedDistros": "已安装的发行版", - "wsl.onboarding.checkAgain": "重新检查", - "wsl.onboarding.distroStatus.ready": "已就绪", - "wsl.onboarding.distroStatus.checking": "检查中...", - "wsl.onboarding.distroStatus.opencodeMissing": "未安装 Kilo", // kilocode_change - "wsl.onboarding.distroStatus.missingTools": "缺少 bash、curl", - "wsl.onboarding.distroStatus.unsupported": "不支持 · 请使用 WSL 2", - "wsl.onboarding.needAnotherDistro": "需要其他发行版?", - "wsl.onboarding.needAnotherDistroHint": "从 WSL 目录安装 Linux 发行版", - "wsl.onboarding.wslNotInstalled.title": "未安装 WSL", - "wsl.onboarding.wslNotInstalled.description": "Kilo 需要 WSL(Windows Subsystem for Linux)才能添加 WSL 服务器", // kilocode_change - "wsl.onboarding.wslUnavailable.title": "WSL 不可用", - "wsl.onboarding.wslUnavailable.description": "Kilo 无法验证此设备上的 WSL。", // kilocode_change - "wsl.onboarding.installWsl": "安装 WSL", - "wsl.onboarding.windowsRestartRequired": "重启 Windows 以完成 WSL 安装,然后重新打开 Kilo。", // kilocode_change - "wsl.onboarding.next": "下一步", - "wsl.onboarding.refresh": "刷新", - "wsl.onboarding.allDistrosAdded": "所有已安装的发行版均已添加。", - "wsl.onboarding.noDistros": "尚未检测到发行版。", - "wsl.onboarding.install": "安装", - "wsl.onboarding.installing": "正在安装...", - "wsl.onboarding.installDistro": "安装发行版", - "wsl.onboarding.searchDistros": "搜索发行版", - "wsl.onboarding.wsl2Required": "需要 WSL 2。", - "wsl.onboarding.toolsRequired": "此发行版需要 bash 和 curl。", - "wsl.onboarding.openTerminal": "打开终端", - "wsl.onboarding.path": "路径:{{path}}", - "wsl.onboarding.notFound": "未找到", - "wsl.onboarding.version": "版本:{{version}}", - "wsl.onboarding.unknown": "未知", - "wsl.onboarding.desktopVersion": "桌面版 {{version}}", - "wsl.onboarding.versionMismatch": "已安装版本与桌面应用版本不一致。", - "wsl.onboarding.adding": "正在添加...", - - "help.tabs.toast.ariaLabel": "标签页功能介绍。使用标签页整理工作和进行中的会话", - "help.tabs.toast.dismiss": "关闭标签页功能介绍", - "help.tabs.title": "标签页功能介绍", - "help.tabs.description": "使用标签页整理工作和进行中的会话", - "help.tabs.date": "7 月 14 日", - "help.tabs.introduction": "Kilo Desktop 现已采用以标签页为核心的设计。", // kilocode_change - "help.tabs.sessions": - "在标签页中开始新会话,或从任意项目打开现有会话。开始处理新任务时打开新标签页,完成后将其关闭。", - "help.tabs.organize": - "保持打开几个标签页,可更轻松地整理进行中的会话。如果打算保留某个标签页,请为其指定一个便于记忆的名称。", - "help.tabs.home": "你可以在新的主页中找到所有会话和项目。选择会话即可在标签页中将其打开。", - "help.tabs.persistence": "重新打开应用时,标签页仍会保持打开。", - "help.tabs.worktrees": - "新设计暂不支持 Git Worktrees,此功能即将推出。如果希望继续使用之前的布局,可在“设置”中切换布局。请注意,新布局将在几周后永久启用。", - - "dialog.project.edit.title": "编辑项目", - "dialog.project.edit.name": "名称", - "dialog.project.edit.icon": "图标", - "dialog.project.edit.icon.alt": "项目图标", - "dialog.project.edit.icon.hint": "点击或拖拽图片", - "dialog.project.edit.icon.recommended": "建议:128x128px", - "dialog.project.edit.color": "颜色", - "dialog.project.edit.color.select": "选择颜色:{{color}}", - "dialog.project.edit.worktree.startup": "工作区启动脚本", - "dialog.project.edit.worktree.startup.description": "创建新工作区 (worktree) 后运行。", - "dialog.project.edit.worktree.startup.placeholder": "例如 bun install", - - "dialog.usageExceeded.dontShowAgain": "不再显示", - - "context.breakdown.title": "上下文细分", - "context.breakdown.note": "输入令牌的大致细分。“其他”包含工具定义和开销。", - "context.breakdown.system": "系统", - "context.breakdown.user": "用户", - "context.breakdown.assistant": "助手", - "context.breakdown.tool": "工具调用", - "context.breakdown.other": "其他", - "context.systemPrompt.title": "系统提示词", - "context.rawMessages.title": "原始消息", - "context.export.session": "导出会话", - - "context.stats.session": "会话", - "context.stats.messages": "消息数", - "context.stats.provider": "提供商", - "context.stats.model": "模型", - "context.stats.limit": "上下文限制", - "context.stats.totalTokens": "令牌总数", - "context.stats.usage": "使用率", - "context.stats.inputTokens": "输入令牌", - "context.stats.outputTokens": "输出令牌", - "context.stats.reasoningTokens": "推理令牌", - "context.stats.cacheTokens": "缓存令牌(读/写)", - "context.stats.userMessages": "用户消息", - "context.stats.assistantMessages": "助手消息", - "context.stats.totalCost": "总成本", - "context.stats.sessionCreated": "创建时间", - "context.stats.lastActivity": "最后活动", - "context.usage.tokens": "令牌数", - "context.usage.usage": "使用率", - "context.usage.cost": "成本", - "context.usage.clickToView": "点击查看上下文", - "context.usage.view": "查看上下文用量", - - "language.en": "English", - "language.zh": "简体中文", - "language.zht": "繁體中文", - "language.ko": "한국어", - "language.de": "Deutsch", - "language.es": "Español", - "language.fr": "Français", - "language.da": "Dansk", - "language.ja": "日本語", - "language.pl": "Polski", - "language.ru": "Русский", - "language.ar": "العربية", - "language.no": "Norsk", - "language.br": "Português (Brasil)", - "language.bs": "Bosanski", - "language.uk": "Українська", - "language.th": "ไทย", - "language.tr": "Türkçe", - - "toast.language.title": "语言", - "toast.language.description": "已切换到{{language}}", - "toast.theme.title": "主题已切换", - "toast.scheme.title": "配色方案", - "toast.workspace.enabled.title": "工作区已启用", - "toast.workspace.enabled.description": "侧边栏现在显示多个工作树", - "toast.workspace.disabled.title": "工作区已禁用", - "toast.workspace.disabled.description": "侧边栏只显示主工作树", - "toast.permissions.autoaccept.on.title": "正在自动接受权限", - "toast.permissions.autoaccept.on.description": "权限请求将被自动批准", - "toast.permissions.autoaccept.off.title": "已停止自动接受权限", - "toast.permissions.autoaccept.off.description": "权限请求将需要批准", - "toast.model.none.title": "未选择模型", - "toast.model.none.description": "请先连接提供商以总结此会话", - "toast.file.loadFailed.title": "加载文件失败", - "toast.file.listFailed.title": "列出文件失败", - "toast.context.noLineSelection.title": "未选择行", - "toast.context.noLineSelection.description": "请先在文件标签中选择行范围。", - "toast.session.share.copyFailed.title": "无法复制链接到剪贴板", - "toast.session.share.success.title": "会话已分享", - "toast.session.share.success.description": "分享链接已复制到剪贴板", - "toast.session.share.failed.title": "分享会话失败", - "toast.session.share.failed.description": "分享会话时发生错误", - "toast.session.unshare.success.title": "已取消分享会话", - "toast.session.unshare.success.description": "会话已成功取消分享", - "toast.session.unshare.failed.title": "取消分享失败", - "toast.session.unshare.failed.description": "取消分享会话时发生错误", - "toast.session.export.success.title": "会话已导出", - "toast.session.export.success.description": "已将会话保存到 {{filename}}", - "toast.session.export.failed.title": "导出会话失败", - "toast.session.export.failed.description": "导出会话时发生错误", - - "toast.session.listFailed.title": "无法加载 {{project}} 的会话", - "toast.update.title": "有可用更新", - "toast.update.description": "Kilo 有新版本 ({{version}}) 可安装。", // kilocode_change - "toast.update.action.installRestart": "安装并重启", - "toast.update.action.notYet": "稍后", - - "error.page.title": "出了点问题", - "error.page.description": "加载应用程序时发生错误。", - "error.page.description.localServerStartup": "启动本地服务器时发生错误。", - "error.page.details.label": "错误详情", - "error.page.action.restart": "重启", - "error.page.action.report": "上报错误", - "error.page.action.reported": "错误已上报", - "error.page.action.exportLogs": "导出日志", - "error.page.action.checking": "检查中...", - "error.page.action.checkUpdates": "检查更新", - "error.page.action.updateTo": "更新到 {{version}}", - "error.page.report.prefix": "请将此错误报告给 Kilo 团队", // kilocode_change - "error.page.report.discord": "在 Discord 上", - "error.page.version": "版本:{{version}}", - "error.dev.rootNotFound": "未找到根元素。你是不是忘了把它添加到 index.html?或者 id 属性拼写错了?", - "error.serverSync.connectFailed": "无法连接到服务器。是否有服务器正在 `{{url}}` 运行?", - - "directory.error.invalidUrl": "URL 中的目录无效。", - - "error.chain.unknown": "未知错误", - "error.chain.causedBy": "原因:", - "error.chain.apiError": "API 错误", - "error.chain.status": "状态:{{status}}", - "error.chain.retryable": "可重试:{{retryable}}", - "error.chain.responseBody": "响应内容:\n{{body}}", - "error.chain.didYouMean": "你是不是想输入:{{suggestions}}", - "error.chain.modelNotFound": "未找到模型:{{provider}}/{{model}}", - "error.chain.checkConfig": "请检查你的配置 (opencode.json) 中的 provider/model 名称", - "error.chain.mcpFailed": 'MCP 服务器 "{{name}}" 启动失败。注意:Kilo 暂不支持 MCP 认证。', // kilocode_change - "error.chain.providerAuthFailed": "提供商认证失败({{provider}}):{{message}}", - "error.chain.providerInitFailed": '无法初始化提供商 "{{provider}}"。请检查凭据和配置。', - "error.chain.configJsonInvalid": "配置文件 {{path}} 不是有效的 JSON(C)", - "error.chain.configJsonInvalidWithMessage": "配置文件 {{path}} 不是有效的 JSON(C):{{message}}", - "error.chain.configDirectoryTypo": - '{{path}} 中的目录 "{{dir}}" 无效。请将目录重命名为 "{{suggestion}}" 或移除它。这是一个常见拼写错误。', - "error.chain.configFrontmatterError": "无法解析 {{path}} 中的 frontmatter:\n{{message}}", - "error.chain.configInvalid": "配置文件 {{path}} 无效", - "error.chain.configInvalidWithMessage": "配置文件 {{path}} 无效:{{message}}", - - "notification.permission.title": "需要权限", - "notification.permission.description": "{{sessionTitle}}({{projectName}})需要权限", - "notification.question.title": "问题", - "notification.question.description": "{{sessionTitle}}({{projectName}})有一个问题", - "notification.action.goToSession": "前往会话", - "notification.session.responseReady.title": "回复已就绪", - "notification.session.error.title": "会话错误", - "notification.session.error.fallbackDescription": "发生错误", - - "home.recentProjects": "最近项目", - "home.empty.title": "没有最近项目", - "home.empty.description": "通过打开本地项目开始使用", - "home.title": "主页", - "home.projects": "项目", - "home.project.add": "添加项目", - "home.recentlyClosed": "最近关闭", - "home.server.collapse": "折叠服务器项目", - "home.server.expand": "展开服务器项目", - "home.sessions.search.placeholder": "搜索会话", - "home.sessions.search.placeholder.scoped": "在 {{scope}} 中搜索会话", - "home.sessions.search.sessions": "会话", - "home.sessions.search.noResults": "未找到与 {{query}} 相关的会话", - "home.sessions.empty": "这里还没有内容", - "home.sessions.empty.description": "创建一个会话以开始。", - "home.sessions.group.today": "今天", - "home.sessions.group.yesterday": "昨天", - "home.sessions.group.older": "更早", - "home.providerTip": "连接 75 个以上的提供商,使用包括 Claude、GPT、Gemini 在内的更多模型", - - "session.tab.session": "会话", - "session.tab.review": "审查", - "session.tab.context": "上下文", - "session.tab.unknown": "未知会话", - "session.panel.reviewAndFiles": "审查和文件", - "session.error.notFound": "找不到此会话", - "session.error.notFound.description": "此标签页指向的会话已不存在于此服务器上。", - "session.error.notFound.closeTab": "关闭标签页", - "session.error.serverConnection": "无法连接到此服务器", - "session.review.filesChanged": "{{count}} 个文件已更改", - "session.review.change.one": "更改", - "session.review.change.other": "更改", - "session.review.loadingChanges": "正在加载更改...", - "session.review.empty": "此会话暂无更改", - "session.review.noVcs": "未检测到 Git,无法显示更改", - "session.review.noSnapshot": "配置中已禁用快照跟踪,因此会话更改不可用", - "session.review.noChanges": "无更改", - "session.review.noUncommittedChanges": "尚无未提交的更改", - "session.review.noBranchChanges": "尚无分支更改", - "session.files.selectToOpen": "选择要打开的文件", - "session.files.all": "所有文件", - "session.files.empty": "无文件", - "session.files.binaryContent": "二进制文件(无法显示内容)", - "session.messages.renderEarlier": "显示更早的消息", - "session.messages.loadingEarlier": "正在加载更早的消息...", - "session.messages.loadEarlier": "加载更早的消息", - "session.messages.loading": "正在加载消息...", - "session.messages.jumpToLatest": "跳转到最新", - "session.context.addToContext": "将 {{selection}} 添加到上下文", - "session.todo.title": "待办事项", - "session.todo.collapse": "折叠", - "session.todo.expand": "展开", - "session.question.minimize": "收起问题", - "session.question.restore": "展开问题", - "session.question.pending.one": "{{count}} 个待处理问题", - "session.question.pending.other": "{{count}} 个待处理问题", - "session.followupDock.summary.one": "{{count}} 条排队消息", - "session.followupDock.summary.other": "{{count}} 条排队消息", - "session.followupDock.sendNow": "立即发送", - "session.followupDock.edit": "编辑", - "session.followupDock.collapse": "折叠排队消息", - "session.followupDock.expand": "展开排队消息", - "session.revertDock.summary.one": "{{count}} 条已回滚消息", - "session.revertDock.summary.other": "{{count}} 条已回滚消息", - "session.revertDock.collapse": "折叠已回滚消息", - "session.revertDock.expand": "展开已回滚消息", - "session.revertDock.restore": "恢复消息", - "session.new.title": "构建任何内容", - "session.new.project.new": "新建项目", - "session.new.project.search": "搜索项目", - "session.new.project.add": "添加项目", - "session.new.worktree.main": "主分支", - "session.new.worktree.mainWithBranch": "主分支({{branch}})", - "session.new.worktree.create": "创建新的 worktree", - "session.new.workspace.runIn": "运行会话的位置", - "session.new.workspace.triggerLocal": "本地", - "session.new.workspace.local": "本地仓库", - "session.new.workspace.existing": "工作区…", - "session.new.git.none": "无 Git", - "session.new.lastModified": "最后修改", - "session.header.search.placeholder": "搜索 {{project}}", - "session.header.searchFiles": "搜索文件", - "session.header.openIn": "打开方式", - "session.header.open.action": "打开 {{app}}", - "session.header.open.ariaLabel": "在 {{app}} 中打开", - "session.header.open.menu": "打开选项", - "session.header.open.copyPath": "复制路径", - - "status.popover.trigger": "状态", - "status.popover.ariaLabel": "服务器配置", - "status.popover.tab.servers": "服务器", - "status.popover.tab.mcp": "MCP", - "status.popover.tab.lsp": "LSP", - "status.popover.tab.plugins": "插件", - "status.popover.action.manageServers": "管理服务器", - - "session.share.popover.title": "发布到网页", - "session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。", - "session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。", - "session.share.action.share": "分享", - "session.share.action.publish": "发布", - "session.share.action.publishing": "正在发布...", - "session.share.action.unpublish": "取消发布", - "session.share.action.unpublishing": "正在取消发布...", - "session.share.action.view": "查看", - "session.share.copy.copied": "已复制", - "session.share.copy.copyLink": "复制链接", - - "lsp.tooltip.none": "没有 LSP 服务器", - "lsp.label.connected": "{{count}} LSP", - - "prompt.loading": "正在加载提示...", - - "terminal.loading": "正在加载终端...", - "terminal.title": "终端", - "terminal.title.numbered": "终端 {{number}}", - "terminal.close": "关闭终端", - "terminal.connectionLost.title": "连接已丢失", - "terminal.connectionLost.description": "终端连接已中断。这可能发生在服务器重启时。", - "terminal.connectTicket.csrfError": "PTY 连接票证因来源或 CSRF 检查被拒绝。请检查服务器 CORS 配置。", - "terminal.connectTicket.statusError": "PTY 连接票证失败,状态为 {{status}}", - - "titlebar.update": "更新", - "titlebar.updateVersion": "更新 {{version}}", - - "common.closeTab": "关闭标签页", - "common.dismiss": "忽略", - "common.requestFailed": "请求失败", - "common.moreOptions": "更多选项", - "common.learnMore": "了解更多", - "common.rename": "重命名", - "common.export": "导出", - "common.reset": "重置", - "common.archive": "归档", - "common.delete": "删除", - "common.close": "关闭", - "common.edit": "编辑", - "common.loadMore": "加载更多", - "common.key.esc": "ESC", - - "sidebar.menu.toggle": "切换菜单", - "sidebar.nav.projectsAndSessions": "项目和会话", - "sidebar.settings": "设置", - "sidebar.help": "帮助", - "sidebar.workspaces.enable": "启用工作区", - "sidebar.workspaces.disable": "禁用工作区", - "sidebar.gettingStarted.title": "入门", - "sidebar.gettingStarted.line1": "Kilo 提供免费模型,你可以立即开始使用。", // kilocode_change - "sidebar.gettingStarted.line2": "连接任意提供商即可使用更多模型,如 Claude、GPT、Gemini 等。", - "sidebar.project.recentSessions": "最近会话", - "sidebar.project.viewAllSessions": "查看全部会话", - "sidebar.project.clearNotifications": "清除通知", - "sidebar.empty.title": "没有打开的项目", - "sidebar.empty.description": "打开一个项目以开始使用", - - "app.name.desktop": "Kilo Desktop", // kilocode_change - - "settings.section.desktop": "桌面", - "settings.section.server": "服务器", - - "settings.tab.general": "通用", - "settings.tab.shortcuts": "快捷键", - - "settings.desktop.section.wsl": "WSL", - "settings.desktop.wsl.title": "WSL 集成", - "settings.desktop.wsl.description": "在 Windows 的 WSL 环境中运行 Kilo 服务器。", // kilocode_change - - "settings.general.section.appearance": "外观", - "settings.general.section.advanced": "高级", - "settings.general.section.notifications": "系统通知", - "settings.general.section.updates": "更新", - "settings.general.section.sounds": "音效", - "settings.general.section.feed": "动态", - "settings.general.section.display": "显示", - "settings.general.row.language.title": "语言", - "settings.general.row.language.description": "更改 Kilo 的显示语言", // kilocode_change - "settings.general.row.shell.title": "终端 Shell", - "settings.general.row.shell.description": "选择终端使用的 shell。兼容的 shell 也会用于智能体工具调用。", - "settings.general.row.shell.autoDefault": "自动(默认)", - "settings.general.row.shell.terminalOnly": "仅终端", - "settings.general.row.appearance.title": "外观", - "settings.general.row.appearance.description": "自定义 Kilo 在你的设备上的外观", // kilocode_change - "settings.general.row.colorScheme.title": "配色方案", - "settings.general.row.colorScheme.description": "选择 Kilo 跟随系统、浅色或深色主题", // kilocode_change - "settings.general.row.theme.title": "主题", - "settings.general.row.theme.description": "自定义 Kilo 的主题。", // kilocode_change - "settings.general.row.font.title": "代码字体", - "settings.general.row.font.description": "自定义代码块使用的字体", - "settings.general.row.terminalFont.title": "终端字体", - "settings.general.row.terminalFont.description": "自定义终端使用的字体", - "settings.general.row.uiFont.title": "界面字体", - "settings.general.row.uiFont.description": "自定义整个界面使用的字体", - "settings.general.row.followup.title": "后续消息行为", - "settings.general.row.followup.description": "选择后续提示是立即引导当前会话还是排队等待", - "settings.general.row.followup.option.queue": "排队", - "settings.general.row.followup.option.steer": "引导", - "settings.general.row.showFileTree.title": "文件树", - "settings.general.row.showFileTree.description": "在会话中显示文件树面板", - "settings.general.row.showNavigation.title": "导航控件", - "settings.general.row.showNavigation.description": "在桌面版标题栏中显示前进和后退按钮", - "settings.general.row.showSearch.title": "命令面板", - "settings.general.row.showSearch.description": "在标题栏中显示搜索和命令面板按钮", - "settings.general.row.showTerminal.title": "终端", - "settings.general.row.showTerminal.description": "在桌面版标题栏中显示终端按钮", - "settings.general.row.showStatus.title": "服务器状态", - "settings.general.row.showStatus.description": "在标题栏中显示服务器状态按钮", - "settings.general.row.mobileTitlebarBottom.title": "底部导航", - "settings.general.row.mobileTitlebarBottom.description": "在移动设备上将标题栏和会话标签页置于屏幕底部", - "settings.general.row.showCustomAgents.title": "显示智能体", - "settings.general.row.showCustomAgents.description": "在输入框中切换智能体。隐藏时默认使用 Build 智能体。", - "settings.general.row.reasoningSummaries.title": "显示推理摘要", - "settings.general.row.reasoningSummaries.description": "在时间线中显示模型推理摘要", - "settings.general.row.shellToolPartsExpanded.title": "展开 Shell 工具调用", - "settings.general.row.shellToolPartsExpanded.description": "默认在时间线中展开 Shell 工具调用详情", - "settings.general.row.editToolPartsExpanded.title": "展开编辑工具调用", - "settings.general.row.editToolPartsExpanded.description": "默认在时间线中展开 edit、write 和 patch 工具调用详情", - "settings.general.row.newInterface.title": "新布局", - "settings.general.row.newInterface.badge": "新", - "settings.general.row.newInterface.description": "使用新的标签页和主页布局。在有限时间内可在不同布局之间切换。", - "settings.general.row.newInterfaceNotice.title": "你现在使用的是新布局", - "settings.general.row.newInterfaceNotice.description": "之前的布局不再可用", - "settings.general.row.newInterfaceNotice.dismiss": "忽略", - "settings.general.row.pinchZoom.title": "双指缩放", - "settings.general.row.pinchZoom.description": "允许使用触控板双指捏合和 Ctrl+滚轮手势缩放", - "settings.general.row.wayland.title": "使用原生 Wayland", - "settings.general.row.wayland.description": "在 Wayland 上禁用 X11 回退。需要重启。", - "settings.general.row.wayland.tooltip": "在混合刷新率显示器的 Linux 系统上,原生 Wayland 可能更稳定。", - "settings.general.row.releaseNotes.title": "发行说明", - "settings.general.row.releaseNotes.description": "更新后显示“新功能”弹窗", - - "settings.updates.row.startup.title": "启动时检查更新", - "settings.updates.row.startup.description": "在 Kilo 启动时自动检查更新", // kilocode_change - "settings.updates.row.check.title": "检查更新", - "settings.updates.row.check.description": "手动检查更新并在有更新时安装", - "settings.updates.action.checkNow": "立即检查", - "settings.updates.action.checking": "正在检查...", - "settings.updates.action.downloading": "正在下载...", - "settings.updates.action.installing": "正在安装...", - "settings.updates.toast.latest.title": "已是最新版本", - "settings.updates.toast.latest.description": "你正在使用最新版本的 Kilo。", // kilocode_change - - "sound.option.none": "无", - "sound.option.alert01": "警报 01", - "sound.option.alert02": "警报 02", - "sound.option.alert03": "警报 03", - "sound.option.alert04": "警报 04", - "sound.option.alert05": "警报 05", - "sound.option.alert06": "警报 06", - "sound.option.alert07": "警报 07", - "sound.option.alert08": "警报 08", - "sound.option.alert09": "警报 09", - "sound.option.alert10": "警报 10", - "sound.option.bipbop01": "Bip-bop 01", - "sound.option.bipbop02": "Bip-bop 02", - "sound.option.bipbop03": "Bip-bop 03", - "sound.option.bipbop04": "Bip-bop 04", - "sound.option.bipbop05": "Bip-bop 05", - "sound.option.bipbop06": "Bip-bop 06", - "sound.option.bipbop07": "Bip-bop 07", - "sound.option.bipbop08": "Bip-bop 08", - "sound.option.bipbop09": "Bip-bop 09", - "sound.option.bipbop10": "Bip-bop 10", - "sound.option.staplebops01": "Staplebops 01", - "sound.option.staplebops02": "Staplebops 02", - "sound.option.staplebops03": "Staplebops 03", - "sound.option.staplebops04": "Staplebops 04", - "sound.option.staplebops05": "Staplebops 05", - "sound.option.staplebops06": "Staplebops 06", - "sound.option.staplebops07": "Staplebops 07", - "sound.option.nope01": "Nope 01", - "sound.option.nope02": "Nope 02", - "sound.option.nope03": "Nope 03", - "sound.option.nope04": "Nope 04", - "sound.option.nope05": "Nope 05", - "sound.option.nope06": "Nope 06", - "sound.option.nope07": "Nope 07", - "sound.option.nope08": "Nope 08", - "sound.option.nope09": "Nope 09", - "sound.option.nope10": "Nope 10", - "sound.option.nope11": "Nope 11", - "sound.option.nope12": "Nope 12", - "sound.option.yup01": "Yup 01", - "sound.option.yup02": "Yup 02", - "sound.option.yup03": "Yup 03", - "sound.option.yup04": "Yup 04", - "sound.option.yup05": "Yup 05", - "sound.option.yup06": "Yup 06", - - "settings.general.notifications.agent.title": "智能体", - "settings.general.notifications.agent.description": "当智能体完成或需要注意时显示系统通知", - "settings.general.notifications.permissions.title": "权限", - "settings.general.notifications.permissions.description": "当需要权限时显示系统通知", - "settings.general.notifications.errors.title": "错误", - "settings.general.notifications.errors.description": "发生错误时显示系统通知", - "settings.general.sounds.agent.title": "智能体", - "settings.general.sounds.agent.description": "当智能体完成或需要注意时播放声音", - "settings.general.sounds.permissions.title": "权限", - "settings.general.sounds.permissions.description": "当需要权限时播放声音", - "settings.general.sounds.errors.title": "错误", - "settings.general.sounds.errors.description": "发生错误时播放声音", - - "settings.shortcuts.title": "键盘快捷键", - "settings.shortcuts.reset.button": "重置为默认值", - "settings.shortcuts.reset.toast.title": "快捷键已重置", - "settings.shortcuts.reset.toast.description": "键盘快捷键已重置为默认设置。", - "settings.shortcuts.conflict.title": "快捷键已被占用", - "settings.shortcuts.conflict.description": "{{keybind}} 已分配给 {{titles}}。", - "settings.shortcuts.unassigned": "未设置", - "settings.shortcuts.pressKeys": "按下按键", - "settings.shortcuts.search.placeholder": "搜索快捷键", - "settings.shortcuts.search.empty": "未找到快捷键", - "settings.shortcuts.group.general": "通用", - "settings.shortcuts.group.session": "会话", - "settings.shortcuts.group.navigation": "导航", - "settings.shortcuts.group.modelAndAgent": "模型与智能体", - "settings.shortcuts.group.terminal": "终端", - "settings.shortcuts.group.prompt": "提示", - - "settings.providers.title": "提供商", - "settings.providers.description": "可在此处配置提供商设置。", - "settings.providers.section.connected": "已连接的提供商", - "settings.providers.connected.empty": "没有已连接的提供商", - "settings.providers.section.popular": "热门提供商", - "settings.providers.tag.environment": "环境", - "settings.providers.tag.config": "配置", - "settings.providers.tag.custom": "自定义", - "settings.providers.tag.other": "其他", - - "settings.models.title": "模型", - "settings.models.description": "可在此处配置模型设置。", - - "settings.agents.title": "智能体", - "settings.agents.description": "可在此处配置智能体设置。", - - "settings.commands.title": "命令", - "settings.commands.description": "可在此处配置命令设置。", - - "settings.mcp.title": "MCP", - "settings.mcp.description": "可在此处配置 MCP 设置。", - - "settings.permissions.title": "权限", - "settings.permissions.description": "控制服务器默认可以使用哪些工具。", - "settings.permissions.section.tools": "工具", - "settings.permissions.toast.updateFailed.title": "更新权限失败", - "settings.permissions.action.allow": "允许", - "settings.permissions.action.ask": "询问", - "settings.permissions.action.deny": "拒绝", - "settings.permissions.tool.read.title": "读取", - "settings.permissions.tool.read.description": "读取文件(匹配文件路径)", - "settings.permissions.tool.edit.title": "编辑", - "settings.permissions.tool.edit.description": "修改文件,包括编辑、写入和应用补丁", - "settings.permissions.tool.glob.title": "Glob", - "settings.permissions.tool.glob.description": "使用 glob 模式匹配文件", - "settings.permissions.tool.grep.title": "Grep", - "settings.permissions.tool.grep.description": "使用正则表达式搜索文件内容", - "settings.permissions.tool.list.title": "列出文件", - "settings.permissions.tool.list.description": "列出目录中的文件", - "settings.permissions.tool.bash.title": "Bash", - "settings.permissions.tool.bash.description": "运行 shell 命令", - "settings.permissions.tool.task.title": "任务", - "settings.permissions.tool.task.description": "启动子智能体", - "settings.permissions.tool.skill.title": "技能", - "settings.permissions.tool.skill.description": "按名称加载技能", - "settings.permissions.tool.lsp.title": "LSP", - "settings.permissions.tool.lsp.description": "运行语言服务器查询", - "settings.permissions.tool.todowrite.title": "更新待办", - "settings.permissions.tool.todowrite.description": "更新待办列表", - "settings.permissions.tool.webfetch.title": "获取网页", - "settings.permissions.tool.webfetch.description": "从 URL 获取内容", - "settings.permissions.tool.websearch.title": "网页搜索", - "settings.permissions.tool.websearch.description": "搜索网页", - "settings.permissions.tool.external_directory.title": "外部目录", - "settings.permissions.tool.external_directory.description": "访问项目目录之外的文件", - "settings.permissions.tool.doom_loop.title": "死循环", - "settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用", - - "session.delete.failed.title": "删除会话失败", - "session.delete.title": "删除会话", - "session.delete.confirm": '删除会话 "{{name}}"?', - "session.delete.button": "删除会话", - - "workspace.new": "新建工作区", - "workspace.type.local": "本地", - "workspace.type.sandbox": "沙盒", - "workspace.create.failed.title": "创建工作区失败", - "workspace.delete.failed.title": "删除工作区失败", - "workspace.resetting.title": "正在重置工作区", - "workspace.resetting.description": "这可能需要一点时间。", - "workspace.reset.failed.title": "重置工作区失败", - "workspace.reset.success.title": "工作区已重置", - "workspace.reset.success.description": "工作区现已与默认分支一致。", - "workspace.error.stillPreparing": "工作区仍在准备中", - "workspace.status.checking": "正在检查未合并的更改...", - "workspace.status.error": "无法验证 Git 状态。", - "workspace.status.clean": "未检测到未合并的更改。", - "workspace.status.dirty": "检测到未合并的更改。", - "workspace.delete.title": "删除工作区", - "workspace.delete.confirm": '删除工作区 "{{name}}"?', - "workspace.delete.button": "删除工作区", - "workspace.reset.title": "重置工作区", - "workspace.reset.confirm": '重置工作区 "{{name}}"?', - "workspace.reset.button": "重置工作区", - "workspace.reset.archived.none": "不会归档任何活跃会话。", - "workspace.reset.archived.one": "将归档 1 个会话。", - "workspace.reset.archived.many": "将归档 {{count}} 个会话。", - "workspace.reset.note": "这将把工作区重置为与默认分支一致。", - "common.open": "打开", - "dialog.releaseNotes.action.getStarted": "开始", - "dialog.releaseNotes.action.next": "下一步", - "dialog.releaseNotes.action.hideFuture": "不再显示", - "dialog.releaseNotes.media.alt": "版本说明预览", - "toast.project.reloadFailed.title": "无法重新加载 {{project}}", - "error.server.invalidConfiguration": "配置无效", - "common.moreCountSuffix": " (还有 {{count}} 个)", - "common.time.justNow": "刚刚", - "common.time.minutesAgo.short": "{{count}}分钟前", - "common.time.hoursAgo.short": "{{count}}小时前", - "common.time.daysAgo.short": "{{count}}天前", - "settings.providers.connected.environmentDescription": "已通过环境变量连接", - "settings.providers.custom.description": "通过基础 URL 添加与 OpenAI 兼容的提供商。", - - "app.server.unreachable": "无法连接到 {{server}}", - "app.server.retrying": "正在自动重试...", - "app.server.otherServers": "其他服务器", - "dialog.server.add.usernamePlaceholder": "用户名", - "dialog.server.add.passwordPlaceholder": "密码", - "server.row.noUsername": "无用户名", - "session.review.noVcs.createGit.title": "创建 Git 仓库", - "session.review.noVcs.createGit.description": "在此项目中跟踪、审查和撤消更改", - "session.review.noVcs.createGit.actionLoading": "正在创建 Git 仓库...", - "session.review.noVcs.createGit.action": "创建 Git 仓库", - "session.todo.progress": "已完成 {{done}} 个任务(共 {{total}} 个)", - "session.question.progress": "{{current}}/{{total}} 个问题", - "session.header.open.finder": "访达", - "session.header.open.fileExplorer": "文件资源管理器", - "session.header.reveal.finder": "在访达中显示", - "session.header.reveal.fileExplorer": "在文件资源管理器中显示", - "session.header.reveal.containingFolder": "打开所在文件夹", - "session.header.open.fileManager": "文件管理器", - "session.header.open.app.vscode": "VS Code", - "session.header.open.app.cursor": "Cursor", - "session.header.open.app.zed": "Zed", - "session.header.open.app.textmate": "TextMate", - "session.header.open.app.antigravity": "Antigravity", - "session.header.open.app.terminal": "终端", - "session.header.open.app.iterm2": "iTerm2", - "session.header.open.app.ghostty": "Ghostty", - "session.header.open.app.warp": "Warp", - "session.header.open.app.xcode": "Xcode", - "session.header.open.app.androidStudio": "Android Studio", - "session.header.open.app.powershell": "PowerShell", - "session.header.open.app.sublimeText": "Sublime Text", - "debugBar.ariaLabel": "开发性能诊断", - "debugBar.na": "不适用", - "debugBar.nav.label": "NAV", - "debugBar.nav.tip": "最后一次完成的涉及会话页面的路由转换,从路由器启动到稳定后的第一次绘制。", - "debugBar.fps.label": "FPS", - "debugBar.fps.tip": "过去 5 秒内的滚动帧率。", - "debugBar.frame.label": "FRAME", - "debugBar.frame.tip": "过去 5 秒内最差的帧时间。", - "debugBar.jank.label": "JANK", - "debugBar.jank.tip": "过去 5 秒内超过 32ms 的帧。", - "debugBar.long.label": "LONG", - "debugBar.long.tip": "过去 5 秒内的阻塞时间和长任务计数。最大任务:{{max}}。", - "debugBar.delay.label": "DELAY", - "debugBar.delay.tip": "过去 5 秒内观察到的最差输入延迟。", - "debugBar.inp.label": "INP", - "debugBar.inp.tip": "过去 5 秒内的近似交互持续时间。这类似于 INP,而非官方的 Web Vitals INP。", - "debugBar.cls.label": "CLS", - "debugBar.cls.tip": "当前应用生命周期的累积布局偏移。", - "debugBar.mem.label": "MEM", - "debugBar.mem.tipUnavailable": "使用的 JS 堆与堆限制。仅限 Chromium。", - "debugBar.mem.tip": "使用的 JS 堆与堆限制。{{used}} / {{limit}}。", - "debugBar.focus.label": "FOCUS", - "debugBar.focus.tip": "强制所有交互元素显示焦点样式", - "debugBar.focus.on": "开", - "debugBar.focus.off": "关", - "debugBar.direction.label": "DIR", - "debugBar.direction.tip": "在不更改所选语言的情况下,强制设置整个应用的布局方向", - "debugBar.direction.ltr": "LTR", - "debugBar.direction.rtl": "RTL", - "common.key.ctrl": "Ctrl", - "common.key.alt": "Alt", - "common.key.shift": "Shift", - "common.key.meta": "Meta", - "common.key.space": "空格", - "common.key.backspace": "退格", - "common.key.enter": "回车", - "common.key.tab": "Tab", - "common.key.delete": "Delete", - "common.key.home": "Home", - "common.key.end": "End", - "common.key.pageUp": "Page Up", - "common.key.pageDown": "Page Down", - "common.key.insert": "Insert", - "common.unknown": "未知", - "error.page.circular": "[循环]", - "error.serverSDK.noServerAvailable": "无可用服务器", - "error.serverSDK.serverNotAvailable": "服务器不可用", - "error.childStore.persistedCacheCreateFailed": "创建持久化缓存失败", - "error.childStore.persistedProjectMetadataCreateFailed": "创建持久化项目元数据失败", - "error.childStore.persistedProjectIconCreateFailed": "创建持久化项目图标失败", - "error.childStore.storeCreateFailed": "创建存储失败", - "terminal.connectionLost.abnormalClose": "WebSocket 异常关闭:{{code}}", -} satisfies Partial> diff --git a/packages/cli/package.json b/packages/cli/package.json deleted file mode 100644 index fc36b362305..00000000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "@kilocode/cli", - "version": "7.4.20", - "type": "module", - "license": "MIT", - "bin": { - "lildax": "./bin/lildax.cjs" - }, - "files": [ - "bin" - ], - "scripts": { - "build": "bun run script/build.ts", - "dev": "bun run src/index.ts", - "typecheck": "tsgo --noEmit" - }, - "dependencies": { - "@effect/platform-node": "catalog:", - "@opencode-ai/core": "workspace:*", - "@kilocode/sdk": "workspace:*", - "@opencode-ai/server": "workspace:*", - "@opencode-ai/tui": "workspace:*", - "@opentui/core": "catalog:", - "@opentui/solid": "catalog:", - "@parcel/watcher": "2.5.1", - "effect": "catalog:", - "solid-js": "catalog:" - }, - "devDependencies": { - "@opencode-ai/script": "workspace:*", - "@tsconfig/bun": "catalog:", - "@types/bun": "catalog:", - "@typescript/native-preview": "catalog:" - } -} diff --git a/packages/codemode/package.json b/packages/codemode/package.json index d90649cdbf4..fab4911e331 100644 --- a/packages/codemode/package.json +++ b/packages/codemode/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/codemode", - "version": "7.4.20", + "version": "7.4.23", "description": "Effect-native confined code execution over schema-described tools", "private": true, "type": "module", @@ -11,7 +11,8 @@ }, "scripts": { "typecheck": "tsgo --noEmit", - "test": "bun test" + "test": "bun test", + "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml" }, "dependencies": { "acorn": "8.15.0", @@ -22,5 +23,6 @@ "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:" - } + }, + "peerDependencies": {} } diff --git a/packages/console/app/package.json b/packages/console/app/package.json deleted file mode 100644 index 9ab0ad88c89..00000000000 --- a/packages/console/app/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "@opencode-ai/console-app", - "version": "7.4.20", - "type": "module", - "license": "MIT", - "scripts": { - "typecheck": "tsgo --noEmit", - "dev": "vite dev --host 0.0.0.0", - "dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RtuLNE7fOCwHSD4mewwzFejyytjdGoSDK7CAvhbffwaZnPbNb2rwJICw6LTOXCmWO320fSNXvb5NzI08RZVkAxd00syfqrW7t bun sst shell --stage=dev bun dev", - "build": "bun ./script/generate-sitemap.ts && vite build && bun ../../opencode/script/schema.ts ./.output/public/config.json ./.output/public/tui.json", - "start": "vite start" - }, - "dependencies": { - "@cloudflare/vite-plugin": "1.15.2", - "@ibm/plex": "6.4.1", - "@jsx-email/render": "1.1.1", - "@kobalte/core": "catalog:", - "@openauthjs/openauth": "catalog:", - "@opencode-ai/console-core": "workspace:*", - "@opencode-ai/console-mail": "workspace:*", - "@opencode-ai/console-resource": "workspace:*", - "@opencode-ai/ui": "workspace:*", - "@smithy/eventstream-codec": "4.2.7", - "@smithy/util-utf8": "4.2.0", - "@solidjs/meta": "catalog:", - "@solidjs/router": "catalog:", - "@solidjs/start": "catalog:", - "@stripe/stripe-js": "8.6.1", - "@upstash/redis": "1.38.0", - "chart.js": "4.5.1", - "nitro": "3.0.1-alpha.1", - "solid-js": "catalog:", - "solid-list": "0.3.0", - "solid-stripe": "0.8.1", - "vite": "catalog:", - "zod": "catalog:" - }, - "devDependencies": { - "@types/bun": "catalog:", - "@typescript/native-preview": "catalog:", - "@webgpu/types": "0.1.54", - "typescript": "catalog:", - "wrangler": "4.50.0" - }, - "engines": { - "node": ">=22" - } -} diff --git a/packages/console/app/src/i18n/en.ts b/packages/console/app/src/i18n/en.ts deleted file mode 100644 index b3f4dc13607..00000000000 --- a/packages/console/app/src/i18n/en.ts +++ /dev/null @@ -1,848 +0,0 @@ -export const dict = { - "nav.github": "GitHub", - "nav.docs": "Docs", - "nav.data": "Data", - "nav.changelog": "Changelog", - "nav.discord": "Discord", - "nav.x": "X", - "nav.enterprise": "Enterprise", - "nav.zen": "Zen", - "nav.go": "Go", - "nav.login": "Login", - "nav.free": "Download", - "nav.home": "Home", - "nav.openMenu": "Open menu", - "nav.getStartedFree": "Get started for free", - "nav.logoAlt": "OpenCode", - - "nav.context.copyLogo": "Copy logo as SVG", - "nav.context.copyWordmark": "Copy wordmark as SVG", - "nav.context.brandAssets": "Brand assets", - - "footer.github": "GitHub", - "footer.docs": "Docs", - "footer.changelog": "Changelog", - "footer.feishu": "Feishu", - "footer.discord": "Discord", - "footer.x": "X", - - "legal.brand": "Brand", - "legal.privacy": "Privacy", - "legal.terms": "Terms", - - "email.title": "Be the first to know when we release new products", - "email.subtitle": "Join the waitlist for early access.", - "email.placeholder": "Email address", - "email.subscribe": "Subscribe", - "email.success": "Almost done, check your inbox and confirm your email address", - - "notFound.title": "Not Found | opencode", - "notFound.heading": "404 - Page Not Found", - "notFound.home": "Home", - "notFound.docs": "Docs", - "notFound.github": "GitHub", - "notFound.discord": "Discord", - "notFound.logoLightAlt": "opencode logo light", - "notFound.logoDarkAlt": "opencode logo dark", - - "user.logout": "Logout", - - "auth.callback.error.codeMissing": "No authorization code found.", - - "workspace.select": "Select workspace", - "workspace.createNew": "+ Create New Workspace", - "workspace.modal.title": "Create New Workspace", - "workspace.modal.placeholder": "Enter workspace name", - - "common.cancel": "Cancel", - "common.creating": "Creating...", - "common.create": "Create", - "common.contactUs": "Contact us", - - "common.videoUnsupported": "Your browser does not support the video tag.", - "common.figure": "Fig {{n}}.", - "common.faq": "FAQ", - "common.learnMore": "Learn more", - - "error.invalidPlan": "Invalid plan", - "error.workspaceRequired": "Workspace ID is required", - "error.alreadySubscribed": "This workspace already has a subscription", - "error.limitRequired": "Limit is required.", - "error.monthlyLimitInvalid": "Set a valid monthly limit.", - "error.workspaceNameRequired": "Workspace name is required.", - "error.nameTooLong": "Name must be 255 characters or less.", - "error.emailRequired": "Email is required", - "error.roleRequired": "Role is required", - "error.idRequired": "ID is required", - "error.nameRequired": "Name is required", - "error.providerRequired": "Provider is required", - "error.apiKeyRequired": "API key is required", - "error.modelRequired": "Model is required", - "error.reloadAmountMin": "Reload amount must be at least ${{amount}}", - "error.reloadTriggerMin": "Balance trigger must be at least ${{amount}}", - - "app.meta.description": "OpenCode - The open source coding agent.", - - "home.title": "OpenCode | The open source AI coding agent", - - "temp.title": "opencode | AI coding agent built for the terminal", - "temp.hero.title": "The AI coding agent built for the terminal", - "temp.zen": "opencode zen", - "temp.getStarted": "Get Started", - "temp.feature.native.title": "Native TUI", - "temp.feature.native.body": "A responsive, native, themeable terminal UI", - "temp.feature.zen.beforeLink": "A", - "temp.feature.zen.link": "curated list of models", - "temp.feature.zen.afterLink": "provided by opencode", - "temp.feature.models.beforeLink": "Supports 75+ LLM providers through", - "temp.feature.models.afterLink": ", including local models", - "temp.screenshot.caption": "opencode TUI with the tokyonight theme", - "temp.screenshot.alt": "opencode TUI with tokyonight theme", - "temp.logoLightAlt": "opencode logo light", - "temp.logoDarkAlt": "opencode logo dark", - - "home.banner.badge": "New", - "home.banner.text": "Introducing tabs for desktop.", - "home.banner.platforms": "Available on macOS, Windows, and Linux", - "home.banner.downloadNow": "Download now", - "home.banner.downloadBetaNow": "Download the desktop beta now", - - "home.promo.title": "Introducing Tabs for Desktop", - "home.promo.body": "Organize your work and active sessions with tabs.", - "home.promo.cta": "Download the latest to get started.", - "home.promo.close": "Dismiss desktop app announcement", - - "home.hero.title": "The open source AI coding agent", - "home.hero.subtitle.a": "Free models included or connect any model from any provider,", - "home.hero.subtitle.b": "including Claude, GPT, Gemini and more.", - - "home.install.ariaLabel": "Install options", - - "home.what.title": "What is OpenCode?", - "home.what.body": "OpenCode is an open source agent that helps you write code in your terminal, IDE, or desktop.", - "home.what.lsp.title": "LSP enabled", - "home.what.lsp.body": "Automatically loads the right LSPs for the LLM", - "home.what.multiSession.title": "Multi-session", - "home.what.multiSession.body": "Start multiple agents in parallel on the same project", - "home.what.shareLinks.title": "Share links", - "home.what.shareLinks.body": "Share a link to any session for reference or to debug", - "home.what.copilot.title": "GitHub Copilot", - "home.what.copilot.body": "Log in with GitHub to use your Copilot account", - "home.what.chatgptPlus.title": "ChatGPT Plus/Pro", - "home.what.chatgptPlus.body": "Log in with OpenAI to use your ChatGPT Plus or Pro account", - "home.what.anyModel.title": "Any model", - "home.what.anyModel.body": "75+ LLM providers through Models.dev, including local models", - "home.what.anyEditor.title": "Any editor", - "home.what.anyEditor.body": "Available as a terminal interface, desktop app, and IDE extension", - "home.what.readDocs": "Read docs", - - "home.growth.title": "The open source AI coding agent", - "home.growth.body": - "With over {{stars}} GitHub stars, {{contributors}} contributors, and over {{commits}} commits, OpenCode is used and trusted by over {{monthlyUsers}} developers every month.", - "home.growth.githubStars": "GitHub Stars", - "home.growth.contributors": "Contributors", - "home.growth.monthlyDevs": "Monthly Devs", - - "home.privacy.title": "Built for privacy first", - "home.privacy.body": - "OpenCode does not store any of your code or context data, so that it can operate in privacy sensitive environments.", - "home.privacy.learnMore": "Learn more about", - "home.privacy.link": "privacy", - - "home.faq.q1": "What is OpenCode?", - "home.faq.a1": - "OpenCode is an open source agent that helps you write and run code with any AI model. It's available as a terminal-based interface, desktop app, or IDE extension.", - "home.faq.q2": "How do I use OpenCode?", - "home.faq.a2.before": "The easiest way to get started is to read the", - "home.faq.a2.link": "intro", - "home.faq.q3": "Do I need extra AI subscriptions to use OpenCode?", - "home.faq.a3.p1": - "Not necessarily, OpenCode comes with a set of free models that you can use without creating an account.", - "home.faq.a3.p2.beforeZen": "Aside from these, you can use any of the popular coding models by creating a", - "home.faq.a3.p2.afterZen": " account.", - "home.faq.a3.p3": - "While we encourage users to use Zen, OpenCode also works with all popular providers such as OpenAI, Anthropic, xAI etc.", - "home.faq.a3.p4.beforeLocal": "You can even connect your", - "home.faq.a3.p4.localLink": "local models", - "home.faq.q4": "Can I use my existing AI subscriptions with OpenCode?", - "home.faq.a4.p1": - "Yes, OpenCode supports subscription plans from all major providers. You can use your Claude Pro/Max, ChatGPT Plus/Pro, or GitHub Copilot subscriptions.", - "home.faq.q5": "Can I only use OpenCode in the terminal?", - "home.faq.a5.beforeDesktop": "Not anymore! OpenCode is now available as an app for your", - "home.faq.a5.desktop": "desktop", - "home.faq.a5.and": "and", - "home.faq.a5.web": "web", - "home.faq.q6": "How much does OpenCode cost?", - "home.faq.a6": - "OpenCode is 100% free to use. It also comes with a set of free models. There might be additional costs if you connect any other provider.", - "home.faq.q7": "What about data and privacy?", - "home.faq.a7.p1": "Your data and information is only stored when you use our free models or create sharable links.", - "home.faq.a7.p2.beforeModels": "Learn more about", - "home.faq.a7.p2.modelsLink": "our models", - "home.faq.a7.p2.and": "and", - "home.faq.a7.p2.shareLink": "share pages", - "home.faq.q8": "Is OpenCode open source?", - "home.faq.a8.p1": "Yes, OpenCode is fully open source. The source code is public on", - "home.faq.a8.p2": "under the", - "home.faq.a8.mitLicense": "MIT License", - "home.faq.a8.p3": - ", meaning anyone can use, modify, or contribute to its development. Anyone from the community can file issues, submit pull requests, and extend functionality.", - - "home.zenCta.title": "Access reliable optimized models for coding agents", - "home.zenCta.body": - "Zen gives you access to a handpicked set of AI models that OpenCode has tested and benchmarked specifically for coding agents. No need to worry about inconsistent performance and quality across providers, use validated models that work.", - "home.zenCta.link": "Learn about Zen", - - "zen.title": "OpenCode Zen | A curated set of reliable optimized models for coding agents", - "zen.hero.title": "Reliable optimized models for coding agents", - "zen.hero.body": - "Zen gives you access to a curated set of AI models that OpenCode has tested and benchmarked specifically for coding agents. No need to worry about inconsistent performance and quality, use validated models that work.", - - "zen.faq.q1": "What is OpenCode Zen?", - "zen.faq.a1": - "Zen is a curated set of AI models tested and benchmarked for coding agents created by the team behind OpenCode.", - "zen.faq.q2": "What makes Zen more accurate?", - "zen.faq.a2": - "Zen only provides models that have been specifically tested and benchmarked for coding agents. You wouldn't use a butter knife to cut steak, don't use poor models for coding.", - "zen.faq.q3": "Is Zen cheaper?", - "zen.faq.a3": - "Zen is not for profit. Zen passes through the costs from the model providers to you. The higher Zen's usage the more OpenCode can negotiate better rates and pass those to you.", - "zen.faq.q4": "How much does Zen cost?", - "zen.faq.a4.p1.beforePricing": "Zen", - "zen.faq.a4.p1.pricingLink": "charges per request", - "zen.faq.a4.p1.afterPricing": "with zero markups, so you pay exactly what the model provider charges.", - "zen.faq.a4.p2.beforeAccount": "Your total cost depends on usage, and you can set monthly spend limits in your", - "zen.faq.a4.p2.accountLink": "account", - "zen.faq.a4.p3": "To cover costs, OpenCode adds only a small payment processing fee of $1.23 per $20 balance top-up.", - "zen.faq.q5": "What about data and privacy?", - "zen.faq.a5.beforeExceptions": - "All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the", - "zen.faq.a5.exceptionsLink": "following exceptions", - "zen.faq.q6": "Can I set spend limits?", - "zen.faq.a6": "Yes, you can set monthly spending limits in your account.", - "zen.faq.q7": "Can I cancel?", - "zen.faq.a7": "Yes, you can disable billing at any time and use your remaining balance.", - "zen.faq.q8": "Can I use Zen with other coding agents?", - "zen.faq.a8": - "While Zen works great with OpenCode, you can use Zen with any agent. Follow the setup instructions in your preferred coding agent.", - - "zen.cta.start": "Get started with Zen", - "zen.pricing.title": "Add $20 Pay as you go balance", - "zen.pricing.fee": "(+$1.23 card processing fee)", - "zen.pricing.body": "Use with any agent. Set monthly spend limits. Cancel any time.", - "zen.problem.title": "What problem is Zen solving?", - "zen.problem.body": - "There are so many models available, but only a few work well with coding agents. Most providers configure them differently with varying results.", - "zen.problem.subtitle": "We're fixing this for everyone, not just OpenCode users.", - "zen.problem.item1": "Testing select models and consulting their teams", - "zen.problem.item2": "Working with providers to ensure they're delivered properly", - "zen.problem.item3": "Benchmarking all model-provider combinations we recommend", - "zen.how.title": "How Zen works", - "zen.how.body": "While we suggest you use Zen with OpenCode, you can use Zen with any agent.", - "zen.how.step1.title": "Sign up and add $20 balance", - "zen.how.step1.beforeLink": "follow the", - "zen.how.step1.link": "setup instructions", - "zen.how.step2.title": "Use Zen with transparent pricing", - "zen.how.step2.link": "pay per request", - "zen.how.step2.afterLink": "with zero markups", - "zen.how.step3.title": "Auto-top up", - "zen.how.step3.body": "when your balance reaches $5 we'll automatically add $20", - "zen.privacy.title": "Your privacy is important to us", - "zen.privacy.beforeExceptions": - "All Zen models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the", - "zen.privacy.exceptionsLink": "following exceptions", - - "go.title": "OpenCode Go | Low cost coding models for everyone", - "go.banner.text": "DeepSeek V4 Flash gets 2× usage limits for a limited time", - "go.meta.description": - "Go starts at $5 for your first month, then $10/month, with generous usage limits and reliable access to leading coding models.", - "go.hero.title": "Low cost coding models for everyone", - "go.hero.body": - "Go brings agentic coding to programmers around the world. Offering generous limits and reliable access to the most capable open-source models, so you can build with powerful agents without worrying about cost or availability.", - - "go.cta.start": "Subscribe to Go", - "go.cta.template": "{{text}} {{price}}", - "go.cta.text": "Subscribe to Go", - "go.cta.price": "$10/month", - "go.cta.promo": "$5 first month", - "go.pricing.body": "Use with any agent. $5 first month, then $10/month. Top up credit if needed. Cancel any time.", - "go.graph.free": "Free", - "go.graph.freePill": "Big Pickle and free models", - "go.graph.go": "Go", - "go.graph.label": "Requests per 5 hour", - "go.graph.tick": "{{n}}x", - "go.graph.usageLimits": "Usage limits", - "go.graph.aria": "Requests per 5h: {{free}} vs {{go}}", - - "go.testimonials.brand.zen": "Zen", - "go.testimonials.brand.go": "Go", - "go.testimonials.handle": "@OpenCode", - "go.testimonials.dax.name": "Dax Raad", - "go.testimonials.dax.title": "ex-CEO, Terminal Products", - "go.testimonials.dax.quoteAfter": "has been life changing, it's truly a no-brainer.", - "go.testimonials.jay.name": "Jay V", - "go.testimonials.jay.title": "ex-Founder, SEED, PM, Melt, Pop, Dapt, Cadmus, and ViewPoint", - "go.testimonials.jay.quoteBefore": "4 out of 5 people on our team love using", - "go.testimonials.jay.quoteAfter": ".", - "go.testimonials.adam.name": "Adam Elmore", - "go.testimonials.adam.title": "ex-Hero, AWS", - "go.testimonials.adam.quoteBefore": "I can't recommend", - "go.testimonials.adam.quoteAfter": "enough. Seriously, it's really good.", - "go.testimonials.david.name": "David Hill", - "go.testimonials.david.title": "ex-Head of Design, Laravel", - "go.testimonials.david.quoteBefore": "With", - "go.testimonials.david.quoteAfter": "I know all the models are tested and perfect for coding agents.", - "go.testimonials.frank.name": "Frank Wang", - "go.testimonials.frank.title": "ex-Intern, Nvidia (4 times)", - "go.testimonials.frank.quote": "I wish I was still at Nvidia.", - "go.problem.title": "What problem is Go solving?", - "go.problem.body": - "We're focused on bringing the OpenCode experience to as many people as possible. OpenCode Go is a low cost subscription: $5 for your first month, then $10/month. It provides generous limits and reliable access to the most capable open source models.", - "go.problem.subtitle": " ", - "go.problem.item1": "Low cost subscription pricing", - "go.problem.item2": "Generous limits and reliable access", - "go.problem.item3": "Built for as many programmers as possible", - "go.problem.item4": "A curated model lineup tested for agentic coding", - "go.how.title": "How Go works", - "go.how.body": "Go starts at $5 for your first month, then $10/month. You can use it with OpenCode or any agent.", - "go.how.step1.title": "Create an account", - "go.how.step1.beforeLink": "follow the", - "go.how.step1.link": "setup instructions", - "go.how.step2.title": "Subscribe to Go", - "go.how.step2.link": "$5 first month", - "go.how.step2.afterLink": "then $10/month with generous limits", - "go.how.step3.title": "Start coding", - "go.how.step3.body": "with reliable access to open-source models", - "go.privacy.title": "Your privacy is important to us", - "go.privacy.body": - "The plan is designed primarily for international users, with models hosted in the US, EU, and Singapore for stable global access.", - "go.privacy.contactAfter": "if you have any questions.", - "go.privacy.beforeExceptions": - "Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the", - "go.privacy.exceptionsLink": "following exceptions", - "go.faq.q1": "What is OpenCode Go?", - "go.faq.a1": - "Go is a low-cost subscription that gives you reliable access to capable open-source models for agentic coding.", - "go.faq.q2": "What models does Go include?", - "go.faq.a2": "Go includes the models listed below, with generous limits and reliable access.", - "go.faq.q3": "Is Go the same as Zen?", - "go.faq.a3": - "No. Zen is pay-as-you-go, while Go starts at $5 for your first month, then $10/month, with generous limits and reliable access to a curated model lineup.", - "go.faq.q4": "How much does Go cost?", - "go.faq.a4.p1.beforePricing": "Go costs", - "go.faq.a4.p1.pricingLink": "$5 first month", - "go.faq.a4.p1.afterPricing": "then $10/month with generous limits.", - "go.faq.a4.p2.beforeAccount": "You can manage your subscription in your", - "go.faq.a4.p2.accountLink": "account", - "go.faq.a4.p3": "Cancel any time.", - "go.faq.q5": "What about data and privacy?", - "go.faq.a5.model": "Model", - "go.faq.a5.training": "Model training", - "go.faq.a5.retention": "Data retention", - "go.faq.a5.retention30": "30 days", - "go.faq.a5.retention0": "0 days", - "go.faq.a5.used": "Used", - "go.faq.a5.notUsed": "Not used", - "go.faq.a5.noAgreement": "No agreement", - "go.faq.a5.grokRetention": - "ZDR disables important API features that depend on stored data, including the stateful Responses API, Files and Collections, and the Batch API.", - "go.faq.a5.gptRetention": - "Abuse monitoring logs are generated for all API feature usage and retained for up to 30 days.", - "go.faq.a5.deepseekRetention": - "ZDR agreement is renewed monthly. The current agreement is valid through August 31, 2026.", - "go.faq.a5.learnMore": "Learn more", - - "go.faq.a5.beforeExceptions": - "Go models are hosted in the US. Providers follow a zero-retention policy and do not use your data for model training, with the", - "go.faq.a5.exceptionsLink": "following exceptions", - "go.faq.q6": "Can I top up credit?", - "go.faq.a6": "If you need more usage, you can top up credit in your account.", - "go.faq.q7": "Can I cancel?", - "go.faq.a7": "Yes, you can cancel any time.", - "go.faq.q8": "Can I use Go with other coding agents?", - "go.faq.a8": "Yes, you can use Go with any agent. Follow the setup instructions in your preferred coding agent.", - - "go.faq.q9": "What is the difference between free models and Go?", - "go.faq.a9": - "Free models include Big Pickle plus promotional models available at the time, with a quota of 200 requests/day. Go offers a curated model lineup with higher request quotas enforced across rolling windows (5-hour, weekly, and monthly), roughly equivalent to $12 per 5 hours, $30 per week, and $60 per month (actual request counts vary by model and usage).", - - "zen.api.error.rateLimitExceeded": "Rate limit exceeded. Please try again later.", - "zen.api.error.modelNotSupported": "Model {{model}} is not supported", - "zen.api.error.modelFormatNotSupported": "Model {{model}} is not supported for format {{format}}", - "zen.api.error.noProviderAvailable": "No provider available", - "zen.api.error.providerNotSupported": "Provider {{provider}} not supported", - "zen.api.error.missingApiKey": "Missing API key.", - "zen.api.error.invalidApiKey": "Invalid API key.", - "zen.api.error.requestBlockedByUpstreamProvider": "Request blocked by upstream provider.", - "zen.api.error.subscriptionQuotaExceeded": "Subscription quota exceeded. Retry in {{retryIn}}.", - "zen.api.error.goSubscriptionRollingLimitExceeded": - "5-hour usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}", - "zen.api.error.goSubscriptionWeeklyLimitExceeded": - "Weekly usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}", - "zen.api.error.goSubscriptionMonthlyLimitExceeded": - "Monthly usage limit reached. Resets in {{retryIn}}. To continue using this model now, enable usage from your available balance: {{consoleGoUrl}}", - "zen.api.error.noPaymentMethod": "No payment method. Add a payment method here: {{billingUrl}}", - "zen.api.error.insufficientBalance": "Insufficient balance. Manage your billing here: {{billingUrl}}", - "zen.api.error.workspaceMonthlyLimitReached": - "Your workspace has reached its monthly spending limit of ${{amount}}. Manage your limits here: {{billingUrl}}", - "zen.api.error.userMonthlyLimitReached": - "You have reached your monthly spending limit of ${{amount}}. Manage your limits here: {{membersUrl}}", - "zen.api.error.modelDisabled": "Model is disabled", - "zen.api.error.regionNotAllowed": - "The latest version of this model is only available hosted in China and requires explicit opt in: {{consoleGoUrl}}", - "zen.api.error.trialEnded": - "Free promotion has ended for {{model}}. You can continue using the model by subscribing to OpenCode Go - {{link}}", - - "black.meta.title": "OpenCode Black | Access all the world's best coding models", - "black.meta.description": "Get access to Claude, GPT, Gemini and more with OpenCode Black subscription plans.", - "black.hero.title": "Access all the world's best coding models", - "black.hero.subtitle": "Including Claude, GPT, Gemini and more", - "black.title": "OpenCode Black | Pricing", - "black.paused": "Black plan enrollment is temporarily paused.", - "black.plan.icon20": "Black 20 plan", - "black.plan.icon100": "Black 100 plan", - "black.plan.icon200": "Black 200 plan", - "black.plan.multiplier100": "5x more usage than Black 20", - "black.plan.multiplier200": "20x more usage than Black 20", - "black.price.perMonth": "per month", - "black.price.perPersonBilledMonthly": "per person billed monthly", - "black.terms.1": "Your subscription will not start immediately", - "black.terms.2": "You will be added to the waitlist and activated soon", - "black.terms.3": "Your card will only be charged when your subscription is activated", - "black.terms.4": "Usage limits apply, heavily automated use may reach limits sooner", - "black.terms.5": "Subscriptions are for individuals, contact Enterprise for teams", - "black.terms.6": "Limits may be adjusted and plans may be discontinued in the future", - "black.terms.7": "Cancel your subscription at any time", - "black.action.continue": "Continue", - "black.finePrint.beforeTerms": "Prices shown don't include applicable tax", - "black.finePrint.terms": "Terms of Service", - "black.workspace.title": "OpenCode Black | Select Workspace", - "black.workspace.selectPlan": "Select a workspace for this plan", - "black.workspace.name": "Workspace {{n}}", - "black.subscribe.title": "Subscribe to OpenCode Black", - "black.subscribe.paymentMethod": "Payment method", - "black.subscribe.loadingPaymentForm": "Loading payment form...", - "black.subscribe.selectWorkspaceToContinue": "Select a workspace to continue", - "black.subscribe.failurePrefix": "Uh oh!", - "black.subscribe.error.generic": "An error occurred", - "black.subscribe.error.invalidPlan": "Invalid plan", - "black.subscribe.error.workspaceRequired": "Workspace ID is required", - "black.subscribe.error.alreadySubscribed": "This workspace already has a subscription", - "black.subscribe.processing": "Processing...", - "black.subscribe.submit": "Subscribe ${{plan}}", - "black.subscribe.form.chargeNotice": "You will only be charged when your subscription is activated", - "black.subscribe.success.title": "You're on the OpenCode Black waitlist", - "black.subscribe.success.subscriptionPlan": "Subscription plan", - "black.subscribe.success.planName": "OpenCode Black {{plan}}", - "black.subscribe.success.amount": "Amount", - "black.subscribe.success.amountValue": "${{plan}} per month", - "black.subscribe.success.paymentMethod": "Payment method", - "black.subscribe.success.dateJoined": "Date joined", - "black.subscribe.success.chargeNotice": "Your card will be charged when your subscription is activated", - - "workspace.nav.zen": "Zen", - "workspace.nav.go": "Go", - "workspace.nav.usage": "Usage", - "workspace.nav.apiKeys": "API Keys", - "workspace.nav.members": "Members", - "workspace.nav.billing": "Billing", - "workspace.nav.settings": "Settings", - - "workspace.home.banner.beforeLink": "Reliable optimized models for coding agents.", - "workspace.lite.banner.beforeLink": "Low cost coding models for everyone.", - "workspace.home.billing.loading": "Loading...", - "workspace.home.billing.enable": "Enable billing", - "workspace.home.billing.currentBalance": "Current balance", - - "workspace.newUser.feature.tested.title": "Tested & Verified Models", - "workspace.newUser.feature.tested.body": - "We've benchmarked and tested models specifically for coding agents to ensure the best performance.", - "workspace.newUser.feature.quality.title": "Highest Quality", - "workspace.newUser.feature.quality.body": - "Access models configured for optimal performance - no downgrades or routing to cheaper providers.", - "workspace.newUser.feature.lockin.title": "No Lock-in", - "workspace.newUser.feature.lockin.body": - "Use Zen with any coding agent, and continue using other providers with opencode whenever you want.", - "workspace.newUser.copyApiKey": "Copy API key", - "workspace.newUser.copyKey": "Copy Key", - "workspace.newUser.copied": "Copied!", - "workspace.newUser.step.enableBilling": "Enable billing", - "workspace.newUser.step.login.before": "Run", - "workspace.newUser.step.login.after": "and select opencode", - "workspace.newUser.step.pasteKey": "Paste your API key", - "workspace.newUser.step.models.before": "Start opencode and run", - "workspace.newUser.step.models.after": "to select a model", - - "workspace.models.title": "Models", - "workspace.models.subtitle.beforeLink": "Manage which models workspace members can access.", - "workspace.models.table.model": "Model", - "workspace.models.table.enabled": "Enabled", - - "workspace.providers.title": "Bring Your Own Key", - "workspace.providers.subtitle": "Configure your own API keys from AI providers.", - "workspace.providers.placeholder": "Enter {{provider}} API key ({{prefix}}...)", - "workspace.providers.configure": "Configure", - "workspace.providers.edit": "Edit", - "workspace.providers.delete": "Delete", - "workspace.providers.saving": "Saving...", - "workspace.providers.save": "Save", - "workspace.providers.table.provider": "Provider", - "workspace.providers.table.apiKey": "API Key", - - "workspace.usage.title": "Usage History", - "workspace.usage.subtitle": "Recent API usage and costs.", - "workspace.usage.empty": "Make your first API call to get started.", - "workspace.usage.table.date": "Date", - "workspace.usage.table.model": "Model", - "workspace.usage.table.input": "Input", - "workspace.usage.table.output": "Output", - "workspace.usage.table.cost": "Cost", - "workspace.usage.table.session": "Session", - "workspace.usage.breakdown.input": "Input", - "workspace.usage.breakdown.cacheRead": "Cache Read", - "workspace.usage.breakdown.cacheWrite": "Cache Write", - "workspace.usage.breakdown.output": "Output", - "workspace.usage.breakdown.reasoning": "Reasoning", - "workspace.usage.subscription": "Black (${{amount}})", - "workspace.usage.lite": "Go (${{amount}})", - "workspace.usage.byok": "BYOK (${{amount}})", - - "workspace.cost.title": "Cost", - "workspace.cost.subtitle": "Usage costs broken down by model.", - "workspace.cost.allModels": "All Models", - "workspace.cost.allKeys": "All Keys", - "workspace.cost.deletedSuffix": "(deleted)", - "workspace.cost.empty": "No usage data available for the selected period.", - "workspace.cost.subscriptionShort": "sub", - - "workspace.keys.title": "API Keys", - "workspace.keys.subtitle": "Manage your API keys for accessing opencode services.", - "workspace.keys.create": "Create API Key", - "workspace.keys.placeholder": "Enter key name", - "workspace.keys.empty": "Create an opencode Gateway API key", - "workspace.keys.table.name": "Name", - "workspace.keys.table.key": "Key", - "workspace.keys.table.createdBy": "Created By", - "workspace.keys.table.lastUsed": "Last Used", - "workspace.keys.copyApiKey": "Copy API key", - "workspace.keys.delete": "Delete", - - "workspace.members.title": "Members", - "workspace.members.subtitle": "Manage workspace members and their permissions.", - "workspace.members.invite": "Invite Member", - "workspace.members.inviting": "Inviting...", - "workspace.members.beta.beforeLink": "Workspaces are free for teams during the beta.", - "workspace.members.form.invitee": "Invitee", - "workspace.members.form.emailPlaceholder": "Enter email", - "workspace.members.form.role": "Role", - "workspace.members.form.monthlyLimit": "Monthly spending limit", - "workspace.members.noLimit": "No limit", - "workspace.members.noLimitLowercase": "no limit", - "workspace.members.invited": "invited", - "workspace.members.edit": "Edit", - "workspace.members.delete": "Delete", - "workspace.members.saving": "Saving...", - "workspace.members.save": "Save", - "workspace.members.table.email": "Email", - "workspace.members.table.role": "Role", - "workspace.members.table.monthLimit": "Month limit", - "workspace.members.role.admin": "Admin", - "workspace.members.role.adminDescription": "Can manage models, members, and billing", - "workspace.members.role.member": "Member", - "workspace.members.role.memberDescription": "Can only generate API keys for themselves", - - "workspace.settings.title": "Settings", - "workspace.settings.subtitle": "Update your workspace name and preferences.", - "workspace.settings.workspaceName": "Workspace name", - "workspace.settings.defaultName": "Default", - "workspace.settings.updating": "Updating...", - "workspace.settings.save": "Save", - "workspace.settings.edit": "Edit", - - "workspace.billing.title": "Billing", - "workspace.billing.subtitle.beforeLink": "Manage payment methods.", - "workspace.billing.contactUs": "Contact us", - "workspace.billing.subtitle.afterLink": "if you have any questions.", - "workspace.billing.currentBalance": "Current Balance", - "workspace.billing.add": "Add $", - "workspace.billing.enterAmount": "Enter amount", - "workspace.billing.loading": "Loading...", - "workspace.billing.addAction": "Add", - "workspace.billing.addBalance": "Add Balance", - "workspace.billing.alipay": "Alipay", - "workspace.billing.wechat": "WeChat Pay", - "workspace.billing.linkedToStripe": "Linked to Stripe", - "workspace.billing.manage": "Manage", - "workspace.billing.enable": "Enable Billing", - - "workspace.monthlyLimit.title": "Monthly Limit", - "workspace.monthlyLimit.subtitle": "Set a monthly usage limit for your account.", - "workspace.monthlyLimit.placeholder": "50", - "workspace.monthlyLimit.setting": "Setting...", - "workspace.monthlyLimit.set": "Set", - "workspace.monthlyLimit.edit": "Edit Limit", - "workspace.monthlyLimit.noLimit": "No usage limit set.", - "workspace.monthlyLimit.currentUsage.beforeMonth": "Current usage for", - "workspace.monthlyLimit.currentUsage.beforeAmount": "is $", - - "workspace.redeem.title": "Redeem Coupon", - "workspace.redeem.subtitle": "Redeem a coupon code to claim credits or perks.", - "workspace.redeem.placeholder": "Enter coupon code", - "workspace.redeem.redeem": "Redeem", - "workspace.redeem.redeeming": "Redeeming...", - "workspace.redeem.success": "Coupon redeemed successfully.", - - "workspace.reload.title": "Auto Reload", - "workspace.reload.disabled.before": "Auto reload is", - "workspace.reload.disabled.state": "disabled", - "workspace.reload.disabled.after": "Enable to automatically reload when balance is low.", - "workspace.reload.enabled.before": "Auto reload is", - "workspace.reload.enabled.state": "enabled", - "workspace.reload.enabled.middle": "We'll reload", - "workspace.reload.processingFee": "processing fee", - "workspace.reload.enabled.after": "when balance reaches", - "workspace.reload.edit": "Edit", - "workspace.reload.enable": "Enable", - "workspace.reload.enableAutoReload": "Enable Auto Reload", - "workspace.reload.reloadAmount": "Reload $", - "workspace.reload.whenBalanceReaches": "When balance reaches $", - "workspace.reload.saving": "Saving...", - "workspace.reload.save": "Save", - "workspace.reload.failedAt": "Reload failed at", - "workspace.reload.reason": "Reason:", - "workspace.reload.updatePaymentMethod": "Please update your payment method and try again.", - "workspace.reload.retrying": "Retrying...", - "workspace.reload.retry": "Retry", - "workspace.reload.error.paymentFailed": "Payment failed.", - - "workspace.payments.title": "Payments History", - "workspace.payments.subtitle": "Recent payment transactions.", - "workspace.payments.table.date": "Date", - "workspace.payments.table.paymentId": "Payment ID", - "workspace.payments.table.amount": "Amount", - "workspace.payments.table.receipt": "Receipt", - "workspace.payments.type.credit": "credit", - "workspace.payments.type.subscription": "subscription", - "workspace.payments.view": "View", - - "workspace.black.loading": "Loading...", - "workspace.black.time.day": "day", - "workspace.black.time.days": "days", - "workspace.black.time.hour": "hour", - "workspace.black.time.hours": "hours", - "workspace.black.time.minute": "minute", - "workspace.black.time.minutes": "minutes", - "workspace.black.time.fewSeconds": "a few seconds", - "workspace.black.subscription.title": "Subscription", - "workspace.black.subscription.message": "You are subscribed to OpenCode Black for ${{plan}} per month.", - "workspace.black.subscription.manage": "Manage Subscription", - "workspace.black.subscription.rollingUsage": "5-hour Usage", - "workspace.black.subscription.weeklyUsage": "Weekly Usage", - "workspace.black.subscription.resetsIn": "Resets in", - "workspace.black.subscription.useBalance": "Use your available balance after reaching the usage limits", - "workspace.black.waitlist.title": "Waitlist", - "workspace.black.waitlist.joined": "You are on the waitlist for the ${{plan}} per month OpenCode Black plan.", - "workspace.black.waitlist.ready": "We're ready to enroll you into the ${{plan}} per month OpenCode Black plan.", - "workspace.black.waitlist.leave": "Leave Waitlist", - "workspace.black.waitlist.leaving": "Leaving...", - "workspace.black.waitlist.left": "Left", - "workspace.black.waitlist.enroll": "Enroll", - "workspace.black.waitlist.enrolling": "Enrolling...", - "workspace.black.waitlist.enrolled": "Enrolled", - "workspace.black.waitlist.enrollNote": - "When you click Enroll, your subscription starts immediately and your card will be charged.", - - "workspace.lite.loading": "Loading...", - "workspace.lite.time.day": "day", - "workspace.lite.time.days": "days", - "workspace.lite.time.hour": "hour", - "workspace.lite.time.hours": "hours", - "workspace.lite.time.minute": "minute", - "workspace.lite.time.minutes": "minutes", - "workspace.lite.time.fewSeconds": "a few seconds", - "workspace.lite.subscription.message": "You are subscribed to OpenCode Go.", - "workspace.lite.subscription.manage": "Manage Subscription", - "workspace.lite.subscription.rollingUsage": "Rolling Usage", - "workspace.lite.subscription.weeklyUsage": "Weekly Usage", - "workspace.lite.subscription.monthlyUsage": "Monthly Usage", - "workspace.lite.subscription.resetsIn": "Resets in", - "workspace.lite.subscription.useBalance": "Use your available balance after reaching the usage limits", - "workspace.lite.subscription.selectProvider": - 'Select "OpenCode Go" as the provider in your opencode configuration to use Go models.', - "workspace.lite.providers.title": "Providers", - "workspace.lite.providers.description": "Control which providers are used for routing.", - "workspace.lite.providers.useChina": "Enable models hosted in China", - "workspace.lite.black.message": - "You're currently subscribed to OpenCode Black or on the waitlist. Please unsubscribe first if you'd like to switch to Go.", - "workspace.lite.other.message": - "Another member in this workspace is already subscribed to OpenCode Go. Only one member per workspace can subscribe.", - "workspace.lite.promo.description": - "OpenCode Go starts at {{price}}, then $10/month, and provides reliable access to popular open coding models with generous usage limits.", - "workspace.lite.promo.price": "$5 for your first month", - "workspace.lite.promo.modelsTitle": "What's Included", - "workspace.lite.promo.footer": - "The plan is designed primarily for international users and provides stable global access. Pricing and usage limits may change as we learn from early usage and feedback.", - "workspace.lite.promo.subscribe": "Subscribe to Go", - "workspace.lite.promo.subscribing": "Redirecting...", - "workspace.lite.promo.otherMethods": "Other payment methods", - "workspace.lite.promo.selectMethod": "Select payment method", - - "workspace.referral.copyLink": "Copy Link", - "workspace.referral.copied": "Copied", - "workspace.referral.overview.title": "Invite friends", - "workspace.referral.overview.subtitle": "Earn $5 when a friend subscribes. They’ll get $5 too.", - "workspace.referral.instructions.share": "Share your referral link", - "workspace.referral.instructions.subscribe": "Your friend joins and subscribes to Go", - "workspace.referral.instructions.claim": "You both get a $5 usage credit to apply toward your Go usage limits", - "workspace.referral.rewards.title": "Referral rewards", - "workspace.referral.rewards.description": "Apply available referral credits toward your Go usage.", - "workspace.referral.rewards.subtitle": "{{applied}} / {{total}} rewards applied.", - "workspace.referral.rewards.empty": "No referral rewards yet.", - "workspace.referral.table.reward": "Reward", - "workspace.referral.table.referral": "Description", - "workspace.referral.table.date": "Date", - "workspace.referral.reward.description.inviter": "Invited {{email}}", - "workspace.referral.reward.description.invitee": "Invited by {{email}}", - "workspace.referral.reward.action.subscribeUnlock": "Subscribe to unlock", - "workspace.referral.reward.action.view": "View Reward", - "workspace.referral.reward.action.applied": "Reward Applied", - "workspace.referral.reward.source.pendingInviter": "Waiting for them to subscribe", - "workspace.referral.reward.source.pendingInvitee": "Subscribe to unlock reward", - "workspace.referral.reward.source.available": "Reward ready to apply", - "workspace.referral.reward.source.applied": "Reward applied", - "workspace.referral.reward.status.applied": "Reward Applied", - "workspace.referral.reward.status.pendingInviter": "Subscribe to unlock", - "workspace.referral.reward.status.pendingInvitee": "Subscribe to unlock", - "workspace.referral.apply.noGo": "Subscribe to unlock", - "workspace.referral.apply.preview": "View Reward", - "workspace.referral.apply.action": "Apply", - "workspace.referral.apply.confirmTitle": "Apply reward", - "workspace.referral.apply.confirmBody": "Apply {{amount}} to reduce this workspace's current usage.", - "workspace.referral.apply.confirmAction": "Apply", - - "download.title": "OpenCode | Download", - "download.meta.description": "Download OpenCode for macOS, Windows, and Linux", - "download.hero.title": "Download OpenCode Desktop", - "download.hero.subtitle": "Available in Beta for macOS, Windows, and Linux", - "download.hero.button": "Download for {{os}}", - "download.section.terminal": "OpenCode Terminal", - "download.section.desktop": "OpenCode Desktop", - "download.section.extensions": "OpenCode Extensions", - "download.section.integrations": "OpenCode Integrations", - "download.action.download": "Download", - "download.action.install": "Install", - - "download.platform.macosAppleSilicon": "macOS (Apple Silicon)", - "download.platform.macosIntel": "macOS (Intel)", - "download.platform.windowsX64": "Windows (x64)", - "download.platform.linuxDeb": "Linux (.deb)", - "download.platform.linuxRpm": "Linux (.rpm)", - - "download.faq.a3.beforeLocal": - "Not necessarily, but probably. You'll need an AI subscription if you want to connect OpenCode to a paid provider, although you can work with", - "download.faq.a3.localLink": "local models", - "download.faq.a3.afterLocal.beforeZen": "for free. While we encourage users to use", - "download.faq.a3.afterZen": ", OpenCode works with all popular providers such as OpenAI, Anthropic, xAI etc.", - - "download.faq.a5.p1": "OpenCode is 100% free to use.", - "download.faq.a5.p2.beforeZen": - "Any additional costs will come from your subscription to a model provider. While OpenCode works with any model provider, we recommend using", - "download.faq.a5.p2.afterZen": ".", - - "download.faq.a6.p1": "Your data and information is only stored when you create sharable links in OpenCode.", - "download.faq.a6.p2.beforeShare": "Learn more about", - "download.faq.a6.shareLink": "share pages", - - "enterprise.title": "OpenCode | Enterprise solutions for your organisation", - "enterprise.meta.description": "Contact OpenCode for enterprise solutions", - "enterprise.hero.title": "Your code is yours", - "enterprise.hero.body1": - "OpenCode operates securely inside your organization with no data or context stored and no licensing restrictions or ownership claims. Start a trial with your team, then deploy it across your organization by integrating it with your SSO and internal AI gateway.", - "enterprise.hero.body2": "Let us know how we can help.", - "enterprise.form.name.label": "Full name", - "enterprise.form.name.placeholder": "Jeff Bezos", - "enterprise.form.role.label": "Role", - "enterprise.form.role.placeholder": "Executive Chairman", - "enterprise.form.company.label": "Company", - "enterprise.form.company.placeholder": "Acme Inc", - "enterprise.form.email.label": "Company email", - "enterprise.form.email.placeholder": "jeff@amazon.com", - "enterprise.form.phone.label": "Phone number", - "enterprise.form.phone.placeholder": "+1 234 567 8900", - "enterprise.form.message.label": "What problem are you trying to solve?", - "enterprise.form.message.placeholder": "We need help with...", - "enterprise.form.send": "Send", - "enterprise.form.sending": "Sending...", - "enterprise.form.success": "Message sent, we'll be in touch soon.", - "enterprise.form.success.submitted": "Form submitted successfully.", - "enterprise.form.error.allFieldsRequired": "All fields are required.", - "enterprise.form.error.invalidEmailFormat": "Invalid email format.", - "enterprise.form.error.internalServer": "Internal server error.", - "enterprise.faq.title": "FAQ", - "enterprise.faq.q1": "What is OpenCode Enterprise?", - "enterprise.faq.a1": - "OpenCode Enterprise is for organizations that want to ensure that their code and data never leaves their infrastructure. It can do this by using a centralized config that integrates with your SSO and internal AI gateway.", - "enterprise.faq.q2": "How do I get started with OpenCode Enterprise?", - "enterprise.faq.a2": - "Simply start with an internal trial with your team. OpenCode by default does not store your code or context data, making it easy to get started. Then contact us to discuss pricing and implementation options.", - "enterprise.faq.q3": "How does enterprise pricing work?", - "enterprise.faq.a3": - "We offer per-seat enterprise pricing. If you have your own LLM gateway, we do not charge for tokens used. For further details, contact us for a custom quote based on your organization's needs.", - "enterprise.faq.q4": "Is my data secure with OpenCode Enterprise?", - "enterprise.faq.a4": - "Yes. OpenCode does not store your code or context data. All processing happens locally or through direct API calls to your AI provider. With central config and SSO integration, your data remains secure within your organization's infrastructure.", - "enterprise.faq.q5": "Where can I find your security and compliance documentation?", - "enterprise.faq.a5.before": - "Our Trust Center has everything: SOC 2 Type 2 report, security policies, subprocessor list, and answers to common security questions. Visit", - "enterprise.faq.a5.after": "to review or request documents under NDA.", - - "brand.title": "OpenCode | Brand", - "brand.meta.description": "OpenCode brand guidelines", - "brand.heading": "Brand guidelines", - "brand.subtitle": "Resources and assets to help you work with the OpenCode brand.", - "brand.downloadAll": "Download all assets", - - "changelog.title": "OpenCode | Changelog", - "changelog.meta.description": "OpenCode release notes and changelog", - "changelog.hero.title": "Changelog", - "changelog.hero.subtitle": "New updates and improvements to OpenCode", - "changelog.empty": "No changelog entries found.", - "changelog.viewJson": "View JSON", - - "bench.list.title": "Benchmark", - "bench.list.heading": "Benchmarks", - "bench.list.table.agent": "Agent", - "bench.list.table.model": "Model", - "bench.list.table.score": "Score", - "bench.submission.error.allFieldsRequired": "All fields are required.", - - "bench.detail.title": "Benchmark - {{task}}", - "bench.detail.notFound": "Task not found", - "bench.detail.na": "N/A", - "bench.detail.labels.agent": "Agent", - "bench.detail.labels.model": "Model", - "bench.detail.labels.task": "Task", - "bench.detail.labels.repo": "Repo", - "bench.detail.labels.from": "From", - "bench.detail.labels.to": "To", - "bench.detail.labels.prompt": "Prompt", - "bench.detail.labels.commit": "Commit", - "bench.detail.labels.averageDuration": "Average Duration", - "bench.detail.labels.averageScore": "Average Score", - "bench.detail.labels.averageCost": "Average Cost", - "bench.detail.labels.summary": "Summary", - "bench.detail.labels.runs": "Runs", - "bench.detail.labels.score": "Score", - "bench.detail.labels.base": "Base", - "bench.detail.labels.penalty": "Penalty", - "bench.detail.labels.weight": "weight", - "bench.detail.table.run": "Run", - "bench.detail.table.score": "Score (Base - Penalty)", - "bench.detail.table.cost": "Cost", - "bench.detail.table.duration": "Duration", - "bench.detail.run.title": "Run {{n}}", - "bench.detail.rawJson": "Raw JSON", -} as const - -export type Key = keyof typeof dict -export type Dict = Record diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts deleted file mode 100644 index eacd0a0e884..00000000000 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ /dev/null @@ -1,1297 +0,0 @@ -import type { APIEvent } from "@solidjs/start/server" -import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js" -import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" -import { BillingTable, LiteTable, SubscriptionTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" -import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" -import { getMonthlyBounds, getWeekBounds } from "@opencode-ai/console-core/util/date.js" -import { Identifier } from "@opencode-ai/console-core/identifier.js" -import { Billing } from "@opencode-ai/console-core/billing.js" -import { Actor } from "@opencode-ai/console-core/actor.js" -import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" -import { ZenData } from "@opencode-ai/console-core/model.js" -import { Subscription } from "@opencode-ai/console-core/subscription.js" -import { BlackData } from "@opencode-ai/console-core/black.js" -import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" -import { ModelTable } from "@opencode-ai/console-core/schema/model.sql.js" -import { ProviderTable } from "@opencode-ai/console-core/schema/provider.sql.js" -import { logger } from "./logger" -import { - AuthError, - CreditsError, - MonthlyLimitError, - UserLimitError, - ModelError, - RegionError, - RateLimitError, - FreeUsageLimitError, - GoUsageLimitError, - BlackUsageLimitError, -} from "./error" -import { - buildCostChunk, - createBodyConverter, - createStreamPartConverter, - createResponseConverter, - UsageInfo, -} from "./provider/provider" -import { anthropicHelper } from "./provider/anthropic" -import { googleHelper } from "./provider/google" -import { openaiHelper } from "./provider/openai" -import { oaCompatHelper } from "./provider/openai-compatible" -import { createRateLimiter as createIpRateLimiter } from "./ipRateLimiter" -import { createRateLimiter as createKeyRateLimiter } from "./keyRateLimiter" -import { createTrialLimiter } from "./trialLimiter" -import { createStickyTracker } from "./stickyProviderTracker" -import { LiteData } from "@opencode-ai/console-core/lite.js" -import { Resource } from "@opencode-ai/console-resource" -import { i18n, type Key } from "~/i18n" -import { localeFromRequest } from "~/lib/language" -import { createModelTpmLimiter } from "./modelTpmLimiter" -import { createModelTpsLimiter } from "./modelTpsLimiter" -import { createProviderBudgetTracker } from "./providerBudgetTracker" -import { accumulateUsage, HOT_WORKSPACES } from "./usageBatcher" -import { Workspace } from "@opencode-ai/console-core/workspace.js" -import { countryFromRequest } from "~/lib/request-country" - -type ZenData = Awaited> -type RetryOptions = { - excludeProviders: string[] - retryCount: number -} -type BillingSource = "anonymous" | "free" | "byok" | "subscription" | "lite" | "balance" - -function resolve(text: string, params?: Record) { - if (!params) return text - return text.replace(/\{\{(\w+)\}\}/g, (raw, key) => { - const value = params[key] - if (value === undefined || value === null) return raw - return String(value) - }) -} - -export async function handler( - input: APIEvent, - opts: { - format: ZenData.Format - modelList: "lite" | "full" - parseApiKey: (headers: Headers) => string | undefined - parseModel: (url: string, body: any) => string - parseVariant: (url: string, body: any) => string | undefined - parseIsStream: (url: string, body: any) => boolean - }, -) { - type AuthInfo = Awaited> - type ModelInfo = Awaited> - type ProviderInfo = Awaited> - type CostInfo = ReturnType - - const MAX_FAILOVER_RETRIES = 3 - const MAX_RETRYABLE_STATUS_RETRIES = 3 - const dict = i18n(localeFromRequest(input.request)) - const t = (key: Key, params?: Record) => resolve(dict[key], params) - const ADMIN_WORKSPACES = [ - "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // anomaly - "wrk_01K6W1A3VE0KMNVSCQT43BG2SX", // benchmark - "wrk_01KKZDKDWCS1VTJF8QTX62DD50", // contributors - ] - - try { - const url = input.request.url - const body = await input.request.json() - const model = opts.parseModel(url, body) - const variant = opts.parseVariant(url, body) - const isStream = opts.parseIsStream(url, body) - const rawIp = input.request.headers.get("x-real-ip") ?? "" - const ip = rawIp.includes(":") ? rawIp.split(":").slice(0, 4).join(":") : rawIp - const rawZenApiKey = opts.parseApiKey(input.request.headers) - const zenApiKey = rawZenApiKey === "public" ? undefined : rawZenApiKey - const sessionId = input.request.headers.get("x-kilo-session") ?? "" - const requestId = input.request.headers.get("x-kilo-request") ?? "" - const ocClient = input.request.headers.get("x-kilo-client") ?? "" - const projectId = input.request.headers.get("x-kilo-project") ?? "" - const userAgent = input.request.headers.get("user-agent") ?? "" - logger.metric({ - is_stream: isStream, - session: sessionId, - request: requestId, - client: ocClient, - user_agent: userAgent, - "model.variant": variant, - "model.tier": opts.modelList === "full" ? "zen" : "go", - }) - const zenData = ZenData.list(opts.modelList) - const modelInfo = validateModel(zenData, model) - const trialLimiter = createTrialLimiter(modelInfo.trialProvider, ip) - const trialProviders = await trialLimiter?.check() - const rateLimiter = modelInfo.allowAnonymous - ? createIpRateLimiter(modelInfo.id, modelInfo.rateLimit, ip, input.request) - : createKeyRateLimiter(modelInfo.id, modelInfo.rateLimit, zenApiKey, input.request) - await rateLimiter?.check() - const authInfo = await authenticate(modelInfo, zenApiKey) - const allowedRegions = authInfo?.region - ? authInfo.region - : await (async () => { - if (!authInfo) return - return Actor.provide("system", { workspaceID: authInfo.workspaceID }, () => - Workspace.setDefaultRegion({ country: countryFromRequest(input.request) }), - ) - })() - if ( - authInfo && - opts.modelList === "lite" && - modelInfo.id === "deepseek-v4-flash" && - !allowedRegions?.includes("cn") - ) - throw new RegionError( - t("zen.api.error.regionNotAllowed", { - consoleGoUrl: `https://opencode.ai/workspace/${authInfo.workspaceID}/go`, - }), - ) - const stickyId = sessionId ? sessionId : (authInfo?.workspaceID ?? ip) - const stickyTracker = createStickyTracker(modelInfo.id, modelInfo.stickyProvider, stickyId) - const stickyProvider = await stickyTracker?.get() - const billingSource = validateBilling(authInfo, modelInfo) - logger.metric({ source: billingSource }) - const modelTpmLimiter = createModelTpmLimiter(modelInfo.providers) - const modelTpmLimits = await modelTpmLimiter?.check() - const modelTpsLimiter = createModelTpsLimiter(modelInfo.providers) - const modelTpsLimits = await modelTpsLimiter?.check() - const providerBudgetTracker = createProviderBudgetTracker( - modelInfo.providers.map((provider) => ({ ...zenData.providers[provider.id], ...provider })), - ) - const providerBudget = await providerBudgetTracker?.check() - - const retriableRequest = async (retry: RetryOptions = { excludeProviders: [], retryCount: 0 }) => { - const providerInfo = selectProvider( - model, - zenData, - authInfo, - modelInfo, - stickyId, - trialProviders, - retry, - stickyProvider, - modelTpmLimits, - modelTpsLimits, - providerBudget, - ) - validateModelSettings(billingSource, authInfo) - updateProviderKey(authInfo, providerInfo) - logger.metric({ - provider: providerInfo.id, - "provider.model": providerInfo.model, - shallowProvider: providerInfo.id, - "shallowProvider.model": providerInfo.model, - }) - - const startTimestamp = Date.now() - const reqUrl = providerInfo.modifyUrl(providerInfo.api, isStream) - const reqBody = JSON.stringify( - providerInfo.modifyBody({ - ...createBodyConverter(opts.format, providerInfo.format)(body), - model: providerInfo.model, - ...(() => { - const replacer = (obj: Record): Record => - Object.fromEntries( - Object.entries(obj).flatMap(([k, v]) => { - if (Array.isArray(v)) return [[k, v]] - if (typeof v === "object") return [[k, replacer(v)]] - if (typeof v === "string") { - if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo.workspaceID]] : [] - if (v === "$org") - return authInfo?.workspaceID ? [[k, authInfo.workspaceID.replace("wrk_", "org_")]] : [] - if (v === "$user") return stickyId ? [[k, stickyId]] : [] - if (v.startsWith("$header.")) { - const headerValue = input.request.headers.get(v.slice(8)) - return headerValue ? [[k, headerValue]] : [] - } - } - return [[k, v]] - }), - ) - return replacer(providerInfo.payloadModifier ?? {}) - })(), - }), - ) - logger.debug("REQUEST URL: " + reqUrl) - logger.debug("REQUEST: " + reqBody.substring(0, 300) + "...") - const isNewInference = - providerInfo.id.startsWith("console.") || - providerInfo.id.startsWith("console-go.") || - providerInfo.id.startsWith("inf.") || - providerInfo.id.startsWith("inf-go.") - const res = await fetchWithRetryableStatus( - reqUrl, - { - method: "POST", - headers: (() => { - const headers = new Headers(input.request.headers) - providerInfo.modifyHeaders(headers, providerInfo.apiKey, stickyId) - Object.entries(providerInfo.headerModifier ?? {}).forEach(([k, v]) => { - if (v === "$ip") return headers.set(k, ip) - if (v === "$caller") return headers.set(k, stickyId) - if (v === "$session") return headers.set(k, sessionId) - if (v === "$model") return headers.set(k, model) - if (v === "$request") return headers.set(k, requestId) - if (v === "$project") return headers.set(k, projectId) - if (v === "$workspace") { - if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID) - return - } - if (v === "$org") { - if (authInfo?.workspaceID) headers.set(k, authInfo.workspaceID.replace("wrk_", "org_")) - return - } - headers.set(k, v) - }) - headers.delete("host") - headers.delete("content-length") - headers.delete("x-kilo-request") - headers.delete("x-kilo-session") - headers.delete("x-kilo-project") - headers.delete("x-kilo-client") - return headers - })(), - body: reqBody, - // Propagate caller disconnects to the upstream provider request so - // abandoned Console requests do not leave orphaned inference work open. - signal: input.request.signal, - }, - { count: isNewInference ? MAX_RETRYABLE_STATUS_RETRIES : 0 }, - ) - - if (isNewInference) { - const resEndpointId = res.headers.get("x-kilo-endpoint-id") - const resEndpointModelId = res.headers.get("x-kilo-upstream-model-id") - if (resEndpointId && resEndpointModelId) - logger.metric({ - provider: resEndpointId, - "provider.model": resEndpointModelId, - }) - } - - if (res.status !== 200) { - logger.metric({ - "llm.error.code": res.status, - "llm.error.message": res.statusText, - }) - } - - // Try another provider => stop retrying if using fallback provider - if ( - //!isNewInference && - res.status !== 200 && - // ie. 400 error is usually provider error like malformed request - res.status !== 400 && - // ie. openai 404 error: Item with id 'msg_0ead8b004a3b165d0069436a6b6834819896da85b63b196a3f' not found. - !(modelInfo.id.startsWith("gpt-") && res.status === 404) && - // ie. cannot change codex model providers mid-session - modelInfo.stickyProvider !== "strict" && - modelInfo.fallbackProvider && - providerInfo.id !== modelInfo.fallbackProvider - ) { - return retriableRequest({ - excludeProviders: [...retry.excludeProviders, providerInfo.id], - retryCount: retry.retryCount + 1, - }) - } - - return { providerInfo, reqBody, res, startTimestamp } - } - - const { providerInfo, reqBody, res, startTimestamp } = await retriableRequest() - - // Store sticky provider - if (res.status === 200) await stickyTracker?.set(providerInfo.id) - - // Temporarily change 404 to 400 status code b/c solid start automatically override 404 response - const resStatus = res.status === 404 ? 400 : res.status - - // Scrub response headers - const resHeaders = new Headers() - const keepHeaders = ["content-type", "cache-control"] - for (const [k, v] of res.headers.entries()) { - if (keepHeaders.includes(k.toLowerCase())) { - resHeaders.set(k, v) - } - } - logger.debug("STATUS: " + res.status + " " + res.statusText) - - // Handle non-streaming response - if (!isStream || [400, 404, 429, 529].includes(res.status)) { - const json = await res.json() - await rateLimiter?.track() - const usage = providerInfo.extractUsage(json) - if (usage) { - const usageInfo = providerInfo.normalizeUsage(usage) - const costInfo = calculateCost(modelInfo, usageInfo) - await trialLimiter?.track(usageInfo) - await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo) - await providerBudgetTracker?.track(providerInfo.id, providerInfo.budgetPriority, costInfo.totalCostInCent) - await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) - await reload(billingSource, authInfo, costInfo) - json.cost = calculateOccurredCost(billingSource, costInfo) - } - if (res.status === 400) { - logger.metric({ "error.response": JSON.stringify(json) }) - } - if (json.error?.message) { - json.error.message = `Error from provider${providerInfo.displayName ? ` (${providerInfo.displayName})` : ""}: ${json.error.message}` - } - - const responseConverter = createResponseConverter(providerInfo.format, opts.format) - const body = JSON.stringify(responseConverter(json)) - logger.metric({ response_length: body.length }) - logger.debug("RESPONSE: " + body) - return new Response(body, { - status: resStatus, - statusText: res.statusText, - headers: resHeaders, - }) - } - - // Handle streaming response - const streamConverter = createStreamPartConverter(providerInfo.format, opts.format) - const usageParser = providerInfo.createUsageParser() - const binaryDecoder = providerInfo.createBinaryStreamDecoder() - let reader: ReadableStreamDefaultReader | undefined - const stream = new ReadableStream({ - start(c) { - reader = res.body?.getReader() - const decoder = new TextDecoder() - const encoder = new TextEncoder() - - let buffer = "" - let responseLength = 0 - let timestampFirstByte = 0 - - function pump(): Promise { - return ( - reader?.read().then(async ({ done, value: rawValue }) => { - if (done) { - const timestampLastByte = Date.now() - logger.metric({ - response_length: responseLength, - "timestamp.last_byte": timestampLastByte, - }) - await rateLimiter?.track() - const usage = usageParser.retrieve() - if (usage) { - const usageInfo = providerInfo.normalizeUsage(usage) - const costInfo = calculateCost(modelInfo, usageInfo) - await trialLimiter?.track(usageInfo) - await modelTpmLimiter?.track(providerInfo.id, providerInfo.model, usageInfo) - await modelTpsLimiter?.track( - providerInfo.id, - providerInfo.model, - providerInfo.tpsGoal, - timestampFirstByte, - timestampLastByte, - usageInfo, - ) - await providerBudgetTracker?.track( - providerInfo.id, - providerInfo.budgetPriority, - costInfo.totalCostInCent, - ) - await trackUsage(sessionId, billingSource, authInfo, modelInfo, providerInfo, usageInfo, costInfo) - await reload(billingSource, authInfo, costInfo) - const cost = calculateOccurredCost(billingSource, costInfo) - c.enqueue(encoder.encode(buildCostChunk(opts.format, cost))) - } - c.close() - return - } - - if (responseLength === 0) { - timestampFirstByte = Date.now() - logger.metric({ - time_to_first_byte: timestampFirstByte - startTimestamp, - "timestamp.first_byte": timestampFirstByte, - }) - } - - const value = binaryDecoder ? binaryDecoder(rawValue) : rawValue - if (!value) return - - responseLength += value.length - buffer += decoder.decode(value, { stream: true }) - - const parts = buffer.split(/\r\n\r\n|\n\n|\r\r/) - buffer = parts.pop() ?? "" - - for (let part of parts) { - logger.debug("PART: " + part) - - part = part.trim() - usageParser.parse(part) - - if (providerInfo.format !== opts.format) { - part = streamConverter(part) - c.enqueue(encoder.encode(part + "\n\n")) - } - } - - if (providerInfo.format === opts.format) { - c.enqueue(value) - } - - return pump() - }) || Promise.resolve() - ) - } - - return pump() - }, - cancel() { - // When the downstream caller stops reading, release the upstream - // response body instead of keeping the provider/inference stream alive. - return reader?.cancel() - }, - }) - return new Response(stream, { - status: resStatus, - statusText: res.statusText, - headers: resHeaders, - }) - } catch (error: any) { - // The caller disconnected before we finished. Because the outbound provider - // request shares input.request.signal, an aborted caller surfaces here as an - // AbortError. There is no client left to receive a body, so skip the error - // metric and 500 and return a quiet client-closed response. - if (input.request.signal.aborted || error?.name === "AbortError") { - logger.debug("REQUEST ABORTED BY CALLER") - return new Response(null, { status: 499 }) - } - - logger.metric({ - "error.type": error.constructor.name, - "error.message": error.message, - "error.cause": error.cause?.toString(), - }) - if (error.message.startsWith("Failed query")) { - try { - logger.metric({ - "error.cause2": JSON.stringify(error.cause), - }) - } catch {} - } - - if (error instanceof RegionError) - return new Response( - JSON.stringify({ - type: "error", - error: { type: error.constructor.name, message: error.message }, - }), - { status: 403 }, - ) - - // Note: both top level "type" and "error.type" fields are used by the @ai-sdk/anthropic client to render the error message. - if ( - error instanceof AuthError || - error instanceof CreditsError || - error instanceof MonthlyLimitError || - error instanceof UserLimitError || - error instanceof ModelError - ) - return new Response( - JSON.stringify({ - type: "error", - error: { type: error.constructor.name, message: error.message }, - }), - { status: 401 }, - ) - - if ( - error instanceof RateLimitError || - error instanceof FreeUsageLimitError || - error instanceof GoUsageLimitError || - error instanceof BlackUsageLimitError - ) { - const headers = new Headers() - if (error.retryAfter) { - headers.set("retry-after", String(error.retryAfter)) - } - return new Response( - JSON.stringify({ - type: "error", - error: { - type: error.constructor.name, - message: error.message, - }, - metadata: - error instanceof GoUsageLimitError - ? { - workspace: error.workspace, - limitName: error.limitName, - } - : {}, - }), - { status: 429, headers }, - ) - } - - return new Response( - JSON.stringify({ - type: "error", - error: { - type: "error", - message: "Internal server error", - }, - }), - { status: 500 }, - ) - } - - function validateModel(zenData: ZenData, reqModel: string) { - if (!(reqModel in zenData.models)) throw new ModelError(t("zen.api.error.modelNotSupported", { model: reqModel })) - - const modelId = reqModel - const modelData = Array.isArray(zenData.models[modelId]) - ? zenData.models[modelId].find((model) => opts.format === model.formatFilter) - : zenData.models[modelId] - - if (!modelData) - throw new ModelError( - t("zen.api.error.modelFormatNotSupported", { - model: reqModel, - format: opts.format, - }), - ) - - if (modelData.trialEnded) - throw new ModelError( - `${t("zen.api.error.trialEnded", { - model: modelData.name, - link: "https://opencode.ai/go", - })}`, - ) - - logger.metric({ model: modelId }) - - return { id: modelId, ...modelData } - } - - function selectProvider( - reqModel: string, - zenData: ZenData, - authInfo: AuthInfo, - modelInfo: ModelInfo, - stickyId: string, - trialProviders: string[] | undefined, - retry: RetryOptions, - stickyProviderId: string | undefined, - modelTpmLimits: Record | undefined, - modelTpsLimits: Record | undefined, - providerBudget: - | { - qualify: (providerId: string, priority: number) => boolean - prefer: (providerId: string, priority: number) => boolean - } - | undefined, - ) { - const modelProvider = (() => { - // Byok is top priority b/c if user set their own API key, we should use it - // instead of using the sticky provider for the same session - if (authInfo?.provider?.credentials) { - return modelInfo.providers.find((provider) => provider.id === modelInfo.byokProvider) - } - - // Prioritize trial providers - let allProviders = modelInfo.providers.filter((provider) => !provider.disabled) - if (trialProviders) { - allProviders = allProviders.map((provider) => ({ - ...provider, - priority: trialProviders.includes(provider.id) ? 0 : provider.priority, - })) - } - - // Use fallback provider if max retries reached - const fallbackProvider = allProviders.find((provider) => provider.id === modelInfo.fallbackProvider) - if (retry.retryCount === MAX_FAILOVER_RETRIES) return fallbackProvider - - let topPriority = Infinity - const providers = allProviders - .filter((provider) => provider.weight !== 0) - .filter((provider) => !retry.excludeProviders.includes(provider.id)) - .filter((provider) => { - if (provider.budgetPriority === undefined) return true - if (!providerBudget) return true - return providerBudget.qualify(provider.id, provider.budgetPriority) - }) - .filter((provider) => { - if (!provider.tpmLimit) return true - const usage = modelTpmLimits?.[`${provider.id}/${provider.model}`] ?? 0 - return usage < provider.tpmLimit * 1_000_000 - }) - .filter((provider) => { - if (!provider.tpsGoal) return true - const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? { - qualify: 0, - unqualify: 0, - } - const isLowTps = tps.qualify + tps.unqualify > 10 && tps.qualify < tps.unqualify - return !isLowTps - }) - .map((provider) => { - topPriority = Math.min(topPriority, provider.priority) - return provider - }) - .filter((p) => p.priority <= topPriority) - .flatMap((provider) => Array(provider.weight).fill(provider)) - - // Use the last 4 characters of session ID to select a provider - let h = 0 - const l = stickyId.length - for (let i = l - 4; i < l; i++) { - h = (h * 31 + stickyId.charCodeAt(i)) | 0 // 32-bit int - } - const index = (h >>> 0) % providers.length // make unsigned + range 0..length-1 - const provider = providers[index || 0] ?? fallbackProvider - - // sticky provider does not exist => use selected provider - if (!stickyProviderId) return provider - const stickProvider = allProviders.find((provider) => provider.id === stickyProviderId) - if (!stickProvider) return provider - - const preferBudgetProvider = - provider.budgetPriority !== undefined && providerBudget?.prefer(provider.id, provider.budgetPriority) - - const preferTpsProvider = (() => { - if (!provider.tpsGoal) return false - const tps = modelTpsLimits?.[`${provider.id}/${provider.model}/${provider.tpsGoal}`] ?? { - qualify: 0, - unqualify: 0, - } - return tps.qualify > tps.unqualify * 3 - })() - - if (!preferBudgetProvider && !preferTpsProvider) return stickProvider - - return provider - })() - - if (!modelProvider) throw new ModelError(t("zen.api.error.noProviderAvailable")) - if (!(modelProvider.id in zenData.providers)) - throw new ModelError(t("zen.api.error.providerNotSupported", { provider: modelProvider.id })) - - return { - ...modelProvider, - ...zenData.providers[modelProvider.id], - ...(() => { - const providerProps = zenData.providers[modelProvider.id] - const format = providerProps.format - const opts = { - reqModel, - providerModel: modelProvider.model, - adjustCacheUsage: providerProps.adjustCacheUsage, - workspaceID: authInfo?.workspaceID, - } - if (format === "anthropic") return anthropicHelper(opts) - if (format === "google") return googleHelper(opts) - if (format === "openai") return openaiHelper(opts) - return oaCompatHelper(opts) - })(), - } - } - - async function authenticate(modelInfo: ModelInfo, zenApiKey?: string) { - if (!zenApiKey) { - if (modelInfo.allowAnonymous) return - throw new AuthError(t("zen.api.error.missingApiKey")) - } - - const data = await Database.use((tx) => - tx - .select({ - apiKey: KeyTable.id, - workspace: { - id: WorkspaceTable.id, - region: WorkspaceTable.region, - isBlocked: WorkspaceTable.is_blocked, - isFlaggedByAnthropic: WorkspaceTable.is_flagged_by_anthropic, - isFlaggedByOpenAI: WorkspaceTable.is_flagged_by_openai, - }, - billing: { - balance: BillingTable.balance, - paymentMethodID: BillingTable.paymentMethodID, - monthlyLimit: BillingTable.monthlyLimit, - monthlyUsage: BillingTable.monthlyUsage, - timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated, - reloadTrigger: BillingTable.reloadTrigger, - timeReloadLockedTill: BillingTable.timeReloadLockedTill, - subscription: BillingTable.subscription, - lite: BillingTable.lite, - }, - user: { - id: UserTable.id, - monthlyLimit: UserTable.monthlyLimit, - monthlyUsage: UserTable.monthlyUsage, - timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated, - }, - black: { - id: SubscriptionTable.id, - rollingUsage: SubscriptionTable.rollingUsage, - fixedUsage: SubscriptionTable.fixedUsage, - timeRollingUpdated: SubscriptionTable.timeRollingUpdated, - timeFixedUpdated: SubscriptionTable.timeFixedUpdated, - }, - lite: { - id: LiteTable.id, - timeCreated: LiteTable.timeCreated, - rollingUsage: LiteTable.rollingUsage, - weeklyUsage: LiteTable.weeklyUsage, - monthlyUsage: LiteTable.monthlyUsage, - timeRollingUpdated: LiteTable.timeRollingUpdated, - timeWeeklyUpdated: LiteTable.timeWeeklyUpdated, - timeMonthlyUpdated: LiteTable.timeMonthlyUpdated, - }, - provider: { - credentials: ProviderTable.credentials, - }, - timeDisabled: ModelTable.timeCreated, - }) - .from(KeyTable) - .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) - .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID)) - .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID))) - .leftJoin(ModelTable, and(eq(ModelTable.workspaceID, KeyTable.workspaceID), eq(ModelTable.model, modelInfo.id))) - .leftJoin( - ProviderTable, - modelInfo.byokProvider - ? and( - eq(ProviderTable.workspaceID, KeyTable.workspaceID), - eq(ProviderTable.provider, modelInfo.byokProvider), - ) - : sql`false`, - ) - .leftJoin( - SubscriptionTable, - and( - eq(SubscriptionTable.workspaceID, KeyTable.workspaceID), - eq(SubscriptionTable.userID, KeyTable.userID), - isNull(SubscriptionTable.timeDeleted), - ), - ) - .leftJoin( - LiteTable, - and( - eq(LiteTable.workspaceID, KeyTable.workspaceID), - eq(LiteTable.userID, KeyTable.userID), - isNull(LiteTable.timeDeleted), - ), - ) - .where(and(eq(KeyTable.key, zenApiKey), isNull(KeyTable.timeDeleted))) - .then((rows) => rows[0]), - ) - - if (!data) throw new AuthError(t("zen.api.error.invalidApiKey")) - if ( - data.workspace.isBlocked || - (data.workspace.isFlaggedByAnthropic && modelInfo.id.startsWith("claude-")) || - (data.workspace.isFlaggedByOpenAI && modelInfo.id.startsWith("gpt-")) - ) - throw new AuthError(t("zen.api.error.requestBlockedByUpstreamProvider")) - if ( - modelInfo.id.startsWith("alpha-") && - Resource.App.stage === "production" && - !ADMIN_WORKSPACES.includes(data.workspace.id) - ) - throw new AuthError(t("zen.api.error.modelNotSupported", { model: modelInfo.id })) - - logger.metric({ - api_key: data.apiKey, - workspace: data.workspace.id, - user_id: data.user.id, - ...(() => { - if (data.billing.subscription) - return { - subscription: data.billing.subscription.plan, - } - if (data.billing.lite) - return { - subscription: "lite", - } - return {} - })(), - }) - - return { - apiKeyId: data.apiKey, - workspaceID: data.workspace.id, - region: data.workspace.region, - billing: data.billing, - user: data.user, - black: data.black, - lite: data.lite, - provider: data.provider, - isFree: ADMIN_WORKSPACES.includes(data.workspace.id), - isDisabled: !!data.timeDisabled, - } - } - - function validateBilling(authInfo: AuthInfo, modelInfo: ModelInfo): BillingSource { - if (!authInfo) return "anonymous" - if (authInfo.provider?.credentials) return "byok" - if (authInfo.isFree) return "free" - if (modelInfo.allowAnonymous) return "free" - - const formatRetryTime = (seconds: number) => { - const days = Math.floor(seconds / 86400) - if (days >= 1) return `${days} day${days > 1 ? "s" : ""}` - const hours = Math.floor(seconds / 3600) - const minutes = Math.ceil((seconds % 3600) / 60) - if (hours >= 1) return `${hours}hr ${minutes}min` - return `${minutes}min` - } - - // Validate black subscription billing - if (authInfo.billing.subscription && authInfo.black) { - try { - const sub = authInfo.black - const plan = authInfo.billing.subscription.plan - - // Check weekly limit - if (sub.fixedUsage && sub.timeFixedUpdated) { - const blackData = BlackData.getLimits({ plan }) - const result = Subscription.analyzeWeeklyUsage({ - limit: blackData.fixedLimit, - usage: sub.fixedUsage, - timeUpdated: sub.timeFixedUpdated, - }) - if (result.status === "rate-limited") - throw new BlackUsageLimitError( - t("zen.api.error.subscriptionQuotaExceeded", { - retryIn: formatRetryTime(result.resetInSec), - }), - result.resetInSec, - ) - } - - // Check rolling limit - if (sub.rollingUsage && sub.timeRollingUpdated) { - const blackData = BlackData.getLimits({ plan }) - const result = Subscription.analyzeRollingUsage({ - limit: blackData.rollingLimit, - window: blackData.rollingWindow, - usage: sub.rollingUsage, - timeUpdated: sub.timeRollingUpdated, - }) - if (result.status === "rate-limited") - throw new BlackUsageLimitError( - t("zen.api.error.subscriptionQuotaExceeded", { - retryIn: formatRetryTime(result.resetInSec), - }), - result.resetInSec, - ) - } - - return "subscription" - } catch (e) { - if (!authInfo.billing.subscription.useBalance) throw e - } - } - - // Validate lite subscription billing - if (opts.modelList === "lite" && authInfo.billing.lite && authInfo.lite) { - try { - const consoleGoUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/go` - const sub = authInfo.lite - const liteData = LiteData.getLimits() - - // Check weekly limit - if (sub.weeklyUsage && sub.timeWeeklyUpdated) { - const result = Subscription.analyzeWeeklyUsage({ - limit: liteData.weeklyLimit, - usage: sub.weeklyUsage, - timeUpdated: sub.timeWeeklyUpdated, - }) - if (result.status === "rate-limited") - throw new GoUsageLimitError( - t("zen.api.error.goSubscriptionWeeklyLimitExceeded", { - retryIn: formatRetryTime(result.resetInSec), - consoleGoUrl, - }), - authInfo.workspaceID, - "weekly", - result.resetInSec, - ) - } - - // Check monthly limit - if (sub.monthlyUsage && sub.timeMonthlyUpdated) { - const result = Subscription.analyzeMonthlyUsage({ - limit: liteData.monthlyLimit, - usage: sub.monthlyUsage, - timeUpdated: sub.timeMonthlyUpdated, - timeSubscribed: sub.timeCreated, - }) - if (result.status === "rate-limited") - throw new GoUsageLimitError( - t("zen.api.error.goSubscriptionMonthlyLimitExceeded", { - retryIn: formatRetryTime(result.resetInSec), - consoleGoUrl, - }), - authInfo.workspaceID, - "monthly", - result.resetInSec, - ) - } - - // Check rolling limit - if (sub.rollingUsage && sub.timeRollingUpdated) { - const result = Subscription.analyzeRollingUsage({ - limit: liteData.rollingLimit, - window: liteData.rollingWindow, - usage: sub.rollingUsage, - timeUpdated: sub.timeRollingUpdated, - }) - if (result.status === "rate-limited") - throw new GoUsageLimitError( - t("zen.api.error.goSubscriptionRollingLimitExceeded", { - retryIn: formatRetryTime(result.resetInSec), - consoleGoUrl, - }), - authInfo.workspaceID, - "5 hour", - result.resetInSec, - ) - } - - return "lite" - } catch (e) { - if (!authInfo.billing.lite.useBalance) throw e - } - } - - // Validate pay as you go billing - const billing = authInfo.billing - const billingUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/billing` - const membersUrl = `https://opencode.ai/workspace/${authInfo.workspaceID}/members` - if (!billing.paymentMethodID && billing.balance <= 0) - throw new CreditsError(t("zen.api.error.noPaymentMethod", { billingUrl })) - if (billing.balance <= 0) throw new CreditsError(t("zen.api.error.insufficientBalance", { billingUrl })) - - const now = new Date() - const currentYear = now.getUTCFullYear() - const currentMonth = now.getUTCMonth() - if ( - billing.monthlyLimit && - billing.monthlyUsage && - billing.timeMonthlyUsageUpdated && - billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100) && - currentYear === billing.timeMonthlyUsageUpdated.getUTCFullYear() && - currentMonth === billing.timeMonthlyUsageUpdated.getUTCMonth() - ) - throw new MonthlyLimitError( - t("zen.api.error.workspaceMonthlyLimitReached", { - amount: billing.monthlyLimit, - billingUrl, - }), - ) - - if ( - authInfo.user.monthlyLimit && - authInfo.user.monthlyUsage && - authInfo.user.timeMonthlyUsageUpdated && - authInfo.user.monthlyUsage >= centsToMicroCents(authInfo.user.monthlyLimit * 100) && - currentYear === authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear() && - currentMonth === authInfo.user.timeMonthlyUsageUpdated.getUTCMonth() - ) - throw new UserLimitError( - t("zen.api.error.userMonthlyLimitReached", { - amount: authInfo.user.monthlyLimit, - membersUrl, - }), - ) - - return "balance" - } - - function validateModelSettings(billingSource: BillingSource, authInfo: AuthInfo) { - if (billingSource === "lite") return - if (billingSource === "anonymous") return - if (authInfo!.isDisabled) throw new ModelError(t("zen.api.error.modelDisabled")) - } - - function updateProviderKey(authInfo: AuthInfo, providerInfo: ProviderInfo) { - if (!authInfo?.provider?.credentials) return - providerInfo.apiKey = authInfo.provider.credentials - } - - async function fetchWithRetryableStatus(url: string, options: RequestInit, retry = { count: 0 }) { - const res = await fetch(url, options) - if ([429, 529].includes(res.status) && retry.count < MAX_RETRYABLE_STATUS_RETRIES) { - await new Promise((resolve) => setTimeout(resolve, Math.pow(2, retry.count) * 500)) - return fetchWithRetryableStatus(url, options, { count: retry.count + 1 }) - } - return res - } - - function calculateCost(modelInfo: ModelInfo, usageInfo: UsageInfo) { - const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = - usageInfo - - const modelCost = - modelInfo.cost200K && - inputTokens + (cacheReadTokens ?? 0) + (cacheWrite5mTokens ?? 0) + (cacheWrite1hTokens ?? 0) > 200_000 - ? modelInfo.cost200K - : modelInfo.cost - - const inputCost = modelCost.input * inputTokens * 100 - const outputCost = modelCost.output * outputTokens * 100 - const cacheReadCost = (() => { - if (!cacheReadTokens) return undefined - if (!modelCost.cacheRead) return undefined - return modelCost.cacheRead * cacheReadTokens * 100 - })() - const cacheWrite5mCost = (() => { - if (!cacheWrite5mTokens) return undefined - if (!modelCost.cacheWrite5m) return undefined - return modelCost.cacheWrite5m * cacheWrite5mTokens * 100 - })() - const cacheWrite1hCost = (() => { - if (!cacheWrite1hTokens) return undefined - if (!modelCost.cacheWrite1h) return undefined - return modelCost.cacheWrite1h * cacheWrite1hTokens * 100 - })() - const totalCostInCent = - inputCost + outputCost + (cacheReadCost ?? 0) + (cacheWrite5mCost ?? 0) + (cacheWrite1hCost ?? 0) - return { - totalCostInCent, - inputCost, - outputCost, - cacheReadCost, - cacheWrite5mCost, - cacheWrite1hCost, - } - } - - function calculateOccurredCost(billingSource: BillingSource, costInfo: CostInfo) { - return billingSource === "balance" ? (costInfo.totalCostInCent / 100).toFixed(8) : "0" - } - - async function trackUsage( - sessionId: string, - billingSource: BillingSource, - authInfo: AuthInfo, - modelInfo: ModelInfo, - providerInfo: ProviderInfo, - usageInfo: UsageInfo, - costInfo: CostInfo, - ) { - const { inputTokens, outputTokens, reasoningTokens, cacheReadTokens, cacheWrite5mTokens, cacheWrite1hTokens } = - usageInfo - const { totalCostInCent, inputCost, outputCost, cacheReadCost, cacheWrite5mCost, cacheWrite1hCost } = costInfo - - logger.metric({ - "tokens.input": inputTokens, - "tokens.output": outputTokens, - "tokens.reasoning": reasoningTokens, - "tokens.cache_read": cacheReadTokens, - "tokens.cache_write_5m": cacheWrite5mTokens, - "tokens.cache_write_1h": cacheWrite1hTokens, - "cost.input.microcents": centsToMicroCents(inputCost), - "cost.output.microcents": centsToMicroCents(outputCost), - "cost.cache_read.microcents": cacheReadCost ? centsToMicroCents(cacheReadCost) : undefined, - "cost.cache_write.microcents": cacheWrite5mCost ? centsToMicroCents(cacheWrite5mCost) : undefined, - "cost.total.microcents": centsToMicroCents(totalCostInCent), - // deprecated - remove after May 20, 2026 - "cost.input": Math.round(inputCost), - "cost.output": Math.round(outputCost), - "cost.cache_read": cacheReadCost ? Math.round(cacheReadCost) : undefined, - "cost.cache_write_5m": cacheWrite5mCost ? Math.round(cacheWrite5mCost) : undefined, - "cost.cache_write_1h": cacheWrite1hCost ? Math.round(cacheWrite1hCost) : undefined, - "cost.total": Math.round(totalCostInCent), - }) - - if (billingSource === "anonymous") return - authInfo = authInfo! - - const cost = centsToMicroCents(totalCostInCent) - - // For hot workspaces, batch balance/usage updates through Redis to avoid - // row-level lock contention on BillingTable/UserTable. Returns the amount - // to flush this request, or null to skip the DB writes entirely. - const balanceFlush = await (async () => { - if (billingSource !== "subscription" && billingSource !== "lite" && HOT_WORKSPACES.has(authInfo.workspaceID)) { - const workspaceCost = billingSource === "free" || billingSource === "byok" ? 0 : cost - const flush = await accumulateUsage(authInfo.workspaceID, authInfo.user.id, workspaceCost, cost) - return { batched: true as const, flush } - } - return { batched: false as const, flush: null } - })() - - await Database.use((db) => - Promise.all([ - db.insert(UsageTable).values({ - workspaceID: authInfo.workspaceID, - id: Identifier.create("usage"), - model: modelInfo.id, - provider: providerInfo.id, - inputTokens, - outputTokens, - reasoningTokens, - cacheReadTokens, - cacheWrite5mTokens, - cacheWrite1hTokens, - cost, - keyID: authInfo.apiKeyId, - sessionID: sessionId.substring(0, 30), - enrichment: (() => { - if (billingSource === "subscription") return { plan: "sub" } - if (billingSource === "byok") return { plan: "byok" } - if (billingSource === "lite") return { plan: "lite" } - return undefined - })(), - }), - ...(() => { - if (billingSource === "subscription") { - const plan = authInfo.billing.subscription!.plan - const black = BlackData.getLimits({ plan }) - const week = getWeekBounds(new Date()) - const rollingWindowSeconds = black.rollingWindow * 3600 - return [ - db - .update(SubscriptionTable) - .set({ - fixedUsage: sql` - CASE - WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost} - ELSE ${cost} - END - `, - timeFixedUpdated: sql`now()`, - rollingUsage: sql` - CASE - WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost} - ELSE ${cost} - END - `, - timeRollingUpdated: sql` - CASE - WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.timeRollingUpdated} - ELSE now() - END - `, - }) - .where( - and( - eq(SubscriptionTable.workspaceID, authInfo.workspaceID), - eq(SubscriptionTable.userID, authInfo.user.id), - ), - ), - ] - } - if (billingSource === "lite") { - const lite = LiteData.getLimits() - const week = getWeekBounds(new Date()) - const month = getMonthlyBounds(new Date(), authInfo.lite!.timeCreated) - const rollingWindowSeconds = lite.rollingWindow * 3600 - const quotaCost = Math.round(cost * modelInfo.costMultiplier) - return [ - db - .update(LiteTable) - .set({ - monthlyUsage: sql` - CASE - WHEN ${LiteTable.timeMonthlyUpdated} >= ${month.start} THEN ${LiteTable.monthlyUsage} + ${quotaCost} - ELSE ${quotaCost} - END - `, - timeMonthlyUpdated: sql`now()`, - weeklyUsage: sql` - CASE - WHEN ${LiteTable.timeWeeklyUpdated} >= ${week.start} THEN ${LiteTable.weeklyUsage} + ${quotaCost} - ELSE ${quotaCost} - END - `, - timeWeeklyUpdated: sql`now()`, - rollingUsage: sql` - CASE - WHEN UNIX_TIMESTAMP(${LiteTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${LiteTable.rollingUsage} + ${quotaCost} - ELSE ${quotaCost} - END - `, - timeRollingUpdated: sql` - CASE - WHEN UNIX_TIMESTAMP(${LiteTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${LiteTable.timeRollingUpdated} - ELSE now() - END - `, - }) - .where(and(eq(LiteTable.workspaceID, authInfo.workspaceID), eq(LiteTable.userID, authInfo.user.id))), - ] - } - - // Batched hot workspace: skip DB writes unless this request is the flush. - if (balanceFlush.batched && !balanceFlush.flush) return [] - - const workspaceDelta = balanceFlush.flush?.workspaceCost ?? cost - const userDelta = balanceFlush.flush?.userCost ?? cost - const balanceDelta = billingSource === "free" || billingSource === "byok" ? 0 : workspaceDelta - - return [ - db - .update(BillingTable) - .set({ - balance: sql`${BillingTable.balance} - ${balanceDelta}`, - monthlyUsage: sql` - CASE - WHEN MONTH(${BillingTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${BillingTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${BillingTable.monthlyUsage} + ${workspaceDelta} - ELSE ${workspaceDelta} - END - `, - timeMonthlyUsageUpdated: sql`now()`, - }) - .where(eq(BillingTable.workspaceID, authInfo.workspaceID)), - db - .update(UserTable) - .set({ - monthlyUsage: sql` - CASE - WHEN MONTH(${UserTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${UserTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${UserTable.monthlyUsage} + ${userDelta} - ELSE ${userDelta} - END - `, - timeMonthlyUsageUpdated: sql`now()`, - }) - .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))), - ] - })(), - ]), - ) - - return { costInMicroCents: cost } - } - - async function reload(billingSource: BillingSource, authInfo: AuthInfo, costInfo: CostInfo) { - if (billingSource !== "balance") return - authInfo = authInfo! - - const reloadTrigger = centsToMicroCents((authInfo.billing.reloadTrigger ?? Billing.RELOAD_TRIGGER) * 100) - if (authInfo.billing.balance - costInfo.totalCostInCent >= reloadTrigger) return - if (authInfo.billing.timeReloadLockedTill && authInfo.billing.timeReloadLockedTill > new Date()) return - - const lock = await Database.use((tx) => - tx - .update(BillingTable) - .set({ - timeReloadLockedTill: sql`now() + interval 1 minute`, - }) - .where( - and( - eq(BillingTable.workspaceID, authInfo.workspaceID), - eq(BillingTable.reload, true), - lt(BillingTable.balance, reloadTrigger), - or(isNull(BillingTable.timeReloadLockedTill), lt(BillingTable.timeReloadLockedTill, sql`now()`)), - ), - ), - ) - if (lock.rowsAffected === 0) return - - await Actor.provide("system", { workspaceID: authInfo.workspaceID }, async () => { - await Billing.reload() - }) - } -} diff --git a/packages/console/core/package.json b/packages/console/core/package.json deleted file mode 100644 index a2016a1e6b4..00000000000 --- a/packages/console/core/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/package.json", - "name": "@opencode-ai/console-core", - "version": "7.4.20", - "private": true, - "type": "module", - "license": "MIT", - "dependencies": { - "@aws-sdk/client-sts": "3.782.0", - "@jsx-email/render": "1.1.1", - "@opencode-ai/console-mail": "workspace:*", - "@opencode-ai/console-resource": "workspace:*", - "@planetscale/database": "1.19.0", - "aws4fetch": "1.0.20", - "drizzle-orm": "catalog:", - "postgres": "3.4.7", - "stripe": "18.0.0", - "ulid": "catalog:", - "zod": "catalog:" - }, - "exports": { - "./*.js": "./src/*.ts", - "./*": "./src/*" - }, - "scripts": { - "db": "sst shell drizzle-kit", - "db-dev": "sst shell --stage dev -- drizzle-kit", - "db-prod": "sst shell --stage production -- drizzle-kit", - "shell": "sst shell", - "shell-dev": "sst shell --stage dev", - "shell-prod": "sst shell --stage production", - "update-models": "script/update-models.ts", - "promote-models-to-dev": "script/promote-models.ts dev", - "promote-models-to-prod": "script/promote-models.ts production", - "pull-models-from-dev": "script/pull-models.ts dev", - "pull-models-from-prod": "script/pull-models.ts production", - "update-limits": "script/update-limits.ts", - "promote-limits-to-dev": "script/promote-limits.ts dev", - "promote-limits-to-prod": "script/promote-limits.ts production", - "typecheck": "tsgo --noEmit" - }, - "devDependencies": { - "@cloudflare/workers-types": "catalog:", - "@tsconfig/node22": "22.0.2", - "@types/bun": "catalog:", - "@types/node": "catalog:", - "drizzle-kit": "catalog:", - "mysql2": "3.14.4", - "typescript": "catalog:", - "@typescript/native-preview": "catalog:" - } -} diff --git a/packages/core/package.json b/packages/core/package.json index de776cdb96e..03ed5fec474 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.20", + "version": "7.4.23", "name": "@opencode-ai/core", "type": "module", "license": "MIT", diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index 9a763c7ea9b..915df79d5be 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -30,15 +30,11 @@ Guidelines: Complete the user's search request efficiently and report your findings clearly.` -const PROMPT_COMPACTION = `You are an anchored context summarization assistant for coding sessions. - -Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work. - -If the prompt includes a block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts. +const PROMPT_COMPACTION = `You are a context summarization agent. You are given a conversation between a user and an agent. Your goal is to produce a structured summary matching the format specified so another coding agent can continue the work. Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs. -Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation.` +Do not continue the conversation. Do not respond to any questions in the conversation. Only output the structured summary in the exact format requested by the user prompt. Respond in the same language as the conversation.` const PROMPT_TITLE = `You are a title generator. You output ONLY a thread title. Nothing else. diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 4b21ff348fe..ea4cf04aaad 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -44,6 +44,15 @@ Rules: - Use terse bullets, not prose paragraphs. - Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known. - Do not mention the summary process or that context was compacted.` +const SUMMARY_UPDATE_INSTRUCTIONS = `The summarizes everything that happened before the . Construct a new summary that combines both. The is discarded after this: anything you do not carry into the new summary is lost. + +When combining: +- Carry forward objectives, constraints, user directives, decisions, and parallel workstreams from the even when the does not mention them. Drop only what is finished and no longer needed. +- The is more recent than the . Where they conflict, the conversation wins: state the corrected fact and drop the old claim. +- Add new progress, decisions, constraints, and context from the conversation. +- Move completed work from "Active" to "Completed". +- If a blocker has been resolved, update the summary to reflect that while keeping any details still needed to continue the work. +- Update "Objective" and "Next Move" to reflect the current work state.` type Entry = { readonly seq: number @@ -136,36 +145,33 @@ const select = ( if (conversation.length === 0) return let total = 0 let split = conversation.length - let splitPrefix = "" - let splitSuffix = "" for (let index = conversation.length - 1; index >= 0; index--) { const next = total + Token.estimate(conversation[index]) - if (next > tokens) { - const remaining = Math.max(0, tokens - total) * 4 - if (remaining > 0) { - splitPrefix = conversation[index].slice(0, -remaining) - splitSuffix = conversation[index].slice(-remaining) - split = index + 1 - } - break - } + if (next > tokens) break total = next split = index } return { - head: [...conversation.slice(0, split), splitPrefix].filter(Boolean).join("\n\n"), - recent: [splitSuffix, ...conversation.slice(split)].filter(Boolean).join("\n\n"), + head: conversation.slice(0, split).join("\n\n"), + recent: conversation.slice(split).join("\n\n"), } } -export const buildPrompt = (input: { readonly previousSummary?: string; readonly context: readonly string[] }) => - [ - input.previousSummary - ? `Update the anchored summary below using the conversation history above.\nPreserve still-true details, remove stale details, and merge in the new facts.\n\n${input.previousSummary}\n` - : "Create a new anchored summary from the conversation history.", +export const buildPrompt = (input: { readonly previousSummary?: string; readonly context: readonly string[] }) => { + const conversation = `Here is the conversation so far:\n\n\n${input.context.join("\n\n")}\n` + if (!input.previousSummary) + return [ + conversation, + "Create a new anchored summary from the conversation history in the tags above so another coding agent can continue the work.", + SUMMARY_TEMPLATE, + ].join("\n\n") + return [ + conversation, + `Here is the summary of the conversation before the above:\n\n\n${input.previousSummary}\n`, + SUMMARY_UPDATE_INSTRUCTIONS, SUMMARY_TEMPLATE, - ...input.context, ].join("\n\n") +} export const make = (dependencies: Dependencies) => { const config = settings(dependencies.config) diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 691f55150ae..7ebb4b69b02 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -156,7 +156,7 @@ export const Info = Schema.Struct({ }), tail_turns: Schema.optional(NonNegativeInt).annotate({ description: - "Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)", + "Maximum number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction. By default retention is limited only by the preserved token budget.", }), preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({ description: "Maximum number of tokens from recent turns to preserve verbatim after compaction", diff --git a/packages/core/test/provider-groq.test.ts b/packages/core/test/provider-groq.test.ts new file mode 100644 index 00000000000..604a2a750e5 --- /dev/null +++ b/packages/core/test/provider-groq.test.ts @@ -0,0 +1,28 @@ +import { createGroq } from "@ai-sdk/groq" +import { expect, test } from "bun:test" + +test("Groq passes through unknown reasoning effort", async () => { + let body: Record | undefined + const mockFetch = Object.assign( + async (_input: Parameters[0], init?: RequestInit) => { + body = JSON.parse(String(init?.body)) + return Response.json({ + id: "response-1", + created: 0, + model: "openai/gpt-oss-120b", + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }, + { preconnect: fetch.preconnect }, + ) + const model = createGroq({ apiKey: "test", fetch: mockFetch })("openai/gpt-oss-120b") + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + providerOptions: { groq: { reasoningEffort: "custom" } }, + }) + + expect(body?.reasoning_effort).toBe("custom") +}) diff --git a/packages/core/test/provider-mistral.test.ts b/packages/core/test/provider-mistral.test.ts index 6e3176695f6..5841bcb6cdc 100644 --- a/packages/core/test/provider-mistral.test.ts +++ b/packages/core/test/provider-mistral.test.ts @@ -27,6 +27,32 @@ test("Mistral sends promptCacheKey as prompt_cache_key", async () => { expect(body?.prompt_cache_key).toBe("session-123") }) +test("Mistral passes through unknown reasoning effort", async () => { + let body: Record | undefined + const mockFetch = Object.assign( + async (_input: Parameters[0], init?: RequestInit) => { + body = JSON.parse(String(init?.body)) + return Response.json({ + id: "response-1", + created: 0, + model: "mistral-large-latest", + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }, + { preconnect: fetch.preconnect }, + ) + const model = createMistral({ apiKey: "test", fetch: mockFetch })("mistral-large-latest") + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + providerOptions: { mistral: { reasoningEffort: "custom" } }, + }) + + expect(body?.reasoning_effort).toBe("custom") +}) + test("Mistral round-trips native reasoning in assistant history", async () => { let body: { messages?: unknown[] } | undefined const mockFetch = Object.assign( diff --git a/packages/core/test/provider-xai-responses.test.ts b/packages/core/test/provider-xai-responses.test.ts index d9d674fe169..34c7d7d4aea 100644 --- a/packages/core/test/provider-xai-responses.test.ts +++ b/packages/core/test/provider-xai-responses.test.ts @@ -30,3 +30,56 @@ test("xAI Responses sends promptCacheKey as prompt_cache_key", async () => { expect(body?.prompt_cache_key).toBe("session-123") }) + +test("xAI Responses passes through xhigh reasoning effort", async () => { + let body: Record | undefined + const mockFetch = Object.assign( + async (_input: Parameters[0], init?: RequestInit) => { + body = JSON.parse(String(init?.body)) + return Response.json({ + id: "response-1", + created_at: 0, + model: "grok-4", + object: "response", + output: [], + usage: { input_tokens: 1, output_tokens: 0 }, + status: "completed", + }) + }, + { preconnect: fetch.preconnect }, + ) + const model = createXai({ apiKey: "test", fetch: mockFetch }).responses("grok-4") + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + providerOptions: { xai: { reasoningEffort: "xhigh" } }, + }) + + expect(body?.reasoning).toEqual({ effort: "xhigh" }) +}) + +test("xAI Chat passes through xhigh reasoning effort", async () => { + let body: Record | undefined + const mockFetch = Object.assign( + async (_input: Parameters[0], init?: RequestInit) => { + body = JSON.parse(String(init?.body)) + return Response.json({ + id: "chat-1", + created: 0, + model: "grok-4", + object: "chat.completion", + choices: [{ index: 0, message: { role: "assistant", content: "Hello" }, finish_reason: "stop" }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }, + { preconnect: fetch.preconnect }, + ) + const model = createXai({ apiKey: "test", fetch: mockFetch }).chat("grok-4") + + await model.doGenerate({ + prompt: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + providerOptions: { xai: { reasoningEffort: "xhigh" } }, + }) + + expect(body?.reasoning_effort).toBe("xhigh") +}) diff --git a/packages/core/test/session-compaction.test.ts b/packages/core/test/session-compaction.test.ts index 9d45e0acc33..246ddc35f5a 100644 --- a/packages/core/test/session-compaction.test.ts +++ b/packages/core/test/session-compaction.test.ts @@ -4,12 +4,32 @@ import { SessionCompaction } from "@opencode-ai/core/session/compaction" test("compaction prompt preserves detailed work state and relevant files", () => { const prompt = SessionCompaction.buildPrompt({ context: ["conversation history"] }) + expect(prompt).toStartWith( + "Here is the conversation so far:\n\n\nconversation history\n", + ) + expect(prompt.indexOf("")).toBeLessThan(prompt.indexOf("Create a new anchored summary")) + expect(prompt).toContain("conversation history in the tags above") expect(prompt).toContain("## Work State\n### Completed") expect(prompt).toContain("### Active") expect(prompt).toContain("### Blocked") expect(prompt).toContain("## Relevant Files") }) +test("compaction prompt gives update instructions for a prior summary", () => { + const prompt = SessionCompaction.buildPrompt({ + context: ["new conversation"], + previousSummary: "existing summary", + }) + + expect(prompt.indexOf("")).toBeLessThan(prompt.indexOf("")) + expect(prompt.indexOf("")).toBeLessThan(prompt.indexOf("The summarizes")) + expect(prompt).toContain( + "Carry forward objectives, constraints, user directives, decisions, and parallel workstreams from the ", + ) + expect(prompt).toContain('Move completed work from "Active" to "Completed".') + expect(prompt).toContain('Update "Objective" and "Next Move" to reflect the current work state.') +}) + test("compaction describes tool media without embedding base64", () => { const base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" const serialized = SessionCompaction.serializeToolContent([ diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 0515d55cf5b..57d4456d2df 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1135,7 +1135,7 @@ describe("SessionRunnerLLM", () => { expect(requests).toHaveLength(2) expect(userTexts(requests[0])[0]).toContain( - "\n## Objective\n- Preserve the task\n", + "\n## Objective\n- Preserve the task\n", ) expect(userTexts(requests[0])[0]).toContain("Recent exact request") expect((yield* (yield* SessionStore.Service).context(sessionID))[0]).toMatchObject({ @@ -1145,6 +1145,67 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("retains only complete serialized messages during compaction", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + const earlier = `EARLIER_BOUNDARY ${"a".repeat(3_000)} EARLIER_END` + const recent = `RECENT_BOUNDARY ${"b".repeat(3_000)} RECENT_END` + response = fragmentFixture("text", "text-earlier", ["Earlier answer"]).completeEvents + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: earlier }), resume: false }) + yield* session.resume(sessionID) + + currentModel = compactModel + requests.length = 0 + responses = [ + fragmentFixture("text", "text-summary", ["## Objective\n- Preserve the task"]).completeEvents, + fragmentFixture("text", "text-final", ["Continued"]).completeEvents, + ] + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: recent }), resume: false }) + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + const summary = userTexts(requests[0])[0] + const continuation = userTexts(requests[1])[0] + expect(summary.match(/EARLIER_BOUNDARY/g)).toHaveLength(1) + expect(summary).toContain(`EARLIER_BOUNDARY ${"a".repeat(3_000)} EARLIER_END`) + expect(summary).not.toContain("RECENT_BOUNDARY") + expect(continuation).not.toContain("EARLIER_BOUNDARY") + expect(continuation).not.toContain("EARLIER_END") + expect(continuation).toContain("\n[Assistant]: Earlier answer") + expect(continuation).toContain(`RECENT_BOUNDARY ${"b".repeat(3_000)} RECENT_END`) + }), + ) + + it.effect("summarizes an oversized newest message without retaining a fragment", () => + Effect.gen(function* () { + yield* setup + const session = yield* SessionV2.Service + response = fragmentFixture("text", "text-earlier", ["Earlier answer"]).completeEvents + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "Earlier question" }), resume: false }) + yield* session.resume(sessionID) + + const oversized = `OVERSIZED_BOUNDARY ${"x".repeat(4_500)} OVERSIZED_END` + currentModel = compactModel + requests.length = 0 + responses = [ + fragmentFixture("text", "text-summary", ["## Objective\n- Preserve the task"]).completeEvents, + fragmentFixture("text", "text-final", ["Continued"]).completeEvents, + ] + yield* session.prompt({ sessionID, prompt: Prompt.make({ text: oversized }), resume: false }) + yield* session.resume(sessionID) + + expect(requests).toHaveLength(2) + const summary = userTexts(requests[0])[0] + const continuation = userTexts(requests[1])[0] + expect(summary.match(/OVERSIZED_BOUNDARY/g)).toHaveLength(1) + expect(summary).toContain(oversized) + expect(continuation).not.toContain("OVERSIZED_BOUNDARY") + expect(continuation).not.toContain("OVERSIZED_END") + expect(continuation).toContain("\n\n") + }), + ) + it.effect("forces one compaction and retries after provider context overflow", () => Effect.gen(function* () { const session = yield* setupOverflowRecovery diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index 907db1f9a39..dd4d70f6a4b 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.20", + "version": "7.4.23", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index 38035cac27f..64515a74d8d 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.20", + "version": "7.4.23", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 7b4004ee081..8094b9cd4d7 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.20", + "version": "7.4.23", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", diff --git a/packages/llm/package.json b/packages/llm/package.json index 971483d0921..dd2e356c1f3 100644 --- a/packages/llm/package.json +++ b/packages/llm/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.20", + "version": "7.4.23", "name": "@opencode-ai/llm", "type": "module", "license": "MIT", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 524c312ce60..62fffa7af06 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.20", + "version": "7.4.23", "name": "@kilocode/cli", "type": "module", "license": "MIT", @@ -106,7 +106,6 @@ "@kilocode/sdk": "workspace:*", "@lydell/node-pty": "catalog:", "@modelcontextprotocol/sdk": "1.29.0", - "@morphllm/morphsdk": "0.2.166", "@npmcli/arborist": "9.4.0", "@octokit/graphql": "9.0.2", "@octokit/rest": "catalog:", diff --git a/packages/opencode/src/agent/prompt/compaction.txt b/packages/opencode/src/agent/prompt/compaction.txt index c7cb838bbaa..1bf58de8a92 100644 --- a/packages/opencode/src/agent/prompt/compaction.txt +++ b/packages/opencode/src/agent/prompt/compaction.txt @@ -1,9 +1,5 @@ -You are an anchored context summarization assistant for coding sessions. - -Summarize only the conversation history you are given. The newest turns may be kept verbatim outside your summary, so focus on the older context that still matters for continuing the work. - -If the prompt includes a block, treat it as the current anchored summary. Update it with the new history by preserving still-true details, removing stale details, and merging in new facts. +You are a context summarization agent. You are given a conversation between a user and an agent. Your goal is to produce a structured summary matching the format specified so another coding agent can continue the work. Always follow the exact output structure requested by the user prompt. Keep every section, preserve exact file paths and identifiers when known, and prefer terse bullets over paragraphs. -Do not answer the conversation itself. Do not mention that you are summarizing, compacting, or merging context. Respond in the same language as the conversation. +Do not continue the conversation. Do not respond to any questions in the conversation. Only output the structured summary in the exact format requested by the user prompt. Respond in the same language as the conversation. diff --git a/packages/opencode/src/config/parse.ts b/packages/opencode/src/config/parse.ts index 35239086884..538f5113118 100644 --- a/packages/opencode/src/config/parse.ts +++ b/packages/opencode/src/config/parse.ts @@ -37,22 +37,11 @@ export function schema>( data: unknown, source: string, ): DeepMutable { - const extra = topLevelExtraKeys(schema, data) - if (extra.length) { - throw new InvalidError({ - path: source, - issues: [ - { - code: "unrecognized_keys", - keys: extra, - path: [], - message: `Unrecognized key${extra.length === 1 ? "" : "s"}: ${extra.join(", ")}`, - }, - ], - }) - } - - const decoded = EffectSchema.decodeUnknownExit(schema)(data, { errors: "all", propertyOrder: "original" }) + const decoded = EffectSchema.decodeUnknownExit(schema)(data, { + errors: "all", + onExcessProperty: "ignore", + propertyOrder: "original", + }) if (Exit.isSuccess(decoded)) return decoded.value as DeepMutable const error = Cause.squash(decoded.cause) @@ -70,10 +59,3 @@ export function schema>( { cause: error }, ) } - -function topLevelExtraKeys(schema: EffectSchema.Top, data: unknown) { - if (typeof data !== "object" || data === null || Array.isArray(data)) return [] - if (schema.ast._tag !== "Objects" || schema.ast.indexSignatures.length > 0) return [] - const known = new Set(schema.ast.propertySignatures.map((item) => String(item.name))) - return Object.keys(data).filter((key) => !known.has(key)) -} diff --git a/packages/opencode/src/plugin/github-copilot/models.ts b/packages/opencode/src/plugin/github-copilot/models.ts index 94580d22c47..d77f4e2c5b4 100644 --- a/packages/opencode/src/plugin/github-copilot/models.ts +++ b/packages/opencode/src/plugin/github-copilot/models.ts @@ -88,6 +88,9 @@ function build(key: string, remote: SelectableItem, url: string, prev?: Model): const image = (remote.capabilities.supports.vision ?? false) || (remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/")) + const pdf = + (remote.capabilities.supports.vision ?? false) && + (remote.capabilities.limits.vision?.supported_media_types?.includes("application/pdf") ?? false) const isMsgApi = remote.supported_endpoints?.includes("/v1/messages") const endpoint: CopilotEndpoint | undefined = isMsgApi @@ -127,7 +130,7 @@ function build(key: string, remote: SelectableItem, url: string, prev?: Model): audio: false, image, video: false, - pdf: false, + pdf, }, output: { text: true, diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 9d98815cdad..fdd03d52056 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -84,6 +84,8 @@ function sdkKey(npm: string): string | undefined { return "gateway" case "@openrouter/ai-sdk-provider": return "openrouter" + case "merge-gateway-ai-sdk-provider": + return "mergeGateway" case "ai-gateway-provider": // ai-gateway-provider/unified wraps createOpenAICompatible({ name: "Unified" }), // and @ai-sdk/openai-compatible parses compatibleOptions from one of @@ -551,6 +553,12 @@ export function topP(model: Provider.Model) { if (["minimax-m2", "kimi-k2.5", "kimi-k2p5", "kimi-k2-5"].some((s) => id.includes(s))) { return 0.95 } + if ( + ["deepseek-v4-flash-0731", "deepseek-v4-flash:0731"].some((name) => id.includes(name)) || + (id.includes("deepseek-v4-flash") && (model.providerID === "deepseek" || model.providerID.startsWith("opencode"))) + ) { + return 0.95 + } return undefined } @@ -1766,6 +1774,7 @@ function reasoningEffort(model: Provider.Model, effort: string) { case "@ai-sdk/togetherai": case "venice-ai-sdk-provider": case "ai-gateway-provider": + case "merge-gateway-ai-sdk-provider": return { reasoningEffort: effort } case "@ai-sdk/cohere": case "@ai-sdk/perplexity": diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index 7693f5ccfdc..75d6374bfa5 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -29,9 +29,8 @@ export const PRUNE_MINIMUM = 20_000 export const PRUNE_PROTECT = 40_000 const TOOL_OUTPUT_MAX_CHARS = 2_000 const PRUNE_PROTECTED_TOOLS = ["skill"] -const DEFAULT_TAIL_TURNS = 2 const MIN_PRESERVE_RECENT_TOKENS = 2_000 -const MAX_PRESERVE_RECENT_TOKENS = 8_000 +const MAX_PRESERVE_RECENT_TOKENS = 15_000 type Turn = { start: number end: number @@ -226,27 +225,22 @@ const layer = Layer.effect( cfg: ConfigV1.Info model: Provider.Model }) { - const limit = input.cfg.compaction?.tail_turns ?? DEFAULT_TAIL_TURNS - if (limit <= 0) return { head: input.messages, tail_start_id: undefined } + const limit = input.cfg.compaction?.tail_turns + if (limit !== undefined && limit <= 0) return { head: input.messages, tail_start_id: undefined } const budget = preserveRecentBudget({ cfg: input.cfg, model: input.model }) const all = turns(input.messages) if (!all.length) return { head: input.messages, tail_start_id: undefined } - const recent = all.slice(-limit) - const sizes = yield* Effect.forEach( - recent, - (turn) => - estimate({ - messages: input.messages.slice(turn.start, turn.end), - model: input.model, - }), - { concurrency: 1 }, - ) + const recent = limit === undefined ? all : all.slice(-limit) let total = 0 let keep: Tail | undefined for (let i = recent.length - 1; i >= 0; i--) { const turn = recent[i]! - const size = sizes[i] + // estimate lazily so cost stays proportional to the retained tail, not the whole session + const size = yield* estimate({ + messages: input.messages.slice(turn.start, turn.end), + model: input.model, + }) if (total + size <= budget) { total += size keep = { start: turn.start, id: turn.id } @@ -381,10 +375,20 @@ const layer = Layer.effect( { sessionID: input.sessionID }, { context: [], prompt: undefined }, ) - const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context }) const msgs = structuredClone(selected.head) yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) const conversation = msgs.map(serialize).filter(Boolean).join("\n\n") + const nextPrompt = + compacting.prompt ?? + [ + buildPrompt({ + previousSummary, + context: [conversation], + }), + ...compacting.context, + ] + .filter(Boolean) + .join("\n\n") const ctx = yield* InstanceState.context const msg: SessionV1.Assistant = { id: MessageID.ascending(), @@ -430,7 +434,10 @@ const layer = Layer.effect( content: [ { type: "text", - text: [nextPrompt, "The following is the conversation history:", conversation] + text: [ + nextPrompt, + ...(compacting.prompt ? ["The following is the conversation history:", conversation] : []), + ] .filter(Boolean) .join("\n\n"), }, diff --git a/packages/opencode/src/session/prompt/meta.txt b/packages/opencode/src/session/prompt/meta.txt index ae43d7c48e8..52afb82356d 100644 --- a/packages/opencode/src/session/prompt/meta.txt +++ b/packages/opencode/src/session/prompt/meta.txt @@ -1,4 +1,4 @@ -You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by Muse Spark, a large language model trained by Meta MSL. +You are Kilo, a coding agent that helps users with software engineering tasks. You are powered by {{MODEL_NAME}}, a large language model trained by Meta MSL. Use the instructions below and the tools available to assist the user. @@ -53,7 +53,7 @@ Use the instructions below and the tools available to assist the user. - For simple one-off Python computations, such as local file parsing, template rendering, or statistics computations, call `bash` with `python3 -c`. Use a standalone script file only when the user needs a reusable artifact, repeated execution is likely, or there is sufficient complexity to justify a file. - `read` may be used to inspect or locate files, but final numeric or rendered results should come from executed code, not copied text plus mental math. -# Tool Use – OpenCode Specifics +# Tool Use – Kilo Specifics - When `WebFetch` returns a message about a redirect to a different host, you should immediately make a new `WebFetch` request with the redirect URL provided in the response. - When `plan` mode is active, you will see a about this. `plan` mode is for planning, not editing. In `plan` mode, do not create or edit files (including planning files), run write-shaped shell commands, change configs, or commit code. If the user is asking you to perform edit operations in `plan` mode, inform them that `plan` mode is active and that they need to switch to build mode. @@ -61,5 +61,5 @@ Use the instructions below and the tools available to assist the user. - NEVER use comments as a place for long-winded chain-of-thought. Long thinking texts must be generated as private reasoning. Comments in code must be appropriately concise. # User Help & Feedback -- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta Muse Spark. -- When users ask directly about OpenCode (eg. "can OpenCode do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from the OpenCode docs at https://opencode.ai/docs. +- Users can give feedback or report issues at https://github.com/Kilo-Org/kilocode and mention that they are using Meta {{MODEL_NAME}}. +- When users ask directly about Kilo (eg. "can Kilo do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from the Kilo docs at https://kilo.ai/docs. diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index cab48dda633..fe9b81f3777 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -25,8 +25,10 @@ export type Retryable = { export const RETRY_INITIAL_DELAY = 2000 export const RETRY_BACKOFF_FACTOR = 2 +export const RETRY_JITTER_FACTOR = 0.25 export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout +export const RETRY_MAX_RETRIES = 5 const RETRYABLE_MESSAGE_PATTERNS = [ /429|500|502|503|504|524/i, @@ -41,7 +43,7 @@ function cap(ms: number) { return Math.min(ms, RETRY_MAX_DELAY) } -export function delay(attempt: number, error?: SessionV1.APIError) { +export function delay(attempt: number, error?: SessionV1.APIError, random = Math.random()) { if (error) { const headers = error.data.responseHeaders if (headers) { @@ -67,11 +69,16 @@ export function delay(attempt: number, error?: SessionV1.APIError) { } } - return cap(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)) + return cap(exponential(attempt, random)) } } - return cap(Math.min(RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1), RETRY_MAX_DELAY_NO_HEADERS)) + return cap(Math.min(exponential(attempt, random), RETRY_MAX_DELAY_NO_HEADERS)) +} + +function exponential(attempt: number, random: number) { + const base = RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1) + return Math.ceil(base + base * RETRY_JITTER_FACTOR * random) } export function retryable(error: Err, provider: string) { @@ -182,6 +189,7 @@ export function policy(opts: { const error = opts.parse(meta.input) const retry = retryable(error, opts.provider) if (!retry) return Cause.done(meta.attempt) + if (meta.attempt > RETRY_MAX_RETRIES) return Cause.done(meta.attempt) return Effect.gen(function* () { const wait = delay(meta.attempt, SessionV1.APIError.isInstance(error) ? error : undefined) const now = yield* Clock.currentTimeMillis diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index b94e7942658..d0c608b203f 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -25,7 +25,10 @@ import { MCP } from "@/mcp" import { PermissionV1 } from "@opencode-ai/core/v1/permission" export function provider(model: Provider.Model) { - if (model.api.id.includes("muse-spark")) return [PROMPT_META] + if (model.api.id.includes("muse")) { + const name = model.api.id.includes("muse-glimmer") ? "Muse Glimmer" : "Muse Spark" + return [PROMPT_META.replaceAll("{{MODEL_NAME}}", name)] + } if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) return [PROMPT_BEAST] if (model.api.id.includes("gpt")) { @@ -37,7 +40,11 @@ export function provider(model: Provider.Model) { if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI] if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC] if (model.api.id.toLowerCase().includes("trinity")) return [PROMPT_TRINITY] - if (model.api.id.toLowerCase().includes("kimi")) return [PROMPT_KIMI] + if ( + model.api.id.toLowerCase().includes("kimi") || + ["kimi-for-coding", "moonshotai", "moonshotai-cn"].includes(model.providerID) + ) + return [PROMPT_KIMI] return [PROMPT_DEFAULT] } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 4f6ba560188..c71fc44d43e 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -597,12 +597,12 @@ accountTokenIt.instance("resolves env templates in account config with account t }), ) -it.instance("validates config schema and throws on invalid fields", () => +it.instance("validates config schema and throws on invalid values", () => Effect.gen(function* () { const test = yield* TestInstance yield* writeConfigEffect(test.directory, { $schema: "https://opencode.ai/config.json", - invalid_field: "should cause error", + model: 42, }) const exit = yield* Config.use.get().pipe(Effect.exit) expect(Exit.isFailure(exit)).toBe(true) @@ -1331,7 +1331,7 @@ it.instance("permission config preserves user key order", () => }), ) -test("config parser preserves permission order while rejecting unknown top-level keys", () => { +test("config parser preserves permission order while ignoring unknown top-level keys", () => { const config = ConfigParse.schema( ConfigV1.Info, { @@ -1340,18 +1340,13 @@ test("config parser preserves permission order while rejecting unknown top-level "*": "deny", edit: "ask", }, + plugins: ["example"], }, "test", ) expect(Object.keys(config.permission!)).toEqual(["bash", "*", "edit"]) - try { - ConfigParse.schema(ConfigV1.Info, { invalid_field: true }, "test") - throw new Error("expected config parse to fail") - } catch (err) { - const error = err as { data?: { issues?: Array<{ code?: string; keys?: string[]; path?: string[] }> } } - expect(error.data?.issues?.[0]).toMatchObject({ code: "unrecognized_keys", keys: ["invalid_field"], path: [] }) - } + expect(config).not.toHaveProperty("plugins") }) // MCP config merging tests diff --git a/packages/opencode/test/plugin/github-copilot-models.test.ts b/packages/opencode/test/plugin/github-copilot-models.test.ts index e6d89fe01cc..d968402e3b8 100644 --- a/packages/opencode/test/plugin/github-copilot-models.test.ts +++ b/packages/opencode/test/plugin/github-copilot-models.test.ts @@ -187,6 +187,74 @@ test("converts Copilot AIC token prices to USD per million tokens", async () => expect(models["ignored-non-chat-record"]).toBeUndefined() }) +test("detects PDF input support when vision and media type are advertised", async () => { + globalThis.fetch = mock(() => + Promise.resolve( + new Response( + JSON.stringify({ + data: [ + { + model_picker_enabled: true, + id: "pdf-model", + name: "PDF Model", + version: "pdf-model-2026-06-01", + capabilities: { + family: "pdf-model", + limits: { + max_context_window_tokens: 128000, + max_output_tokens: 16384, + max_prompt_tokens: 128000, + vision: { + max_prompt_image_size: 10000000, + max_prompt_images: 10, + supported_media_types: ["application/pdf"], + }, + }, + supports: { + streaming: true, + vision: true, + tool_calls: true, + }, + }, + }, + { + model_picker_enabled: true, + id: "vision-only-model", + name: "Vision Only Model", + version: "vision-only-model-2026-06-01", + capabilities: { + family: "vision-only-model", + limits: { + max_context_window_tokens: 128000, + max_output_tokens: 16384, + max_prompt_tokens: 128000, + vision: { + max_prompt_image_size: 10000000, + max_prompt_images: 10, + supported_media_types: ["image/png"], + }, + }, + supports: { + streaming: true, + vision: true, + tool_calls: true, + }, + }, + }, + ], + }), + { status: 200 }, + ), + ), + ) as unknown as typeof fetch + + const models = (await CopilotModels.get("https://api.githubcopilot.com")).models + const model = models["pdf-model"] + + expect(model.capabilities.input.pdf).toBe(true) + expect(models["vision-only-model"].capabilities.input.pdf).toBe(false) +}) + test("uses zero cost when Copilot reports a zero billing batch size", async () => { globalThis.fetch = mock(() => Promise.resolve( diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 44e82c7e1f4..df23a5c4963 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1548,6 +1548,33 @@ test("models.dev reasoning options replace generated variants and unsupported to expect(models["gemini-3-pro-fast"].variants).toEqual(models.override.variants) }) +test("MERGE Gateway exposes declared effort variants without model-specific handling", () => { + const provider = { + id: "merge-gateway", + name: "MERGE Gateway", + env: ["MERGE_GATEWAY_API_KEY"], + npm: "merge-gateway-ai-sdk-provider", + models: { + "openai/gpt-5.6-sol": { + id: "openai/gpt-5.6-sol", + name: "GPT-5.6 Sol", + reasoning: true, + reasoning_options: [{ type: "effort", values: ["none", "low", "medium", "high", "xhigh", "max"] }], + limit: { context: 128_000, output: 64_000 }, + }, + }, + } as unknown as ModelsDev.Provider + + expect(Provider.fromModelsDevProvider(provider).models["openai/gpt-5.6-sol"].variants).toEqual({ + none: { reasoningEffort: "none" }, + low: { reasoningEffort: "low" }, + medium: { reasoningEffort: "medium" }, + high: { reasoningEffort: "high" }, + xhigh: { reasoningEffort: "xhigh" }, + max: { reasoningEffort: "max" }, + }) +}) + test("public provider info omits invalid models", () => { const provider = Provider.fromModelsDevProvider({ id: "test", diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 9897c543fb8..70165898740 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -3272,6 +3272,37 @@ describe("ProviderTransform sampling defaults - Gemini", () => { }) }) +describe("ProviderTransform sampling defaults - DeepSeek", () => { + const model = (providerID: string, id: string) => + ({ + id: `${providerID}/${id}`, + providerID, + api: { id }, + }) as any + + test.each([ + ["deepseek", "deepseek-v4-flash"], + ["opencode", "deepseek-v4-flash"], + ["opencode-go", "deepseek-v4-flash"], + ["openrouter", "deepseek/deepseek-v4-flash-0731"], + ["ollama-cloud", "deepseek-v4-flash:0731"], + ])("defaults top_p for %s/%s", (providerID, id) => { + expect(ProviderTransform.temperature(model(providerID, id))).toBeUndefined() + expect(ProviderTransform.topP(model(providerID, id))).toBe(0.95) + expect(ProviderTransform.topK(model(providerID, id))).toBeUndefined() + }) + + test.each([ + ["openrouter", "deepseek/deepseek-v4-flash"], + ["vercel", "deepseek/deepseek-v4-flash"], + ["custom", "deepseek-ai/DeepSeek-V4-Flash"], + ])("preserves legacy defaults for %s/%s", (providerID, id) => { + expect(ProviderTransform.temperature(model(providerID, id))).toBeUndefined() + expect(ProviderTransform.topP(model(providerID, id))).toBeUndefined() + expect(ProviderTransform.topK(model(providerID, id))).toBeUndefined() + }) +}) + describe("ProviderTransform.reasoningVariants", () => { const model = (reasoning_options: ModelsDev.Model["reasoning_options"]) => ({ reasoning_options }) as ModelsDev.Model const target = (npm: string, id = "test-model") => @@ -3339,6 +3370,7 @@ describe("ProviderTransform.reasoningVariants", () => { ["@ai-sdk/togetherai", { reasoningEffort: "high" }], ["venice-ai-sdk-provider", { reasoningEffort: "high" }], ["ai-gateway-provider", { reasoningEffort: "high" }], + ["merge-gateway-ai-sdk-provider", { reasoningEffort: "high" }], ["@ai-sdk/amazon-bedrock", { reasoningConfig: { type: "enabled", maxReasoningEffort: "high" } }], ])("converts effort for %s", (npm, expected, ...args) => { const id = args[0] as string | undefined @@ -5524,6 +5556,25 @@ describe("ProviderTransform.providerOptions - ai-gateway-provider", () => { }) }) +describe("ProviderTransform.providerOptions - merge-gateway-ai-sdk-provider", () => { + const model = { + id: "merge-gateway/openai/gpt-5.6-sol", + providerID: "merge-gateway", + api: { + id: "openai/gpt-5.6-sol", + url: "https://api-gateway.merge.dev/v1/ai-sdk", + npm: "merge-gateway-ai-sdk-provider", + }, + capabilities: { reasoning: true }, + } as any + + test("routes normalized effort under the adapter's mergeGateway key", () => { + expect(ProviderTransform.providerOptions(model, { reasoningEffort: "high" })).toEqual({ + mergeGateway: { reasoningEffort: "high" }, + }) + }) +}) + describe("ProviderTransform.options - kimi family adaptive thinking", () => { const createModel = (overrides: Record = {}) => ({ diff --git a/packages/opencode/test/server/httpapi-instance-context.test.ts b/packages/opencode/test/server/httpapi-instance-context.test.ts index 263e2c472b9..9ecdd0e4756 100644 --- a/packages/opencode/test/server/httpapi-instance-context.test.ts +++ b/packages/opencode/test/server/httpapi-instance-context.test.ts @@ -161,6 +161,21 @@ describe("HttpApi instance context middleware", () => { }), ) + it.live("persists the routed project while loading instance context", () => + Effect.gen(function* () { + const dir = yield* tmpdirScoped({ git: true }) + const project = yield* Project.Service + yield* serveProbe() + + const response = yield* HttpClient.get(`/probe?directory=${encodeURIComponent(dir)}`) + + expect(response.status).toBe(200) + const saved = (yield* project.list()).find((item) => item.worktree === dir) + expect(saved).toBeDefined() + expect(saved?.id).not.toBe("global") + }), + ) + it.live("falls back to the raw directory when URI decoding fails", () => Effect.gen(function* () { yield* serveProbe() diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 0dff7354b5b..4f0981fa647 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -365,6 +365,20 @@ function autocontinue(enabled: boolean) { }) } +function compactionContext(context: string) { + return Layer.mock(Plugin.Service)({ + trigger: (name: Name, _input: Input, output: Output) => { + if (name !== "experimental.session.compacting") return Effect.succeed(output) + return Effect.sync(() => { + ;(output as { context: string[] }).context.push(context) + return output + }) + }, + list: () => Effect.succeed([]), + init: () => Effect.void, + }) +} + describe("session.compaction.isOverflow", () => { it.live( "returns true when token count exceeds usable context", @@ -1389,11 +1403,21 @@ describe("session.compaction.process", () => { const captured = JSON.stringify(messages) expect(messages).toHaveLength(1) expect(messages[0]?.role).toBe("user") + expect(captured).toContain("Here is the conversation so far:") + expect(captured).toContain("") + expect(captured.indexOf("[User]: older context")).toBeLessThan( + captured.indexOf("Create a new anchored summary"), + ) expect(captured).toContain("[User]: older context") expect(captured).not.toContain("keep this turn") expect(captured).not.toContain("and this one too") expect(captured).not.toContain("What did we do so far?") - }).pipe(withCompaction({ llm: stub.llmLayer })) + }).pipe( + withCompaction({ + llm: stub.llmLayer, + config: cfg({ tail_turns: 2, preserve_recent_tokens: 10_000 }), + }), + ) }, { git: true }, ) @@ -1430,9 +1454,11 @@ describe("session.compaction.process", () => { expect(parent).toBeTruthy() yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false }) - expect(captured).toContain("") + expect(captured).toContain("") expect(captured).toContain("summary one") expect(captured.match(/summary one/g)?.length).toBe(1) + expect(captured.indexOf("latest turn")).toBeLessThan(captured.indexOf("")) + expect(captured).toContain("summary of the conversation before the above") expect(captured).toContain("## Important Details") expect(captured).toContain("## Work State") }).pipe(withCompaction({ llm: stub.llmLayer })) @@ -1440,6 +1466,49 @@ describe("session.compaction.process", () => { { git: true }, ) + itCompaction.instance( + "keeps plugin context outside the serialized conversation", + () => { + const stub = llm() + let captured = "" + stub.push( + reply("summary", (input) => { + captured = JSON.stringify(input.messages) + }), + ) + + return Effect.gen(function* () { + const ssn = yield* SessionNs.Service + const session = yield* ssn.create({}) + yield* createUserMessage(session.id, "older context") + yield* createUserMessage(session.id, "keep this turn") + yield* createUserMessage(session.id, "and this one too") + yield* createCompactionMarker(session.id) + + const msgs = yield* ssn.messages({ sessionID: session.id }) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + yield* SessionCompaction.use.process({ + parentID: parent!, + messages: msgs, + sessionID: session.id, + auto: false, + }) + + expect(captured).toContain("Prioritize unresolved migration details") + expect(captured.indexOf("")).toBeLessThan( + captured.indexOf("Prioritize unresolved migration details"), + ) + }).pipe( + withCompaction({ + llm: stub.llmLayer, + plugin: compactionContext("Prioritize unresolved migration details"), + }), + ) + }, + { git: true }, + ) + itCompaction.instance( "serializes repeated compaction history as one user message", () => { diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 018f76fc3ea..e21b12c0895 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -35,10 +35,18 @@ function wrap(message: unknown): ReturnType { describe("session.retry.delay", () => { test("caps delay at 30 seconds when headers missing", () => { const error = apiError() - const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.delay(index + 1, error)) + const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.delay(index + 1, error, 0)) expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 30000, 30000, 30000, 30000, 30000, 30000]) }) + test("adds jitter to exponential delays", () => { + const error = apiError() + expect(SessionRetry.delay(1, error, 0)).toBe(2000) + expect(SessionRetry.delay(1, error, 1)).toBe(2500) + expect(SessionRetry.delay(4, error, 1)).toBe(20000) + expect(SessionRetry.delay(5, error, 1)).toBe(30000) + }) + test("prefers retry-after-ms when shorter than exponential", () => { const error = apiError({ "retry-after-ms": "1500" }) expect(SessionRetry.delay(4, error)).toBe(1500) @@ -59,18 +67,18 @@ describe("session.retry.delay", () => { test("ignores invalid retry hints", () => { const error = apiError({ "retry-after": "not-a-number" }) - expect(SessionRetry.delay(1, error)).toBe(2000) + expect(SessionRetry.delay(1, error, 0)).toBe(2000) }) test("ignores malformed date retry hints", () => { const error = apiError({ "retry-after": "Invalid Date String" }) - expect(SessionRetry.delay(1, error)).toBe(2000) + expect(SessionRetry.delay(1, error, 0)).toBe(2000) }) test("ignores past date retry hints", () => { const pastDate = new Date(Date.now() - 5000).toUTCString() const error = apiError({ "retry-after": pastDate }) - expect(SessionRetry.delay(1, error)).toBe(2000) + expect(SessionRetry.delay(1, error, 0)).toBe(2000) }) test("uses retry-after values even when exceeding 10 minutes with headers", () => { @@ -115,6 +123,29 @@ describe("session.retry.delay", () => { }) }), ) + + it.instance("policy stops after five retries", () => + Effect.gen(function* () { + const attempts: number[] = [] + const error = apiError({ "retry-after-ms": "0" }) + const step = yield* Schedule.toStepWithMetadata( + SessionRetry.policy({ + provider: "test", + parse: Schema.decodeUnknownSync(SessionV1.APIError.Schema), + set: (info) => + Effect.sync(() => { + attempts.push(info.attempt) + }), + }), + ) + + yield* Effect.forEach(Array.from({ length: SessionRetry.RETRY_MAX_RETRIES + 1 }), () => + Effect.ignore(step(error)), + ) + + expect(attempts).toStrictEqual([1, 2, 3, 4, 5]) + }), + ) }) describe("session.retry.retryable", () => { diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index d9d47be3df9..09bac3f8c5f 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -85,9 +85,28 @@ const it = testEffect( describe("session.system", () => { test("selects the Meta prompt for Muse Spark model IDs", () => { - expect(SystemPrompt.provider({ api: { id: "meta/muse-spark-preview" } } as Provider.Model)[0]).toContain( - "Meta Muse Spark", - ) + for (const id of ["meta/muse-spark-preview", "muse-spark-1.1", "muse-spark-1.2"]) { + const prompt = SystemPrompt.provider({ api: { id } } as Provider.Model)[0] + expect(prompt).toContain("powered by Muse Spark,") + expect(prompt).toContain("using Meta Muse Spark.") + expect(prompt).not.toContain("{{MODEL_NAME}}") + } + }) + + test("selects the Meta prompt for Muse Glimmer model IDs", () => { + for (const id of ["meta/muse-glimmer", "meta/muse-glimmer-30b", "muse-glimmer-30b"]) { + const prompt = SystemPrompt.provider({ api: { id } } as Provider.Model)[0] + expect(prompt).toContain("powered by Muse Glimmer,") + expect(prompt).toContain("using Meta Muse Glimmer.") + expect(prompt).not.toContain("{{MODEL_NAME}}") + } + }) + + test("selects the Kimi prompt for official provider model IDs", () => { + for (const providerID of ["kimi-for-coding", "moonshotai", "moonshotai-cn"]) { + const prompt = SystemPrompt.provider({ providerID, api: { id: "k3" } } as Provider.Model)[0] + expect(prompt).toContain("# Prompt and Tool Use") + } }) it.effect("skills output is sorted by name and stable across calls", () => diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 4ab44181752..2e2e5e1d0a2 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/plugin", - "version": "7.4.20", + "version": "7.4.23", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json index 51c5a266464..dd504ea6f3a 100644 --- a/packages/sdk/js/package.json +++ b/packages/sdk/js/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/sdk", - "version": "7.4.20", + "version": "7.4.23", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 702ef81d834..337a6870950 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/server", - "version": "7.4.20", + "version": "7.4.23", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/package.json b/packages/session-ui/package.json index b6cd5e46942..2d3356a79f2 100644 --- a/packages/session-ui/package.json +++ b/packages/session-ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/session-ui", - "version": "7.4.20", + "version": "7.4.23", "private": true, "type": "module", "license": "MIT", diff --git a/packages/session-ui/src/context/data.tsx b/packages/session-ui/src/context/data.tsx index bc1011503f0..4b3f5275588 100644 --- a/packages/session-ui/src/context/data.tsx +++ b/packages/session-ui/src/context/data.tsx @@ -5,6 +5,10 @@ import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr" export type NormalizedProviderListResponse = { all: Map + defaultModel?: { + providerID: string + modelID: string + } | null default: { [key: string]: string } diff --git a/packages/tui/package.json b/packages/tui/package.json index 6a8c46a73d7..3ce311fbe26 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@opencode-ai/tui", - "version": "7.4.20", + "version": "7.4.23", "private": true, "type": "module", "license": "MIT", diff --git a/packages/ui/package.json b/packages/ui/package.json index f3f260dbc8b..48881ebe14d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/ui", - "version": "7.4.20", + "version": "7.4.23", "type": "module", "license": "MIT", "repository": { diff --git a/packages/ui/src/theme/themes/oc-2.json b/packages/ui/src/theme/themes/oc-2.json index ef05ccfbffc..461262533ab 100644 --- a/packages/ui/src/theme/themes/oc-2.json +++ b/packages/ui/src/theme/themes/oc-2.json @@ -25,7 +25,7 @@ "border-weak-base": "#DBDBDB", "border-weaker-base": "#E8E8E8", "icon-base": "#8F8F8F", - "icon-weak-base": "C7C7C7", + "icon-weak-base": "#C7C7C7", "surface-raised-base": "#F3F3F3", "surface-raised-base-hover": "#EDEDED", "surface-base": "#F8F8F8", diff --git a/packages/ui/src/v2/components/button-v2.css b/packages/ui/src/v2/components/button-v2.css index 531efe9f811..20a8855503f 100644 --- a/packages/ui/src/v2/components/button-v2.css +++ b/packages/ui/src/v2/components/button-v2.css @@ -195,9 +195,9 @@ /* Loading */ [data-component="button-v2"][data-variant="loading"] { - background: #f2f2f2; + background: var(--v2-background-bg-layer-02); color: var(--v2-text-text-base); - box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.08); + box-shadow: inset 0 0 0 0.5px var(--v2-border-border-muted); cursor: default; pointer-events: none; } diff --git a/patches/@ai-sdk%2Fgroq@3.0.31.patch b/patches/@ai-sdk%2Fgroq@3.0.31.patch new file mode 100644 index 00000000000..f26a4bedfa2 --- /dev/null +++ b/patches/@ai-sdk%2Fgroq@3.0.31.patch @@ -0,0 +1,79 @@ +diff --git a/dist/index.d.mts b/dist/index.d.mts +index 8b23996dcce6c1ad5b17ef59f92196fb97312d79..80be2e52a347042b89da8e502834afd92120877a 100644 +--- a/dist/index.d.mts ++++ b/dist/index.d.mts +@@ -10,13 +10,7 @@ declare const groqLanguageModelOptions: z.ZodObject<{ + raw: "raw"; + hidden: "hidden"; + }>>; +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + parallelToolCalls: z.ZodOptional; + user: z.ZodOptional; + structuredOutputs: z.ZodOptional; +diff --git a/dist/index.d.ts b/dist/index.d.ts +index 8b23996dcce6c1ad5b17ef59f92196fb97312d79..80be2e52a347042b89da8e502834afd92120877a 100644 +--- a/dist/index.d.ts ++++ b/dist/index.d.ts +@@ -10,13 +10,7 @@ declare const groqLanguageModelOptions: z.ZodObject<{ + raw: "raw"; + hidden: "hidden"; + }>>; +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + parallelToolCalls: z.ZodOptional; + user: z.ZodOptional; + structuredOutputs: z.ZodOptional; +diff --git a/dist/index.js b/dist/index.js +index 45a104f2e0775761858eac2a82ced64bceba1f5e..f60ac36f4a064d527e8f8881b1d6c58ff69286a3 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -214,7 +214,7 @@ var groqLanguageModelOptions = import_v4.z.object({ + * Specifies the reasoning effort level for model inference. + * @see https://console.groq.com/docs/reasoning#reasoning-effort + */ +- reasoningEffort: import_v4.z.enum(["none", "default", "low", "medium", "high"]).optional(), ++ reasoningEffort: import_v4.z.string().optional(), + /** + * Whether to enable parallel function calling during tool use. Default to true. + */ +diff --git a/dist/index.mjs b/dist/index.mjs +index c644c32235d8fa88c51c0fc6958feb1da4877c96..2c2f81869673eb4633e843d93ff5376abf1e67d0 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -203,7 +203,7 @@ var groqLanguageModelOptions = z.object({ + * Specifies the reasoning effort level for model inference. + * @see https://console.groq.com/docs/reasoning#reasoning-effort + */ +- reasoningEffort: z.enum(["none", "default", "low", "medium", "high"]).optional(), ++ reasoningEffort: z.string().optional(), + /** + * Whether to enable parallel function calling during tool use. Default to true. + */ +diff --git a/src/groq-chat-options.ts b/src/groq-chat-options.ts +index 3812cdf53308709f166f05c58c5d46a5d8189c8b..af520c5459bd752b3cce03c3b4afbeed31157d90 100644 +--- a/src/groq-chat-options.ts ++++ b/src/groq-chat-options.ts +@@ -33,9 +33,7 @@ export const groqLanguageModelOptions = z.object({ + * Specifies the reasoning effort level for model inference. + * @see https://console.groq.com/docs/reasoning#reasoning-effort + */ +- reasoningEffort: z +- .enum(['none', 'default', 'low', 'medium', 'high']) +- .optional(), ++ reasoningEffort: z.string().optional(), + + /** + * Whether to enable parallel function calling during tool use. Default to true. diff --git a/patches/@ai-sdk%2Fmistral@3.0.51.patch b/patches/@ai-sdk%2Fmistral@3.0.51.patch index 141b14a689b..f76ed1c126e 100644 --- a/patches/@ai-sdk%2Fmistral@3.0.51.patch +++ b/patches/@ai-sdk%2Fmistral@3.0.51.patch @@ -2,10 +2,12 @@ diff --git a/dist/index.d.mts b/dist/index.d.mts index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts -@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ - none: "none"; - high: "high"; - }>>; +@@ -13,7 +13,5 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + promptCacheKey: z.ZodOptional; }, z.core.$strip>; type MistralLanguageModelOptions = z.infer; @@ -14,10 +16,12 @@ diff --git a/dist/index.d.ts b/dist/index.d.ts index 1bde0b9f8cbe6771a52c1041095c9dddfe8e5b6c..0ca2ffb2a0c9327aed5ddcf0004500dc8b42569f 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts -@@ -14,6 +14,7 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ - none: "none"; - high: "high"; - }>>; +@@ -13,7 +13,5 @@ declare const mistralLanguageModelOptions: z.ZodObject<{ +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + promptCacheKey: z.ZodOptional; }, z.core.$strip>; type MistralLanguageModelOptions = z.infer; @@ -69,7 +73,7 @@ index d3f904c12a1d582cc7b9e9a2d30273e1a8505b28..267f34e20ea392b7a85ad5259d72d506 * - `'none'`: Disable reasoning */ - reasoningEffort: import_v4.z.enum(["high", "none"]).optional() -+ reasoningEffort: import_v4.z.enum(["high", "none"]).optional(), ++ reasoningEffort: import_v4.z.string().optional(), + promptCacheKey: import_v4.z.string().optional() }); @@ -268,7 +272,7 @@ index d2eff622c1b84a96bdeb4012cb0206a33012a04d..3bff11ddd6136ada45809568828cbc8f * - `'none'`: Disable reasoning */ - reasoningEffort: z.enum(["high", "none"]).optional() -+ reasoningEffort: z.enum(["high", "none"]).optional(), ++ reasoningEffort: z.string().optional(), + promptCacheKey: z.string().optional() }); @@ -655,7 +659,8 @@ index 54b29c08517d348995b6ca093b11160e453d5c8b..de30c3e7d924889339e38b1067cb26e9 @@ -64,6 +64,11 @@ export const mistralLanguageModelOptions = z.object({ * - `'none'`: Disable reasoning */ - reasoningEffort: z.enum(['high', 'none']).optional(), +- reasoningEffort: z.enum(['high', 'none']).optional(), ++ reasoningEffort: z.string().optional(), + + /** + * A stable identifier used to route requests with shared prompt prefixes. diff --git a/patches/@ai-sdk%2Fxai@3.0.102.patch b/patches/@ai-sdk%2Fxai@3.0.102.patch index 27a46014fca..1ea20de9cd9 100644 --- a/patches/@ai-sdk%2Fxai@3.0.102.patch +++ b/patches/@ai-sdk%2Fxai@3.0.102.patch @@ -1,8 +1,33 @@ diff --git a/dist/index.d.mts b/dist/index.d.mts -index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..990ef4195bc67b6d25f249e1c81cf51710390f9a 100644 +index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..6ac6e2873b7681ac632c903694aa38c7b09773fa 100644 --- a/dist/index.d.mts +++ b/dist/index.d.mts -@@ -78,6 +78,7 @@ declare const xaiLanguageModelResponsesOptions: z.ZodObject<{ +@@ -5,12 +5,7 @@ import { FetchFunction } from '@ai-sdk/provider-utils'; + + type XaiChatModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20-0309-non-reasoning' | 'grok-4.20-multi-agent-0309' | 'grok-build-0.1' | (string & {}); + declare const xaiLanguageModelChatOptions: z.ZodObject<{ +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + logprobs: z.ZodOptional; + topLogprobs: z.ZodOptional; + parallel_function_calling: z.ZodOptional; +@@ -68,16 +63,12 @@ type XaiResponsesModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20- + * @see https://docs.x.ai/docs/api-reference#create-new-response + */ + declare const xaiLanguageModelResponsesOptions: z.ZodObject<{ +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + logprobs: z.ZodOptional; topLogprobs: z.ZodOptional; store: z.ZodOptional; previousResponseId: z.ZodOptional; @@ -11,10 +36,35 @@ index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..990ef4195bc67b6d25f249e1c81cf517 "file_search_call.results": "file_search_call.results"; }>>>>; diff --git a/dist/index.d.ts b/dist/index.d.ts -index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..990ef4195bc67b6d25f249e1c81cf51710390f9a 100644 +index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..6ac6e2873b7681ac632c903694aa38c7b09773fa 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts -@@ -78,6 +78,7 @@ declare const xaiLanguageModelResponsesOptions: z.ZodObject<{ +@@ -5,12 +5,7 @@ import { FetchFunction } from '@ai-sdk/provider-utils'; + + type XaiChatModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20-0309-non-reasoning' | 'grok-4.20-multi-agent-0309' | 'grok-build-0.1' | (string & {}); + declare const xaiLanguageModelChatOptions: z.ZodObject<{ +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + logprobs: z.ZodOptional; + topLogprobs: z.ZodOptional; + parallel_function_calling: z.ZodOptional; +@@ -68,16 +63,12 @@ type XaiResponsesModelId = 'grok-4.3' | 'grok-4.20-0309-reasoning' | 'grok-4.20- + * @see https://docs.x.ai/docs/api-reference#create-new-response + */ + declare const xaiLanguageModelResponsesOptions: z.ZodObject<{ +- reasoningEffort: z.ZodOptional>; ++ reasoningEffort: z.ZodOptional; + logprobs: z.ZodOptional; topLogprobs: z.ZodOptional; store: z.ZodOptional; previousResponseId: z.ZodOptional; @@ -23,9 +73,18 @@ index 266c5ffdd9ee74ff95908ce90858ee4369d4e4ae..990ef4195bc67b6d25f249e1c81cf517 "file_search_call.results": "file_search_call.results"; }>>>>; diff --git a/dist/index.js b/dist/index.js -index 717b74538f5c8f0d6ab1475ebb2a84a47ccd3950..dd7dbeb3bc307e0d355f4bb4939d06cc8eae7528 100644 +index 717b74538f5c8f0d6ab1475ebb2a84a47ccd3950..0fd8f0d1cae951cd24401034a9c1dba762d9fd84 100644 --- a/dist/index.js +++ b/dist/index.js +@@ -246,7 +246,7 @@ var searchSourceSchema = import_v4.z.discriminatedUnion("type", [ + rssSourceSchema + ]); + var xaiLanguageModelChatOptions = import_v4.z.object({ +- reasoningEffort: import_v4.z.enum(["none", "low", "medium", "high"]).optional(), ++ reasoningEffort: import_v4.z.string().optional(), + logprobs: import_v4.z.boolean().optional(), + topLogprobs: import_v4.z.number().int().min(0).max(8).optional(), + /** @@ -1119,6 +1119,14 @@ async function convertToXaiResponsesInput({ type: "input_file", file_url: block.data.toString() @@ -41,6 +100,15 @@ index 717b74538f5c8f0d6ab1475ebb2a84a47ccd3950..dd7dbeb3bc307e0d355f4bb4939d06cc } else { throw new import_provider4.UnsupportedFunctionalityError({ functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)` +@@ -1746,7 +1754,7 @@ var xaiLanguageModelResponsesOptions = import_v47.z.object({ + * tokens), `medium` and `high` (uses more reasoning tokens). Not all models + * support reasoning effort; see xAI's docs for the values each model accepts. + */ +- reasoningEffort: import_v47.z.enum(["none", "low", "medium", "high"]).optional(), ++ reasoningEffort: import_v47.z.string().optional(), + logprobs: import_v47.z.boolean().optional(), + topLogprobs: import_v47.z.number().int().min(0).max(8).optional(), + /** @@ -1760,6 +1768,10 @@ var xaiLanguageModelResponsesOptions = import_v47.z.object({ * The ID of the previous response from the model. */ @@ -63,9 +131,18 @@ index 717b74538f5c8f0d6ab1475ebb2a84a47ccd3950..dd7dbeb3bc307e0d355f4bb4939d06cc }; if (xaiTools2 && xaiTools2.length > 0) { diff --git a/dist/index.mjs b/dist/index.mjs -index a26af109585fc2bd3053b320142aa869c06d36f4..774adaf971b648544317a4fc65d0c56e488d4fc7 100644 +index a26af109585fc2bd3053b320142aa869c06d36f4..5faca56477b4e55a87f6f57850731c7d3e1721a5 100644 --- a/dist/index.mjs +++ b/dist/index.mjs +@@ -230,7 +230,7 @@ var searchSourceSchema = z.discriminatedUnion("type", [ + rssSourceSchema + ]); + var xaiLanguageModelChatOptions = z.object({ +- reasoningEffort: z.enum(["none", "low", "medium", "high"]).optional(), ++ reasoningEffort: z.string().optional(), + logprobs: z.boolean().optional(), + topLogprobs: z.number().int().min(0).max(8).optional(), + /** @@ -1122,6 +1122,14 @@ async function convertToXaiResponsesInput({ type: "input_file", file_url: block.data.toString() @@ -81,6 +158,15 @@ index a26af109585fc2bd3053b320142aa869c06d36f4..774adaf971b648544317a4fc65d0c56e } else { throw new UnsupportedFunctionalityError3({ functionality: `file part media type ${block.mediaType} as inline data (xAI Responses requires a URL or a Files API reference for non-image files)` +@@ -1749,7 +1757,7 @@ var xaiLanguageModelResponsesOptions = z7.object({ + * tokens), `medium` and `high` (uses more reasoning tokens). Not all models + * support reasoning effort; see xAI's docs for the values each model accepts. + */ +- reasoningEffort: z7.enum(["none", "low", "medium", "high"]).optional(), ++ reasoningEffort: z7.string().optional(), + logprobs: z7.boolean().optional(), + topLogprobs: z7.number().int().min(0).max(8).optional(), + /** @@ -1763,6 +1771,10 @@ var xaiLanguageModelResponsesOptions = z7.object({ * The ID of the previous response from the model. */ @@ -158,9 +244,18 @@ index f90df62eb9a30154388b1390e9f3acc3ccc022bf..00e61cba6cf048ae0045be692f33cb7e if (xaiTools && xaiTools.length > 0) { diff --git a/src/responses/xai-responses-options.ts b/src/responses/xai-responses-options.ts -index f8e96c061bf8793a402ababb8cad65bb2ad6aead..15c168892c1e8755453c61d3061e958cfd51ac71 100644 +index f8e96c061bf8793a402ababb8cad65bb2ad6aead..2a39a36221ab23ea0000bff1d7854c5bce3f9d74 100644 --- a/src/responses/xai-responses-options.ts +++ b/src/responses/xai-responses-options.ts +@@ -18,7 +18,7 @@ export const xaiLanguageModelResponsesOptions = z.object({ + * tokens), `medium` and `high` (uses more reasoning tokens). Not all models + * support reasoning effort; see xAI's docs for the values each model accepts. + */ +- reasoningEffort: z.enum(['none', 'low', 'medium', 'high']).optional(), ++ reasoningEffort: z.string().optional(), + logprobs: z.boolean().optional(), + topLogprobs: z.number().int().min(0).max(8).optional(), + /** @@ -32,6 +32,10 @@ export const xaiLanguageModelResponsesOptions = z.object({ * The ID of the previous response from the model. */ @@ -172,3 +267,16 @@ index f8e96c061bf8793a402ababb8cad65bb2ad6aead..15c168892c1e8755453c61d3061e958c /** * Specify additional output data to include in the model response. * Example values: 'file_search_call.results'. +diff --git a/src/xai-chat-options.ts b/src/xai-chat-options.ts +index d70a72a9fa01da2c711c291da5ce949efbde60b5..fd6b1ae025388b614f08b620244be553199479ca 100644 +--- a/src/xai-chat-options.ts ++++ b/src/xai-chat-options.ts +@@ -51,7 +51,7 @@ const searchSourceSchema = z.discriminatedUnion('type', [ + + // xai-specific provider options + export const xaiLanguageModelChatOptions = z.object({ +- reasoningEffort: z.enum(['none', 'low', 'medium', 'high']).optional(), ++ reasoningEffort: z.string().optional(), + logprobs: z.boolean().optional(), + topLogprobs: z.number().int().min(0).max(8).optional(), + diff --git a/script/beta.ts b/script/beta.ts index d738c36ff01..b90c399828c 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -3,7 +3,7 @@ import { $ } from "bun" import fs from "fs/promises" -const model = "opencode/gpt-5.3-codex" +const model = "opencode/deepseek-v4-flash" interface PR { number: number @@ -86,7 +86,7 @@ async function build() { console.log(" Running final build smoke check...") try { - await $`./script/build.ts --single`.cwd("packages/opencode") + await $`./script/build.ts --single`.cwd("packages/cli") return true } catch (err) { console.log(`Build failed: ${err}`) @@ -200,7 +200,7 @@ async function smoke(prs: PR[], applied: number[]) { "The beta merge batch is complete, but the deterministic final smoke check failed.", `Merged PRs on HEAD:\n${done}`, "Run `bun typecheck` at the repo root.", - "Run `./script/build.ts --single` in `packages/opencode`.", + "Run `./script/build.ts --single` in `packages/cli`.", "Fix any merge-caused issues until both commands pass.", "Do not create a commit.", ].join("\n") @@ -216,25 +216,27 @@ async function smoke(prs: PR[], applied: number[]) { return commitSmokeChanges() } +async function checkout() { + console.log("Fetching latest v2 branch...") + await $`git fetch origin v2` + + console.log("Checking out beta branch...") + await $`git checkout -B beta origin/v2` + + console.log("Installing v2 dependencies...") + await $`bun install --frozen-lockfile` +} + async function main() { console.log("Fetching open PRs with beta label...") const stdout = - await $`gh pr list --state open --draft=false --label beta --json number,title,author,labels --limit 100`.text() + await $`gh pr list --state open --draft=false --base v2 --label beta --json number,title,author,labels --limit 100`.text() const prs: PR[] = JSON.parse(stdout).sort((a: PR, b: PR) => a.number - b.number) console.log(`Found ${prs.length} open PRs with beta label`) - if (prs.length === 0) { - console.log("No team PRs to merge") - return - } - - console.log("Fetching latest dev branch...") - await $`git fetch origin dev` - - console.log("Checking out beta branch...") - await $`git checkout -B beta origin/dev` + await checkout() const applied: number[] = [] const failed: FailedPR[] = [] @@ -262,7 +264,7 @@ async function main() { if (!(await fix(pr, files, prs, applied, idx))) { await cleanup() failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) - await commentOnPR(pr.number, "Merge conflicts with dev branch") + await commentOnPR(pr.number, "Merge conflicts with v2 branch") continue } } else { @@ -318,7 +320,7 @@ async function main() { await $`git fetch origin beta` const localTree = (await $`git rev-parse beta^{tree}`.text()).trim() - const remoteTrees = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n") + const remoteTrees = (await $`git log origin/v2..origin/beta --format=%T`.text()).split("\n") const matchIdx = remoteTrees.indexOf(localTree) if (matchIdx !== -1) { @@ -335,7 +337,7 @@ async function main() { await $`git fetch origin beta` const validatedTree = (await $`git rev-parse beta^{tree}`.text()).trim() - const remoteTreesAfterSmoke = (await $`git log origin/dev..origin/beta --format=%T`.text()).split("\n") + const remoteTreesAfterSmoke = (await $`git log origin/v2..origin/beta --format=%T`.text()).split("\n") const matchIdxAfterSmoke = remoteTreesAfterSmoke.indexOf(validatedTree) if (matchIdxAfterSmoke !== -1) { if (matchIdxAfterSmoke !== 0) {