Compare commits

..

5 Commits

Author SHA1 Message Date
Shoubhit Dash
069acaf821 merge dev into implement-background-agents 2026-03-05 21:37:14 +05:30
Shoubhit Dash
3bd3904902 refine tui subagent status feedback 2026-03-04 11:21:44 +05:30
Shoubhit Dash
1a705cbca5 fix task_status stale terminal reads 2026-03-04 11:21:31 +05:30
Shoubhit Dash
e6222529e7 refine background task ux and auto-resume 2026-03-04 10:48:30 +05:30
Shoubhit Dash
2453f40d88 implement background agents 2026-02-28 00:25:08 +05:30
89 changed files with 1092 additions and 1290 deletions

View File

@@ -26,7 +26,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -76,7 +76,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -110,7 +110,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -137,7 +137,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -161,7 +161,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -185,7 +185,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -218,7 +218,7 @@
},
"packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*",
@@ -248,7 +248,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -277,7 +277,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -293,7 +293,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.2.19",
"version": "1.2.17",
"bin": {
"opencode": "./bin/opencode",
},
@@ -373,7 +373,6 @@
"ulid": "catalog:",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10",
"which": "6.0.1",
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
@@ -396,7 +395,6 @@
"@types/bun": "catalog:",
"@types/mime-types": "3.0.1",
"@types/turndown": "5.0.5",
"@types/which": "3.0.4",
"@types/yargs": "17.0.33",
"@typescript/native-preview": "catalog:",
"drizzle-kit": "1.0.0-beta.12-a5629fb",
@@ -409,7 +407,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -429,7 +427,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.2.19",
"version": "1.2.17",
"devDependencies": {
"@hey-api/openapi-ts": "0.90.10",
"@tsconfig/node22": "catalog:",
@@ -440,7 +438,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -475,7 +473,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -521,7 +519,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"zod": "catalog:",
},
@@ -532,7 +530,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -2122,8 +2120,6 @@
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
"@types/which": ["@types/which@3.0.4", "", {}, "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="],
@@ -3240,7 +3236,7 @@
"isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="],
"isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
"isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="],
@@ -4590,7 +4586,7 @@
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
"which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@@ -5206,8 +5202,6 @@
"app-builder-lib/minimatch": ["minimatch@10.2.1", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A=="],
"app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@@ -5394,8 +5388,6 @@
"node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="],
"node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
"nypm/citty": ["citty@0.2.1", "", {}, "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg=="],
@@ -5926,8 +5918,6 @@
"app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"archiver-utils/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -6010,8 +6000,6 @@
"node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="],
"node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
"opencode/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],

View File

@@ -8,7 +8,6 @@ import type { Context as GitHubContext } from "@actions/github/lib/context"
import type { IssueCommentEvent, PullRequestReviewCommentEvent } from "@octokit/webhooks-types"
import { createOpencodeClient } from "@opencode-ai/sdk"
import { spawn } from "node:child_process"
import { setTimeout as sleep } from "node:timers/promises"
type GitHubAuthor = {
login: string
@@ -282,7 +281,7 @@ async function assertOpencodeConnected() {
connected = true
break
} catch (e) {}
await sleep(300)
await Bun.sleep(300)
} while (retry++ < 30)
if (!connected) {

View File

@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-pBTIT8Pgdm3272YhBjiAZsmj0SSpHTklh6lGc8YcMoE=",
"aarch64-linux": "sha256-prt039++d5UZgtldAN6+RVOR557ifIeusiy5XpzN8QU=",
"aarch64-darwin": "sha256-Y3f+cXcIGLqz6oyc5fG22t6CLD4wGkvwqO6RNXjFriQ=",
"x86_64-darwin": "sha256-BjbBBhQUgGhrlP56skABcrObvutNUZSWnrnPCg1OTKE="
"x86_64-linux": "sha256-v83hWzYVg/g4zJiBpGsQ71wTdndPk3BQVZ2mjMApUIQ=",
"aarch64-linux": "sha256-inpMwkQqwBFP2wL8w/pTOP7q3fg1aOqvE0wgzVd3/B8=",
"aarch64-darwin": "sha256-r42LGrQWqDyIy62mBSU5Nf3M22dJ3NNo7mjN/1h8d8Y=",
"x86_64-darwin": "sha256-J6XrrdK5qBK3sQBQOO/B3ZluOnsAf5f65l4q/K1nDTI="
}
}

View File

@@ -197,7 +197,6 @@ export async function createTestProject() {
await fs.writeFile(path.join(root, "README.md"), "# e2e\n")
execSync("git init", { cwd: root, stdio: "ignore" })
execSync("git config core.fsmonitor false", { cwd: root, stdio: "ignore" })
execSync("git add -A", { cwd: root, stdio: "ignore" })
execSync('git -c user.name="e2e" -c user.email="e2e@example.com" commit -m "init" --allow-empty', {
cwd: root,
@@ -208,10 +207,7 @@ export async function createTestProject() {
}
export async function cleanupTestProject(directory: string) {
try {
execSync("git fsmonitor--daemon stop", { cwd: directory, stdio: "ignore" })
} catch {}
await fs.rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }).catch(() => undefined)
await fs.rm(directory, { recursive: true, force: true }).catch(() => undefined)
}
export function sessionIDFromUrl(url: string) {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.2.19",
"version": "1.2.17",
"description": "",
"type": "module",
"exports": {

View File

@@ -22,7 +22,7 @@ import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu"
import { Dialog } from "@opencode-ai/ui/dialog"
import { getFilename } from "@opencode-ai/util/path"
@@ -1937,14 +1937,20 @@ export default function Layout(props: ParentProps) {
fallback={
<>
<div class="shrink-0 py-4 px-3">
<Button
size="large"
icon="plus-small"
class="w-full"
onClick={() => navigateWithSidebarReset(`/${base64Encode(p.worktree)}/session`)}
<TooltipKeybind
title={language.t("command.session.new")}
keybind={command.keybind("session.new")}
placement="top"
>
{language.t("command.session.new")}
</Button>
<Button
size="large"
icon="plus-small"
class="w-full"
onClick={() => navigateWithSidebarReset(`/${base64Encode(p.worktree)}/session`)}
>
{language.t("command.session.new")}
</Button>
</TooltipKeybind>
</div>
<div class="flex-1 min-h-0">
<LocalWorkspace
@@ -1959,9 +1965,15 @@ export default function Layout(props: ParentProps) {
>
<>
<div class="shrink-0 py-4 px-3">
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p)}>
{language.t("workspace.new")}
</Button>
<TooltipKeybind
title={language.t("workspace.new")}
keybind={command.keybind("workspace.new")}
placement="top"
>
<Button size="large" icon="plus-small" class="w-full" onClick={() => createWorkspace(p)}>
{language.t("workspace.new")}
</Button>
</TooltipKeybind>
</div>
<div class="relative flex-1 min-h-0">
<DragDropProvider

View File

@@ -720,7 +720,6 @@ export default function Page() {
showAllFiles,
tabForPath: file.tab,
openTab: tabs().open,
setActive: tabs().setActive,
loadFile: file.load,
})

View File

@@ -11,13 +11,12 @@ describe("createOpenReviewFile", () => {
return `file://${path}`
},
openTab: (tab) => calls.push(`open:${tab}`),
setActive: (tab) => calls.push(`active:${tab}`),
loadFile: (path) => calls.push(`load:${path}`),
})
openReviewFile("src/a.ts")
expect(calls).toEqual(["show", "load:src/a.ts", "tab:src/a.ts", "open:file://src/a.ts", "active:file://src/a.ts"])
expect(calls).toEqual(["show", "load:src/a.ts", "tab:src/a.ts", "open:file://src/a.ts"])
})
})

View File

@@ -24,20 +24,15 @@ export const createOpenReviewFile = (input: {
showAllFiles: () => void
tabForPath: (path: string) => string
openTab: (tab: string) => void
setActive: (tab: string) => void
loadFile: (path: string) => any | Promise<void>
}) => {
return (path: string) => {
batch(() => {
input.showAllFiles()
const maybePromise = input.loadFile(path)
const open = () => {
const tab = input.tabForPath(path)
input.openTab(tab)
input.setActive(tab)
}
if (maybePromise instanceof Promise) maybePromise.then(open)
else open()
const openTab = () => input.openTab(input.tabForPath(path))
if (maybePromise instanceof Promise) maybePromise.then(openTab)
else openTab()
})
}
}

View File

@@ -1,4 +1,4 @@
import { For, createEffect, createMemo, on, onCleanup, Show, Index, type JSX } from "solid-js"
import { For, createEffect, createMemo, on, onCleanup, Show, startTransition, Index, type JSX } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { useNavigate, useParams } from "@solidjs/router"
import { Button } from "@opencode-ai/ui/button"
@@ -160,7 +160,7 @@ function createTimelineStaging(input: TimelineStageInput) {
}
const currentTotal = input.messages().length
count = Math.min(currentTotal, count + input.config.batch)
setState("count", count)
startTransition(() => setState("count", count))
if (count >= currentTotal) {
setState({ completedSession: sessionKey, activeSession: "" })
frame = undefined

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.2.19",
"version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -108,26 +108,6 @@ const DOCS_SEGMENT = new Set([
"zh-tw",
])
const DOCS_LOCALE = {
ar: "ar",
da: "da",
de: "de",
en: "en",
es: "es",
fr: "fr",
it: "it",
ja: "ja",
ko: "ko",
nb: "no",
"pt-br": "br",
root: "en",
ru: "ru",
th: "th",
tr: "tr",
"zh-cn": "zh",
"zh-tw": "zht",
} as const satisfies Record<string, Locale>
function suffix(pathname: string) {
const index = pathname.search(/[?#]/)
if (index === -1) {
@@ -150,12 +130,7 @@ export function docs(locale: Locale, pathname: string) {
return `${next.path}${next.suffix}`
}
if (value === "root") {
if (next.path === "/docs/en") return `/docs${next.suffix}`
if (next.path === "/docs/en/") return `/docs/${next.suffix}`
if (next.path.startsWith("/docs/en/")) return `/docs/${next.path.slice("/docs/en/".length)}${next.suffix}`
return `${next.path}${next.suffix}`
}
if (value === "root") return `${next.path}${next.suffix}`
if (next.path === "/docs") return `/docs/${value}${next.suffix}`
if (next.path === "/docs/") return `/docs/${value}/${next.suffix}`
@@ -179,15 +154,6 @@ export function fromPathname(pathname: string) {
return parseLocale(fix(pathname).split("/")[1])
}
export function fromDocsPathname(pathname: string) {
const next = fix(pathname)
const value = next.split("/")[2]?.toLowerCase()
if (!value) return null
if (!next.startsWith("/docs/")) return null
if (!(value in DOCS_LOCALE)) return null
return DOCS_LOCALE[value as keyof typeof DOCS_LOCALE]
}
export function strip(pathname: string) {
const locale = fromPathname(pathname)
if (!locale) return fix(pathname)
@@ -306,9 +272,6 @@ export function localeFromRequest(request: Request) {
const fromPath = fromPathname(new URL(request.url).pathname)
if (fromPath) return fromPath
const fromDocsPath = fromDocsPathname(new URL(request.url).pathname)
if (fromDocsPath) return fromDocsPath
return (
localeFromCookieHeader(request.headers.get("cookie")) ??
detectFromAcceptLanguage(request.headers.get("accept-language"))

View File

@@ -1,6 +1,6 @@
import type { APIEvent } from "@solidjs/start/server"
import { Resource } from "@opencode-ai/console-resource"
import { cookie, docs, localeFromRequest, tag } from "~/lib/language"
import { docs, localeFromRequest, tag } from "~/lib/language"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
@@ -17,9 +17,7 @@ async function handler(evt: APIEvent) {
headers,
body: req.body,
})
const next = new Response(response.body, response)
next.headers.append("set-cookie", cookie(locale))
return next
return response
}
export const GET = handler

View File

@@ -1,6 +1,6 @@
import type { APIEvent } from "@solidjs/start/server"
import { Resource } from "@opencode-ai/console-resource"
import { cookie, docs, localeFromRequest, tag } from "~/lib/language"
import { docs, localeFromRequest, tag } from "~/lib/language"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
@@ -17,9 +17,7 @@ async function handler(evt: APIEvent) {
headers,
body: req.body,
})
const next = new Response(response.body, response)
next.headers.append("set-cookie", cookie(locale))
return next
return response
}
export const GET = handler

View File

@@ -1,6 +1,6 @@
import type { APIEvent } from "@solidjs/start/server"
import { Resource } from "@opencode-ai/console-resource"
import { cookie, docs, localeFromRequest, tag } from "~/lib/language"
import { docs, localeFromRequest, tag } from "~/lib/language"
async function handler(evt: APIEvent) {
const req = evt.request.clone()
@@ -17,9 +17,7 @@ async function handler(evt: APIEvent) {
headers,
body: req.body,
})
const next = new Response(response.body, response)
next.headers.append("set-cookie", cookie(locale))
return next
return response
}
export const GET = handler

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.2.19",
"version": "1.2.17",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.2.19",
"version": "1.2.17",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.2.19",
"version": "1.2.17",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop-electron",
"private": true,
"version": "1.2.19",
"version": "1.2.17",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",

View File

@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.2.19",
"version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.2.19",
"version": "1.2.17",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.2.19"
version = "1.2.17"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-darwin-arm64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-darwin-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-linux-arm64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-linux-x64.tar.gz"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.19/opencode-windows-x64.zip"
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.17/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.2.19",
"version": "1.2.17",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.2.19",
"version": "1.2.17",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -43,7 +43,6 @@
"@types/mime-types": "3.0.1",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"@types/which": "3.0.4",
"@typescript/native-preview": "catalog:",
"drizzle-kit": "1.0.0-beta.12-a5629fb",
"drizzle-orm": "1.0.0-beta.12-a5629fb",
@@ -128,7 +127,6 @@
"ulid": "catalog:",
"vscode-jsonrpc": "8.2.1",
"web-tree-sitter": "0.25.10",
"which": "6.0.1",
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",

View File

@@ -31,7 +31,6 @@ import {
import { Log } from "../util/log"
import { pathToFileURL } from "bun"
import { Filesystem } from "../util/filesystem"
import { Hash } from "../util/hash"
import { ACPSessionManager } from "./session"
import type { ACPConfig } from "./types"
import { Provider } from "../provider/provider"
@@ -282,7 +281,7 @@ export namespace ACP {
const output = this.bashOutput(part)
const content: ToolCallContent[] = []
if (output) {
const hash = Hash.fast(output)
const hash = String(Bun.hash(output))
if (part.tool === "bash") {
if (this.bashSnapshots.get(part.callID) === hash) {
await this.connection

View File

@@ -13,7 +13,6 @@ import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util/process"
import { text } from "node:stream/consumers"
import { setTimeout as sleep } from "node:timers/promises"
type PluginAuth = NonNullable<Hooks["auth"]>
@@ -48,7 +47,7 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string,
const method = plugin.auth.methods[index]
// Handle prompts for all auth types
await sleep(10)
await Bun.sleep(10)
const inputs: Record<string, string> = {}
if (method.prompts) {
for (const prompt of method.prompts) {

View File

@@ -3,7 +3,6 @@ import { bootstrap } from "../../bootstrap"
import { cmd } from "../cmd"
import { Log } from "../../../util/log"
import { EOL } from "os"
import { setTimeout as sleep } from "node:timers/promises"
export const LSPCommand = cmd({
command: "lsp",
@@ -20,7 +19,7 @@ const DiagnosticsCommand = cmd({
async handler(args) {
await bootstrap(process.cwd(), async () => {
await LSP.touchFile(args.file, true)
await sleep(1000)
await Bun.sleep(1000)
process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
})
},

View File

@@ -28,7 +28,6 @@ import { Bus } from "../../bus"
import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "@/session/prompt"
import { $ } from "bun"
import { setTimeout as sleep } from "node:timers/promises"
type GitHubAuthor = {
login: string
@@ -354,7 +353,7 @@ export const GithubInstallCommand = cmd({
}
retries++
await sleep(1000)
await Bun.sleep(1000)
} while (true)
s.stop("Installed GitHub app")
@@ -1373,7 +1372,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
} catch (e) {
if (retries > 0) {
console.log(`Retrying after ${delayMs}ms...`)
await sleep(delayMs)
await Bun.sleep(delayMs)
return withRetry(fn, retries - 1, delayMs)
}
throw e

View File

@@ -9,7 +9,6 @@ import { Filesystem } from "../../util/filesystem"
import { Process } from "../../util/process"
import { EOL } from "os"
import path from "path"
import { which } from "../../util/which"
function pagerCmd(): string[] {
const lessOptions = ["-R", "-S"]
@@ -18,7 +17,7 @@ function pagerCmd(): string[] {
}
// user could have less installed via other options
const lessOnPath = which("less")
const lessOnPath = Bun.which("less")
if (lessOnPath) {
if (Filesystem.stat(lessOnPath)?.size) return [lessOnPath, ...lessOptions]
}
@@ -28,7 +27,7 @@ function pagerCmd(): string[] {
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
}
const git = which("git")
const git = Bun.which("git")
if (git) {
const less = path.join(git, "..", "..", "usr", "bin", "less.exe")
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]

View File

@@ -129,14 +129,40 @@ export function Session() {
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
})
const messages = createMemo(() => sync.data.message[route.sessionID] ?? [])
const localPermissions = createMemo(() => sync.data.permission[route.sessionID] ?? [])
const localQuestions = createMemo(() => sync.data.question[route.sessionID] ?? [])
const childSessions = createMemo(() => {
if (session()?.parentID) return []
return children().filter((x) => x.id !== route.sessionID)
})
const permissions = createMemo(() => {
if (session()?.parentID) return []
return children().flatMap((x) => sync.data.permission[x.id] ?? [])
const child = childSessions().flatMap((x) => sync.data.permission[x.id] ?? [])
return [...localPermissions(), ...child]
})
const questions = createMemo(() => {
if (session()?.parentID) return []
return children().flatMap((x) => sync.data.question[x.id] ?? [])
const child = childSessions().flatMap((x) => sync.data.question[x.id] ?? [])
return [...localQuestions(), ...child]
})
const activeSubagents = createMemo(() =>
childSessions().flatMap((item) => {
const status = sync.data.session_status?.[item.id]
if (status?.type !== "busy" && status?.type !== "retry") return []
const count = (sync.data.message[item.id] ?? [])
.flatMap((message) => sync.data.part[message.id] ?? [])
.filter(
(part) => part.type === "tool" && (part.state.status === "completed" || part.state.status === "error"),
).length
return [
{
session: item,
status,
count,
},
]
}),
)
const pending = createMemo(() => {
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
@@ -1150,6 +1176,29 @@ export function Session() {
</For>
</scrollbox>
<box flexShrink={0}>
<Show when={activeSubagents().length > 0}>
<box paddingLeft={3} paddingBottom={1} gap={0}>
<text fg={theme.text}>
<span style={{ fg: theme.textMuted }}>Subagents</span> {activeSubagents().length} running
<span style={{ fg: theme.textMuted }}> · {keybind.print("session_child_cycle")} open</span>
</text>
<For each={activeSubagents()}>
{(item) => (
<text
fg={theme.textMuted}
onMouseUp={() => {
navigate({
type: "session",
sessionID: item.session.id,
})
}}
>
{Locale.truncate(item.session.title, 36)} · {item.count} toolcalls
</text>
)}
</For>
</box>
</Show>
<Show when={permissions().length > 0}>
<PermissionPrompt request={permissions()[0]} />
</Show>
@@ -1157,7 +1206,7 @@ export function Session() {
<QuestionPrompt request={questions()[0]} />
</Show>
<Prompt
visible={!session()?.parentID && permissions().length === 0 && questions().length === 0}
visible={!session()?.parentID && localPermissions().length === 0 && localQuestions().length === 0}
ref={(r) => {
prompt = r
promptRef.set(r)
@@ -1166,7 +1215,7 @@ export function Session() {
r.set(route.initialPrompt)
}
}}
disabled={permissions().length > 0 || questions().length > 0}
disabled={localPermissions().length > 0 || localQuestions().length > 0}
onSubmit={() => {
toBottom()
}}
@@ -1953,10 +2002,8 @@ function WebSearch(props: ToolProps<any>) {
}
function Task(props: ToolProps<typeof TaskTool>) {
const { theme } = useTheme()
const keybind = useKeybind()
const { navigate } = useRoute()
const local = useLocal()
const sync = useSync()
onMount(() => {
@@ -1974,9 +2021,47 @@ function Task(props: ToolProps<typeof TaskTool>) {
)
})
const current = createMemo(() => tools().findLast((x) => (x.state as any).title))
const isRunning = createMemo(() => props.part.state.status === "running")
const current = createMemo(() => tools().findLast((x) => x.state.status !== "pending"))
const background = createMemo(() => props.metadata.background === true)
const status = createMemo(() => {
const sessionID = props.metadata.sessionId
if (!sessionID) return
return sync.data.session_status?.[sessionID]
})
const counts = createMemo(() => {
const all = tools()
const done = all.filter((item) => item.state.status === "completed" || item.state.status === "error").length
return {
all: all.length,
done,
}
})
const childRunning = createMemo(() => status()?.type === "busy" || status()?.type === "retry")
const latest = createMemo(() => {
const user = messages().findLast((msg) => msg.role === "user")
const assistant = messages().findLast((msg) => msg.role === "assistant")
return {
user,
assistant,
}
})
const terminal = createMemo(() => {
const assistant = latest().assistant
if (!assistant) return false
const user = latest().user
if (user && user.id > assistant.id) return false
if (assistant.error) return true
return !!assistant.finish && !["tool-calls", "unknown"].includes(assistant.finish)
})
const backgroundRunning = createMemo(() => background() && childRunning())
const failed = createMemo(() => !!background() && terminal() && !!latest().assistant?.error)
const statusLabel = createMemo(() => {
if (backgroundRunning()) return "running in background"
if (!terminal()) return "background task pending sync"
if (failed()) return "background task failed"
return "background task finished"
})
const isRunning = createMemo(() => props.part.state.status === "running" || childRunning())
const duration = createMemo(() => {
const first = messages().find((x) => x.role === "user")?.time.created
@@ -1987,16 +2072,21 @@ function Task(props: ToolProps<typeof TaskTool>) {
const content = createMemo(() => {
if (!props.input.description) return ""
let content = [`Task ${props.input.description}`]
const toolLabel = `${childRunning() ? counts().done : counts().all} toolcalls`
const content = [`Task ${props.input.description}`]
if (background()) content.push(`${statusLabel()}`)
if (isRunning() && tools().length > 0) {
// content[0] += ` · ${tools().length} toolcalls`
if (current()) content.push(`${Locale.titlecase(current()!.tool)} ${(current()!.state as any).title}`)
else content.push(`${tools().length} toolcalls`)
const title = current() && (current()!.state as any).title
if (title) content.push(`${Locale.titlecase(current()!.tool)} ${title}`)
else content.push(`${toolLabel}`)
}
if (props.part.state.status === "completed") {
content.push(`${tools().length} toolcalls · ${Locale.duration(duration())}`)
content.push(`${toolLabel} · ${Locale.duration(duration())}`)
} else if (props.metadata.sessionId) {
content.push(`${keybind.print("session_child_cycle")} view subagents`)
}
return content.join("\n")

View File

@@ -6,7 +6,6 @@ import { tmpdir } from "os"
import path from "path"
import { Filesystem } from "../../../../util/filesystem"
import { Process } from "../../../../util/process"
import { which } from "../../../../util/which"
/**
* Writes text to clipboard via OSC 52 escape sequence.
@@ -77,7 +76,7 @@ export namespace Clipboard {
const getCopyMethod = lazy(() => {
const os = platform()
if (os === "darwin" && which("osascript")) {
if (os === "darwin" && Bun.which("osascript")) {
console.log("clipboard: using osascript")
return async (text: string) => {
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
@@ -86,7 +85,7 @@ export namespace Clipboard {
}
if (os === "linux") {
if (process.env["WAYLAND_DISPLAY"] && which("wl-copy")) {
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
console.log("clipboard: using wl-copy")
return async (text: string) => {
const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
@@ -96,7 +95,7 @@ export namespace Clipboard {
await proc.exited.catch(() => {})
}
}
if (which("xclip")) {
if (Bun.which("xclip")) {
console.log("clipboard: using xclip")
return async (text: string) => {
const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
@@ -110,7 +109,7 @@ export namespace Clipboard {
await proc.exited.catch(() => {})
}
}
if (which("xsel")) {
if (Bun.which("xsel")) {
console.log("clipboard: using xsel")
return async (text: string) => {
const proc = Process.spawn(["xsel", "--clipboard", "--input"], {

View File

@@ -10,7 +10,6 @@ import { GlobalBus } from "@/bus/global"
import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2"
import type { BunWebSocketData } from "hono/bun"
import { Flag } from "@/flag/flag"
import { setTimeout as sleep } from "node:timers/promises"
await Log.init({
print: process.argv.includes("--print-logs"),
@@ -76,7 +75,7 @@ const startEventStream = (directory: string) => {
).catch(() => undefined)
if (!events) {
await sleep(250)
await Bun.sleep(250)
continue
}
@@ -85,7 +84,7 @@ const startEventStream = (directory: string) => {
}
if (!signal.aborted) {
await sleep(250)
await Bun.sleep(250)
}
}
})().catch((error) => {

View File

@@ -25,12 +25,12 @@ export namespace UI {
export function println(...message: string[]) {
print(...message)
process.stderr.write(EOL)
Bun.stderr.write(EOL)
}
export function print(...message: string[]) {
blank = false
process.stderr.write(message.join(" "))
Bun.stderr.write(message.join(" "))
}
let blank = false
@@ -44,7 +44,7 @@ export namespace UI {
const result: string[] = []
const reset = "\x1b[0m"
const left = {
fg: "\x1b[90m",
fg: Bun.color("gray", "ansi") ?? "",
shadow: "\x1b[38;5;235m",
bg: "\x1b[48;5;235m",
}

View File

@@ -1240,7 +1240,7 @@ export namespace Config {
if (!parsed.data.$schema && isFile) {
parsed.data.$schema = "https://opencode.ai/config.json"
const updated = original.replace(/^\s*\{/, '{\n "$schema": "https://opencode.ai/config.json",')
await Filesystem.write(options.path, updated).catch(() => {})
await Bun.write(options.path, updated).catch(() => {})
}
const data = parsed.data
if (data.plugin && isFile) {
@@ -1401,5 +1401,3 @@ export namespace Config {
return state().then((x) => x.directories)
}
}
Filesystem.write
Filesystem.write

View File

@@ -70,7 +70,7 @@ export async function migrateTuiConfig(input: MigrateInput) {
if (extracted.keybinds !== undefined) payload.keybinds = extracted.keybinds
if (tui) Object.assign(payload, tui)
const wrote = await Filesystem.write(target, JSON.stringify(payload, null, 2))
const wrote = await Bun.write(target, JSON.stringify(payload, null, 2))
.then(() => true)
.catch((error) => {
log.warn("failed to write tui migration target", { from: file, to: target, error })
@@ -104,7 +104,7 @@ async function backupAndStripLegacy(file: string, source: string) {
const hasBackup = await Filesystem.exists(backup)
const backed = hasBackup
? true
: await Filesystem.write(backup, source)
: await Bun.write(backup, source)
.then(() => true)
.catch((error) => {
log.warn("failed to backup source config during tui migration", { path: file, backup, error })
@@ -123,7 +123,7 @@ async function backupAndStripLegacy(file: string, source: string) {
return applyEdits(acc, edits)
}, source)
return Filesystem.write(file, text)
return Bun.write(file, text)
.then(() => {
log.info("stripped tui keys from server config", { path: file, backup })
return true

View File

@@ -418,7 +418,7 @@ export namespace File {
const project = Instance.project
if (project.vcs !== "git") return []
const diffOutput = await $`git -c core.fsmonitor=false -c core.quotepath=false diff --numstat HEAD`
const diffOutput = await $`git -c core.quotepath=false diff --numstat HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
@@ -439,12 +439,11 @@ export namespace File {
}
}
const untrackedOutput =
await $`git -c core.fsmonitor=false -c core.quotepath=false ls-files --others --exclude-standard`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const untrackedOutput = await $`git -c core.quotepath=false ls-files --others --exclude-standard`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
if (untrackedOutput.trim()) {
const untrackedFiles = untrackedOutput.trim().split("\n")
@@ -465,12 +464,11 @@ export namespace File {
}
// Get deleted files
const deletedOutput =
await $`git -c core.fsmonitor=false -c core.quotepath=false diff --name-only --diff-filter=D HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
const deletedOutput = await $`git -c core.quotepath=false diff --name-only --diff-filter=D HEAD`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
if (deletedOutput.trim()) {
const deletedFiles = deletedOutput.trim().split("\n")
@@ -541,14 +539,8 @@ export namespace File {
const content = (await Filesystem.readText(full).catch(() => "")).trim()
if (project.vcs === "git") {
let diff = await $`git -c core.fsmonitor=false diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (!diff.trim()) {
diff = await $`git -c core.fsmonitor=false diff --staged ${file}`
.cwd(Instance.directory)
.quiet()
.nothrow()
.text()
}
let diff = await $`git diff ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (!diff.trim()) diff = await $`git diff --staged ${file}`.cwd(Instance.directory).quiet().nothrow().text()
if (diff.trim()) {
const original = await $`git show HEAD:${file}`.cwd(Instance.directory).quiet().nothrow().text()
const patch = structuredPatch(file, file, original, content, "old", "new", {

View File

@@ -8,7 +8,6 @@ import { lazy } from "../util/lazy"
import { $ } from "bun"
import { Filesystem } from "../util/filesystem"
import { Process } from "../util/process"
import { which } from "../util/which"
import { text } from "node:stream/consumers"
import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
@@ -127,7 +126,7 @@ export namespace Ripgrep {
)
const state = lazy(async () => {
const system = which("rg")
const system = Bun.which("rg")
if (system) {
const stat = await fs.stat(system).catch(() => undefined)
if (stat?.isFile()) return { filepath: system }

View File

@@ -3,7 +3,6 @@ import { BunProc } from "../bun"
import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem"
import { Process } from "../util/process"
import { which } from "../util/which"
import { Flag } from "@/flag/flag"
export interface Info {
@@ -19,7 +18,7 @@ export const gofmt: Info = {
command: ["gofmt", "-w", "$FILE"],
extensions: [".go"],
async enabled() {
return which("gofmt") !== null
return Bun.which("gofmt") !== null
},
}
@@ -28,7 +27,7 @@ export const mix: Info = {
command: ["mix", "format", "$FILE"],
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
async enabled() {
return which("mix") !== null
return Bun.which("mix") !== null
},
}
@@ -153,7 +152,7 @@ export const zig: Info = {
command: ["zig", "fmt", "$FILE"],
extensions: [".zig", ".zon"],
async enabled() {
return which("zig") !== null
return Bun.which("zig") !== null
},
}
@@ -172,7 +171,7 @@ export const ktlint: Info = {
command: ["ktlint", "-F", "$FILE"],
extensions: [".kt", ".kts"],
async enabled() {
return which("ktlint") !== null
return Bun.which("ktlint") !== null
},
}
@@ -181,7 +180,7 @@ export const ruff: Info = {
command: ["ruff", "format", "$FILE"],
extensions: [".py", ".pyi"],
async enabled() {
if (!which("ruff")) return false
if (!Bun.which("ruff")) return false
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
for (const config of configs) {
const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
@@ -211,7 +210,7 @@ export const rlang: Info = {
command: ["air", "format", "$FILE"],
extensions: [".R"],
async enabled() {
const airPath = which("air")
const airPath = Bun.which("air")
if (airPath == null) return false
try {
@@ -240,7 +239,7 @@ export const uvformat: Info = {
extensions: [".py", ".pyi"],
async enabled() {
if (await ruff.enabled()) return false
if (which("uv") !== null) {
if (Bun.which("uv") !== null) {
const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
const code = await proc.exited
return code === 0
@@ -254,7 +253,7 @@ export const rubocop: Info = {
command: ["rubocop", "--autocorrect", "$FILE"],
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async enabled() {
return which("rubocop") !== null
return Bun.which("rubocop") !== null
},
}
@@ -263,7 +262,7 @@ export const standardrb: Info = {
command: ["standardrb", "--fix", "$FILE"],
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async enabled() {
return which("standardrb") !== null
return Bun.which("standardrb") !== null
},
}
@@ -272,7 +271,7 @@ export const htmlbeautifier: Info = {
command: ["htmlbeautifier", "$FILE"],
extensions: [".erb", ".html.erb"],
async enabled() {
return which("htmlbeautifier") !== null
return Bun.which("htmlbeautifier") !== null
},
}
@@ -281,7 +280,7 @@ export const dart: Info = {
command: ["dart", "format", "$FILE"],
extensions: [".dart"],
async enabled() {
return which("dart") !== null
return Bun.which("dart") !== null
},
}
@@ -290,7 +289,7 @@ export const ocamlformat: Info = {
command: ["ocamlformat", "-i", "$FILE"],
extensions: [".ml", ".mli"],
async enabled() {
if (!which("ocamlformat")) return false
if (!Bun.which("ocamlformat")) return false
const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
return items.length > 0
},
@@ -301,7 +300,7 @@ export const terraform: Info = {
command: ["terraform", "fmt", "$FILE"],
extensions: [".tf", ".tfvars"],
async enabled() {
return which("terraform") !== null
return Bun.which("terraform") !== null
},
}
@@ -310,7 +309,7 @@ export const latexindent: Info = {
command: ["latexindent", "-w", "-s", "$FILE"],
extensions: [".tex"],
async enabled() {
return which("latexindent") !== null
return Bun.which("latexindent") !== null
},
}
@@ -319,7 +318,7 @@ export const gleam: Info = {
command: ["gleam", "format", "$FILE"],
extensions: [".gleam"],
async enabled() {
return which("gleam") !== null
return Bun.which("gleam") !== null
},
}
@@ -328,7 +327,7 @@ export const shfmt: Info = {
command: ["shfmt", "-w", "$FILE"],
extensions: [".sh", ".bash"],
async enabled() {
return which("shfmt") !== null
return Bun.which("shfmt") !== null
},
}
@@ -337,7 +336,7 @@ export const nixfmt: Info = {
command: ["nixfmt", "$FILE"],
extensions: [".nix"],
async enabled() {
return which("nixfmt") !== null
return Bun.which("nixfmt") !== null
},
}
@@ -346,7 +345,7 @@ export const rustfmt: Info = {
command: ["rustfmt", "$FILE"],
extensions: [".rs"],
async enabled() {
return which("rustfmt") !== null
return Bun.which("rustfmt") !== null
},
}
@@ -373,7 +372,7 @@ export const ormolu: Info = {
command: ["ormolu", "-i", "$FILE"],
extensions: [".hs"],
async enabled() {
return which("ormolu") !== null
return Bun.which("ormolu") !== null
},
}
@@ -382,7 +381,7 @@ export const cljfmt: Info = {
command: ["cljfmt", "fix", "--quiet", "$FILE"],
extensions: [".clj", ".cljs", ".cljc", ".edn"],
async enabled() {
return which("cljfmt") !== null
return Bun.which("cljfmt") !== null
},
}
@@ -391,6 +390,6 @@ export const dfmt: Info = {
command: ["dfmt", "-i", "$FILE"],
extensions: [".d"],
async enabled() {
return which("dfmt") !== null
return Bun.which("dfmt") !== null
},
}

View File

@@ -12,7 +12,6 @@ import { Instance } from "../project/instance"
import { Flag } from "../flag/flag"
import { Archive } from "../util/archive"
import { Process } from "../util/process"
import { which } from "../util/which"
export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
@@ -76,7 +75,7 @@ export namespace LSPServer {
},
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
async spawn(root) {
const deno = which("deno")
const deno = Bun.which("deno")
if (!deno) {
log.info("deno not found, please install deno first")
return
@@ -123,7 +122,7 @@ export namespace LSPServer {
extensions: [".vue"],
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
async spawn(root) {
let binary = which("vue-language-server")
let binary = Bun.which("vue-language-server")
const args: string[] = []
if (!binary) {
const js = path.join(
@@ -261,7 +260,7 @@ export namespace LSPServer {
let lintBin = await resolveBin(lintTarget)
if (!lintBin) {
const found = which("oxlint")
const found = Bun.which("oxlint")
if (found) lintBin = found
}
@@ -282,7 +281,7 @@ export namespace LSPServer {
let serverBin = await resolveBin(serverTarget)
if (!serverBin) {
const found = which("oxc_language_server")
const found = Bun.which("oxc_language_server")
if (found) serverBin = found
}
if (serverBin) {
@@ -333,7 +332,7 @@ export namespace LSPServer {
let bin: string | undefined
if (await Filesystem.exists(localBin)) bin = localBin
if (!bin) {
const found = which("biome")
const found = Bun.which("biome")
if (found) bin = found
}
@@ -369,11 +368,11 @@ export namespace LSPServer {
},
extensions: [".go"],
async spawn(root) {
let bin = which("gopls", {
let bin = Bun.which("gopls", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
if (!which("go")) return
if (!Bun.which("go")) return
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing gopls")
@@ -406,12 +405,12 @@ export namespace LSPServer {
root: NearestRoot(["Gemfile"]),
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn(root) {
let bin = which("rubocop", {
let bin = Bun.which("rubocop", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
const ruby = which("ruby")
const gem = which("gem")
const ruby = Bun.which("ruby")
const gem = Bun.which("gem")
if (!ruby || !gem) {
log.info("Ruby not found, please install Ruby first")
return
@@ -458,7 +457,7 @@ export namespace LSPServer {
return undefined
}
let binary = which("ty")
let binary = Bun.which("ty")
const initialization: Record<string, string> = {}
@@ -510,7 +509,7 @@ export namespace LSPServer {
extensions: [".py", ".pyi"],
root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
async spawn(root) {
let binary = which("pyright-langserver")
let binary = Bun.which("pyright-langserver")
const args = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
@@ -564,7 +563,7 @@ export namespace LSPServer {
extensions: [".ex", ".exs"],
root: NearestRoot(["mix.exs", "mix.lock"]),
async spawn(root) {
let binary = which("elixir-ls")
let binary = Bun.which("elixir-ls")
if (!binary) {
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
binary = path.join(
@@ -575,7 +574,7 @@ export namespace LSPServer {
)
if (!(await Filesystem.exists(binary))) {
const elixir = which("elixir")
const elixir = Bun.which("elixir")
if (!elixir) {
log.error("elixir is required to run elixir-ls")
return
@@ -626,12 +625,12 @@ export namespace LSPServer {
extensions: [".zig", ".zon"],
root: NearestRoot(["build.zig"]),
async spawn(root) {
let bin = which("zls", {
let bin = Bun.which("zls", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
const zig = which("zig")
const zig = Bun.which("zig")
if (!zig) {
log.error("Zig is required to use zls. Please install Zig first.")
return
@@ -738,11 +737,11 @@ export namespace LSPServer {
root: NearestRoot([".slnx", ".sln", ".csproj", "global.json"]),
extensions: [".cs"],
async spawn(root) {
let bin = which("csharp-ls", {
let bin = Bun.which("csharp-ls", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
if (!which("dotnet")) {
if (!Bun.which("dotnet")) {
log.error(".NET SDK is required to install csharp-ls")
return
}
@@ -777,11 +776,11 @@ export namespace LSPServer {
root: NearestRoot([".slnx", ".sln", ".fsproj", "global.json"]),
extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
async spawn(root) {
let bin = which("fsautocomplete", {
let bin = Bun.which("fsautocomplete", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
if (!bin) {
if (!which("dotnet")) {
if (!Bun.which("dotnet")) {
log.error(".NET SDK is required to install fsautocomplete")
return
}
@@ -818,7 +817,7 @@ export namespace LSPServer {
async spawn(root) {
// Check if sourcekit-lsp is available in the PATH
// This is installed with the Swift toolchain
const sourcekit = which("sourcekit-lsp")
const sourcekit = Bun.which("sourcekit-lsp")
if (sourcekit) {
return {
process: spawn(sourcekit, {
@@ -829,7 +828,7 @@ export namespace LSPServer {
// If sourcekit-lsp not found, check if xcrun is available
// This is specific to macOS where sourcekit-lsp is typically installed with Xcode
if (!which("xcrun")) return
if (!Bun.which("xcrun")) return
const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
@@ -878,7 +877,7 @@ export namespace LSPServer {
},
extensions: [".rs"],
async spawn(root) {
const bin = which("rust-analyzer")
const bin = Bun.which("rust-analyzer")
if (!bin) {
log.info("rust-analyzer not found in path, please install it")
return
@@ -897,7 +896,7 @@ export namespace LSPServer {
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
async spawn(root) {
const args = ["--background-index", "--clang-tidy"]
const fromPath = which("clangd")
const fromPath = Bun.which("clangd")
if (fromPath) {
return {
process: spawn(fromPath, args, {
@@ -1042,7 +1041,7 @@ export namespace LSPServer {
extensions: [".svelte"],
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
async spawn(root) {
let binary = which("svelteserver")
let binary = Bun.which("svelteserver")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
@@ -1089,7 +1088,7 @@ export namespace LSPServer {
}
const tsdk = path.dirname(tsserver)
let binary = which("astro-ls")
let binary = Bun.which("astro-ls")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
@@ -1133,7 +1132,7 @@ export namespace LSPServer {
root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
extensions: [".java"],
async spawn(root) {
const java = which("java")
const java = Bun.which("java")
if (!java) {
log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
return
@@ -1325,7 +1324,7 @@ export namespace LSPServer {
extensions: [".yaml", ".yml"],
root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
async spawn(root) {
let binary = which("yaml-language-server")
let binary = Bun.which("yaml-language-server")
const args: string[] = []
if (!binary) {
const js = path.join(
@@ -1381,7 +1380,7 @@ export namespace LSPServer {
]),
extensions: [".lua"],
async spawn(root) {
let bin = which("lua-language-server", {
let bin = Bun.which("lua-language-server", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
@@ -1513,7 +1512,7 @@ export namespace LSPServer {
extensions: [".php"],
root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
async spawn(root) {
let binary = which("intelephense")
let binary = Bun.which("intelephense")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
@@ -1557,7 +1556,7 @@ export namespace LSPServer {
extensions: [".prisma"],
root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
async spawn(root) {
const prisma = which("prisma")
const prisma = Bun.which("prisma")
if (!prisma) {
log.info("prisma not found, please install prisma")
return
@@ -1575,7 +1574,7 @@ export namespace LSPServer {
extensions: [".dart"],
root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
async spawn(root) {
const dart = which("dart")
const dart = Bun.which("dart")
if (!dart) {
log.info("dart not found, please install dart first")
return
@@ -1593,7 +1592,7 @@ export namespace LSPServer {
extensions: [".ml", ".mli"],
root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
async spawn(root) {
const bin = which("ocamllsp")
const bin = Bun.which("ocamllsp")
if (!bin) {
log.info("ocamllsp not found, please install ocaml-lsp-server")
return
@@ -1610,7 +1609,7 @@ export namespace LSPServer {
extensions: [".sh", ".bash", ".zsh", ".ksh"],
root: async () => Instance.directory,
async spawn(root) {
let binary = which("bash-language-server")
let binary = Bun.which("bash-language-server")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
@@ -1649,7 +1648,7 @@ export namespace LSPServer {
extensions: [".tf", ".tfvars"],
root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
async spawn(root) {
let bin = which("terraform-ls", {
let bin = Bun.which("terraform-ls", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
@@ -1732,7 +1731,7 @@ export namespace LSPServer {
extensions: [".tex", ".bib"],
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
async spawn(root) {
let bin = which("texlab", {
let bin = Bun.which("texlab", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
@@ -1822,7 +1821,7 @@ export namespace LSPServer {
extensions: [".dockerfile", "Dockerfile"],
root: async () => Instance.directory,
async spawn(root) {
let binary = which("docker-langserver")
let binary = Bun.which("docker-langserver")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
@@ -1861,7 +1860,7 @@ export namespace LSPServer {
extensions: [".gleam"],
root: NearestRoot(["gleam.toml"]),
async spawn(root) {
const gleam = which("gleam")
const gleam = Bun.which("gleam")
if (!gleam) {
log.info("gleam not found, please install gleam first")
return
@@ -1879,9 +1878,9 @@ export namespace LSPServer {
extensions: [".clj", ".cljs", ".cljc", ".edn"],
root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
async spawn(root) {
let bin = which("clojure-lsp")
let bin = Bun.which("clojure-lsp")
if (!bin && process.platform === "win32") {
bin = which("clojure-lsp.exe")
bin = Bun.which("clojure-lsp.exe")
}
if (!bin) {
log.info("clojure-lsp not found, please install clojure-lsp first")
@@ -1910,7 +1909,7 @@ export namespace LSPServer {
return Instance.directory
},
async spawn(root) {
const nixd = which("nixd")
const nixd = Bun.which("nixd")
if (!nixd) {
log.info("nixd not found, please install nixd first")
return
@@ -1931,7 +1930,7 @@ export namespace LSPServer {
extensions: [".typ", ".typc"],
root: NearestRoot(["typst.toml"]),
async spawn(root) {
let bin = which("tinymist", {
let bin = Bun.which("tinymist", {
PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
})
@@ -2025,7 +2024,7 @@ export namespace LSPServer {
extensions: [".hs", ".lhs"],
root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
async spawn(root) {
const bin = which("haskell-language-server-wrapper")
const bin = Bun.which("haskell-language-server-wrapper")
if (!bin) {
log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
return
@@ -2043,7 +2042,7 @@ export namespace LSPServer {
extensions: [".jl"],
root: NearestRoot(["Project.toml", "Manifest.toml", "*.jl"]),
async spawn(root) {
const julia = which("julia")
const julia = Bun.which("julia")
if (!julia) {
log.info("julia not found, please install julia first (https://julialang.org/downloads/)")
return

View File

@@ -1,4 +1,3 @@
import { createConnection } from "net"
import { Log } from "../util/log"
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
@@ -161,12 +160,21 @@ export namespace McpOAuthCallback {
export async function isPortInUse(): Promise<boolean> {
return new Promise((resolve) => {
const socket = createConnection(OAUTH_CALLBACK_PORT, "127.0.0.1")
socket.on("connect", () => {
socket.destroy()
resolve(true)
})
socket.on("error", () => {
Bun.connect({
hostname: "127.0.0.1",
port: OAUTH_CALLBACK_PORT,
socket: {
open(socket) {
socket.end()
resolve(true)
},
error() {
resolve(false)
},
data() {},
close() {},
},
}).catch(() => {
resolve(false)
})
})

View File

@@ -4,7 +4,6 @@ import { Installation } from "../installation"
import { Auth, OAUTH_DUMMY_KEY } from "../auth"
import os from "os"
import { ProviderTransform } from "@/provider/transform"
import { setTimeout as sleep } from "node:timers/promises"
const log = Log.create({ service: "plugin.codex" })
@@ -362,7 +361,6 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
"gpt-5.1-codex-max",
"gpt-5.1-codex-mini",
"gpt-5.2",
"gpt-5.4",
"gpt-5.2-codex",
"gpt-5.3-codex",
"gpt-5.1-codex",
@@ -604,7 +602,7 @@ export async function CodexAuthPlugin(input: PluginInput): Promise<Hooks> {
return { type: "failed" as const }
}
await sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS)
await Bun.sleep(interval + OAUTH_POLLING_SAFETY_MARGIN_MS)
}
},
}

View File

@@ -1,7 +1,6 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { Installation } from "@/installation"
import { iife } from "@/util/iife"
import { setTimeout as sleep } from "node:timers/promises"
const CLIENT_ID = "Ov23li8tweQw6odWQebz"
// Add a small safety buffer when polling to avoid hitting the server
@@ -271,7 +270,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
}
if (data.error === "authorization_pending") {
await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
continue
}
@@ -287,13 +286,13 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
newInterval = serverInterval * 1000
}
await sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS)
await Bun.sleep(newInterval + OAUTH_POLLING_SAFETY_MARGIN_MS)
continue
}
if (data.error) return { type: "failed" as const }
await sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
await Bun.sleep(deviceData.interval * 1000 + OAUTH_POLLING_SAFETY_MARGIN_MS)
continue
}
},

View File

@@ -14,7 +14,6 @@ import { GlobalBus } from "@/bus/global"
import { existsSync } from "fs"
import { git } from "../util/git"
import { Glob } from "../util/glob"
import { which } from "../util/which"
export namespace Project {
const log = Log.create({ service: "project" })
@@ -98,7 +97,7 @@ export namespace Project {
if (dotgit) {
let sandbox = path.dirname(dotgit)
const gitBinary = which("git")
const gitBinary = Bun.which("git")
// cached id calculation
let id = await Filesystem.readText(path.join(dotgit, "opencode"))

View File

@@ -6,10 +6,9 @@ import { mapValues, mergeDeep, omit, pickBy, sortBy } from "remeda"
import { NoSuchModelError, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import { BunProc } from "../bun"
import { Hash } from "../util/hash"
import { Plugin } from "../plugin"
import { NamedError } from "@opencode-ai/util/error"
import { ModelsDev } from "./models"
import { NamedError } from "@opencode-ai/util/error"
import { Auth } from "../auth"
import { Env } from "../env"
import { Instance } from "../project/instance"
@@ -796,7 +795,7 @@ export namespace Provider {
const modelLoaders: {
[providerID: string]: CustomModelLoader
} = {}
const sdk = new Map<string, SDK>()
const sdk = new Map<number, SDK>()
log.info("init")
@@ -1086,7 +1085,7 @@ export namespace Provider {
...model.headers,
}
const key = Hash.fast(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options }))
const key = Bun.hash.xxHash32(JSON.stringify({ providerID: model.providerID, npm: model.api.npm, options }))
const existing = s.sdk.get(key)
if (existing) return existing
@@ -1231,42 +1230,6 @@ export namespace Provider {
}
}
async function pick(providerID: string, query: string[]) {
const provider = await state().then((state) => state.providers[providerID])
if (!provider) return
const models = Object.keys(provider.models)
for (const item of query) {
if (providerID === "amazon-bedrock") {
const prefixes = ["global.", "us.", "eu."]
const candidates = models.filter((model) => model.toLowerCase().includes(item.toLowerCase()))
// Model selection priority:
// 1. global. prefix (works everywhere)
// 2. User's region prefix (us., eu.)
// 3. Unprefixed model
const best = candidates.find((model) => model.startsWith("global."))
if (best) return getModel(providerID, best)
const region = provider.options?.region
if (region) {
const prefix = region.split("-")[0]
if (prefix === "us" || prefix === "eu") {
const hit = candidates.find((model) => model.startsWith(`${prefix}.`))
if (hit) return getModel(providerID, hit)
}
}
const bare = candidates.find((model) => !prefixes.some((prefix) => model.startsWith(prefix)))
if (bare) return getModel(providerID, bare)
continue
}
const hit = models.find((model) => model.toLowerCase().includes(item.toLowerCase()))
if (hit) return getModel(providerID, hit)
}
}
export async function getSmallModel(providerID: string) {
const cfg = await Config.get()
@@ -1275,25 +1238,54 @@ export namespace Provider {
return getModel(parsed.providerID, parsed.modelID)
}
let query = [
"claude-haiku-4-5",
"claude-haiku-4.5",
"3-5-haiku",
"3.5-haiku",
"gemini-3-flash",
"gemini-2.5-flash",
"gpt-5-nano",
]
if (providerID.startsWith("opencode")) {
query = ["gpt-5-nano"]
}
if (providerID.startsWith("github-copilot")) {
// prioritize free models for github copilot
query = ["gpt-5-mini", "claude-haiku-4.5", ...query]
}
const provider = await state().then((state) => state.providers[providerID])
if (provider) {
let priority = [
"claude-haiku-4-5",
"claude-haiku-4.5",
"3-5-haiku",
"3.5-haiku",
"gemini-3-flash",
"gemini-2.5-flash",
"gpt-5-nano",
]
if (providerID.startsWith("opencode")) {
priority = ["gpt-5-nano"]
}
if (providerID.startsWith("github-copilot")) {
// prioritize free models for github copilot
priority = ["gpt-5-mini", "claude-haiku-4.5", ...priority]
}
for (const item of priority) {
if (providerID === "amazon-bedrock") {
const crossRegionPrefixes = ["global.", "us.", "eu."]
const candidates = Object.keys(provider.models).filter((m) => m.includes(item))
const model = await pick(providerID, query)
if (model) return model
// Model selection priority:
// 1. global. prefix (works everywhere)
// 2. User's region prefix (us., eu.)
// 3. Unprefixed model
const globalMatch = candidates.find((m) => m.startsWith("global."))
if (globalMatch) return getModel(providerID, globalMatch)
const region = provider.options?.region
if (region) {
const regionPrefix = region.split("-")[0]
if (regionPrefix === "us" || regionPrefix === "eu") {
const regionalMatch = candidates.find((m) => m.startsWith(`${regionPrefix}.`))
if (regionalMatch) return getModel(providerID, regionalMatch)
}
}
const unprefixed = candidates.find((m) => !crossRegionPrefixes.some((p) => m.startsWith(p)))
if (unprefixed) return getModel(providerID, unprefixed)
} else {
for (const model of Object.keys(provider.models)) {
if (model.includes(item)) return getModel(providerID, model)
}
}
}
}
// Check if opencode provider is available before using it
const opencodeProvider = await state().then((state) => state.providers["opencode"])
@@ -1304,22 +1296,6 @@ export namespace Provider {
return undefined
}
export async function getExploreModel(providerID: string) {
const model = await pick(providerID, [
"gpt-5.3-codex-spark",
"claude-haiku-4-5",
"claude-haiku-4.5",
"gemini-3-flash",
"minimax-m2.5",
"minimax-m2-5",
"glm-5",
"kimi-k2.5",
"kimi-k2-5",
])
if (model) return model
return undefined
}
const priority = ["gpt-5", "claude-sonnet-4", "big-pickle", "gemini-3-pro"]
export function sort(models: Model[]) {
return sortBy(

View File

@@ -1,10 +1,8 @@
import { Flag } from "@/flag/flag"
import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util/filesystem"
import { which } from "@/util/which"
import path from "path"
import { spawn, type ChildProcess } from "child_process"
import { setTimeout as sleep } from "node:timers/promises"
const SIGKILL_TIMEOUT_MS = 200
@@ -24,13 +22,13 @@ export namespace Shell {
try {
process.kill(-pid, "SIGTERM")
await sleep(SIGKILL_TIMEOUT_MS)
await Bun.sleep(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
process.kill(-pid, "SIGKILL")
}
} catch (_e) {
proc.kill("SIGTERM")
await sleep(SIGKILL_TIMEOUT_MS)
await Bun.sleep(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
proc.kill("SIGKILL")
}
@@ -41,7 +39,7 @@ export namespace Shell {
function fallback() {
if (process.platform === "win32") {
if (Flag.OPENCODE_GIT_BASH_PATH) return Flag.OPENCODE_GIT_BASH_PATH
const git = which("git")
const git = Bun.which("git")
if (git) {
// git.exe is typically at: C:\Program Files\Git\cmd\git.exe
// bash.exe is at: C:\Program Files\Git\bin\bash.exe
@@ -51,7 +49,7 @@ export namespace Shell {
return process.env.COMSPEC || "cmd.exe"
}
if (process.platform === "darwin") return "/bin/zsh"
const bash = which("bash")
const bash = Bun.which("bash")
if (bash) return bash
return "/bin/sh"
}

View File

@@ -1,7 +1,6 @@
import { $ } from "bun"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../util/filesystem"
import { Log } from "../util/log"
import { Flag } from "../flag/flag"
import { Global } from "../global"
@@ -272,12 +271,13 @@ export namespace Snapshot {
const target = path.join(git, "info", "exclude")
await fs.mkdir(path.join(git, "info"), { recursive: true })
if (!file) {
await Filesystem.write(target, "")
await Bun.write(target, "")
return
}
const text = await Filesystem.readText(file).catch(() => "")
await Filesystem.write(target, text)
const text = await Bun.file(file)
.text()
.catch(() => "")
await Bun.write(target, text)
}
async function excludes() {

View File

@@ -7,6 +7,7 @@ import { GrepTool } from "./grep"
import { BatchTool } from "./batch"
import { ReadTool } from "./read"
import { TaskTool } from "./task"
import { TaskStatusTool } from "./task_status"
import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
@@ -110,6 +111,7 @@ export namespace ToolRegistry {
EditTool,
WriteTool,
TaskTool,
TaskStatusTool,
WebFetchTool,
TodoWriteTool,
// TodoReadTool,

View File

@@ -5,8 +5,8 @@ import { Session } from "../session"
import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Agent } from "../agent/agent"
import { Provider } from "../provider/provider"
import { SessionPrompt } from "../session/prompt"
import { SessionStatus } from "../session/status"
import { iife } from "@/util/iife"
import { defer } from "@/util/defer"
import { Config } from "../config/config"
@@ -23,8 +23,93 @@ const parameters = z.object({
)
.optional(),
command: z.string().describe("The command that triggered this task").optional(),
background: z
.boolean()
.optional()
.describe("When true, launch the subagent in the background and return immediately"),
})
function output(sessionID: string, text: string) {
return [
`task_id: ${sessionID} (for resuming to continue this task if needed)`,
"",
"<task_result>",
text,
"</task_result>",
].join("\n")
}
function backgroundOutput(sessionID: string) {
return [
`task_id: ${sessionID} (for polling this task with task_status)`,
"state: running",
"",
"<task_result>",
"Background task started. Continue your current work and call task_status when you need the result.",
"</task_result>",
].join("\n")
}
function backgroundMessage(input: {
sessionID: string
description: string
state: "completed" | "error"
text: string
}) {
const tag = input.state === "completed" ? "task_result" : "task_error"
const title =
input.state === "completed"
? `Background task completed: ${input.description}`
: `Background task failed: ${input.description}`
return [title, `task_id: ${input.sessionID}`, `state: ${input.state}`, `<${tag}>`, input.text, `</${tag}>`].join("\n")
}
function errorText(error: unknown) {
if (error instanceof Error) return error.message
return String(error)
}
function resultTaskID(input: unknown) {
if (!input || typeof input !== "object") return
const taskID = Reflect.get(input, "task_id")
if (typeof taskID === "string") return taskID
}
function polled(input: { message: MessageV2.WithParts; taskID: string }) {
if (input.message.info.role !== "assistant") return false
return input.message.parts.some((part) => {
if (part.type !== "tool") return false
if (part.tool !== "task_status") return false
if (part.state.status !== "completed") return false
return resultTaskID(part.state.input) === input.taskID
})
}
async function latestUser(sessionID: string) {
const [message] = await Session.messages({
sessionID,
limit: 1,
})
if (!message) return
if (message.info.role !== "user") return
return message.info.id
}
async function continueParent(input: { parentID: string; userID: string; taskID: string }) {
const message =
SessionStatus.get(input.parentID).type === "idle"
? undefined
: await SessionPrompt.loop({
sessionID: input.parentID,
}).catch(() => undefined)
if (message && polled({ message, taskID: input.taskID })) return
if (SessionStatus.get(input.parentID).type !== "idle") return
if ((await latestUser(input.parentID)) !== input.userID) return
await SessionPrompt.loop({
sessionID: input.parentID,
})
}
export const TaskTool = Tool.define("task", async (ctx) => {
const agents = await Agent.list().then((x) => x.filter((a) => a.mode !== "primary"))
@@ -103,82 +188,111 @@ export const TaskTool = Tool.define("task", async (ctx) => {
})
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
const info = msg.info
const model = await iife(async () => {
if (agent.model) return agent.model
if (agent.name !== "explore") {
return {
modelID: info.modelID,
providerID: info.providerID,
}
}
const pick = await Provider.getExploreModel(info.providerID)
if (pick) {
return {
modelID: pick.id,
providerID: pick.providerID,
}
}
return {
modelID: info.modelID,
providerID: info.providerID,
}
})
const parentModel = {
modelID: msg.info.modelID,
providerID: msg.info.providerID,
}
const model = agent.model ?? parentModel
const background = params.background === true
const metadata = {
sessionId: session.id,
model,
...(background ? { background: true } : {}),
}
ctx.metadata({
title: params.description,
metadata: {
sessionId: session.id,
model,
},
metadata,
})
const messageID = Identifier.ascending("message")
const run = async () => {
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
const result = await SessionPrompt.prompt({
messageID: Identifier.ascending("message"),
sessionID: session.id,
model: {
modelID: model.modelID,
providerID: model.providerID,
},
agent: agent.name,
tools: {
todowrite: false,
todoread: false,
...(hasTaskPermission ? {} : { task: false }),
...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])),
},
parts: promptParts,
})
return result.parts.findLast((x) => x.type === "text")?.text ?? ""
}
if (background) {
const inject = (state: "completed" | "error", text: string) =>
SessionPrompt.prompt({
sessionID: ctx.sessionID,
noReply: true,
model: {
modelID: parentModel.modelID,
providerID: parentModel.providerID,
},
agent: ctx.agent,
parts: [
{
type: "text",
synthetic: true,
text: backgroundMessage({
sessionID: session.id,
description: params.description,
state,
text,
}),
},
],
})
void run()
.then((text) =>
inject("completed", text)
.then((message) =>
continueParent({
parentID: ctx.sessionID,
userID: message.info.id,
taskID: session.id,
}),
)
.catch(() => {}),
)
.catch((error) =>
inject("error", errorText(error))
.then((message) =>
continueParent({
parentID: ctx.sessionID,
userID: message.info.id,
taskID: session.id,
}),
)
.catch(() => {}),
)
return {
title: params.description,
metadata,
output: backgroundOutput(session.id),
}
}
function cancel() {
SessionPrompt.cancel(session.id)
}
ctx.abort.addEventListener("abort", cancel)
using _ = defer(() => ctx.abort.removeEventListener("abort", cancel))
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
const result = await SessionPrompt.prompt({
messageID,
sessionID: session.id,
model: {
modelID: model.modelID,
providerID: model.providerID,
},
agent: agent.name,
tools: {
todowrite: false,
todoread: false,
...(hasTaskPermission ? {} : { task: false }),
...Object.fromEntries((config.experimental?.primary_tools ?? []).map((t) => [t, false])),
},
parts: promptParts,
})
const text = result.parts.findLast((x) => x.type === "text")?.text ?? ""
const output = [
`task_id: ${session.id} (for resuming to continue this task if needed)`,
"",
"<task_result>",
text,
"</task_result>",
].join("\n")
const text = await run()
return {
title: params.description,
metadata: {
sessionId: session.id,
model,
},
output,
metadata,
output: output(session.id, text),
}
},
}

View File

@@ -17,11 +17,13 @@ When NOT to use the Task tool:
Usage notes:
1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. The output includes a task_id you can reuse later to continue the same subagent session.
3. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
4. The agent's outputs should generally be trusted
5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands).
6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
2. By default, task waits for completion and returns the result immediately, along with a task_id you can reuse later to continue the same subagent session.
3. Set background=true to launch asynchronously. In background mode, continue your current work without waiting.
4. For background runs, use task_status(task_id=..., wait=false) to poll, or wait=true to block until done (optionally with timeout_ms).
5. Each agent invocation starts with a fresh context unless you provide task_id to resume the same subagent session (which continues with its previous messages and tool outputs). When starting fresh, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
6. The agent's outputs should generally be trusted
7. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent. Tell it how to verify its work if possible (e.g., relevant test commands).
8. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement.
Example usage (NOTE: The agents below are fictional examples for illustration only - use the actual agents listed above):

View File

@@ -0,0 +1,163 @@
import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./task_status.txt"
import { Identifier } from "../id/id"
import { Session } from "../session"
import { SessionStatus } from "../session/status"
import { MessageV2 } from "../session/message-v2"
type State = "running" | "completed" | "error"
const DEFAULT_TIMEOUT = 60_000
const POLL_MS = 300
const parameters = z.object({
task_id: Identifier.schema("session").describe("The task_id returned by the task tool"),
wait: z.boolean().optional().describe("When true, wait until the task reaches a terminal state or timeout"),
timeout_ms: z
.number()
.int()
.positive()
.optional()
.describe("Maximum milliseconds to wait when wait=true (default: 60000)"),
})
function format(input: { taskID: string; state: State; text: string }) {
return [`task_id: ${input.taskID}`, `state: ${input.state}`, "", "<task_result>", input.text, "</task_result>"].join(
"\n",
)
}
function errorText(error: NonNullable<MessageV2.Assistant["error"]>) {
const data = error.data as Record<string, unknown> | undefined
const message = data?.message
if (typeof message === "string" && message) return message
return error.name
}
async function inspect(taskID: string) {
const status = SessionStatus.get(taskID)
if (status.type === "busy" || status.type === "retry") {
return {
state: "running" as const,
text: status.type === "retry" ? `Task is retrying: ${status.message}` : "Task is still running.",
}
}
let latestUser: MessageV2.User | undefined
let latestAssistant:
| {
info: MessageV2.Assistant
parts: MessageV2.Part[]
}
| undefined
for await (const item of MessageV2.stream(taskID)) {
if (!latestUser && item.info.role === "user") latestUser = item.info
if (!latestAssistant && item.info.role === "assistant") {
latestAssistant = {
info: item.info,
parts: item.parts,
}
}
if (latestUser && latestAssistant) break
}
if (!latestAssistant) {
return {
state: "running" as const,
text: "Task has started but has not produced output yet.",
}
}
if (latestUser && latestUser.id > latestAssistant.info.id) {
return {
state: "running" as const,
text: "Task is starting.",
}
}
const text = latestAssistant.parts.findLast((part) => part.type === "text")?.text ?? ""
if (latestAssistant.info.error) {
const summary = errorText(latestAssistant.info.error)
return {
state: "error" as const,
text: text || summary,
}
}
const done = latestAssistant.info.finish && !["tool-calls", "unknown"].includes(latestAssistant.info.finish)
if (done) {
return {
state: "completed" as const,
text,
}
}
return {
state: "running" as const,
text: text || "Task is still running.",
}
}
function sleep(ms: number, abort: AbortSignal) {
return new Promise<void>((resolve, reject) => {
if (abort.aborted) {
reject(new Error("Task status polling aborted"))
return
}
const onAbort = () => {
clearTimeout(timer)
reject(new Error("Task status polling aborted"))
}
const timer = setTimeout(() => {
abort.removeEventListener("abort", onAbort)
resolve()
}, ms)
abort.addEventListener("abort", onAbort, { once: true })
})
}
export const TaskStatusTool = Tool.define("task_status", {
description: DESCRIPTION,
parameters,
async execute(params, ctx) {
await Session.get(params.task_id)
let result = await inspect(params.task_id)
if (!params.wait || result.state !== "running") {
return {
title: "Task status",
metadata: {
task_id: params.task_id,
state: result.state,
timed_out: false,
},
output: format({ taskID: params.task_id, state: result.state, text: result.text }),
}
}
const timeout = params.timeout_ms ?? DEFAULT_TIMEOUT
const end = Date.now() + timeout
while (Date.now() < end) {
const left = end - Date.now()
await sleep(Math.min(POLL_MS, left), ctx.abort)
result = await inspect(params.task_id)
if (result.state !== "running") break
}
const done = result.state !== "running"
const text = done ? result.text : `Timed out after ${timeout}ms while waiting for task completion.`
return {
title: "Task status",
metadata: {
task_id: params.task_id,
state: result.state,
timed_out: !done,
},
output: format({ taskID: params.task_id, state: result.state, text }),
}
},
})

View File

@@ -0,0 +1,13 @@
Poll the status of a subagent task launched with the task tool.
Use this to check background tasks started with `task(background=true)`.
Parameters:
- `task_id` (required): the task session id returned by the task tool
- `wait` (optional): when true, wait for completion
- `timeout_ms` (optional): max wait duration in milliseconds when `wait=true`
Returns compact, parseable output:
- `task_id`
- `state` (`running`, `completed`, or `error`)
- `<task_result>...</task_result>` containing final output, error summary, or current progress text

View File

@@ -1,7 +0,0 @@
import { createHash } from "crypto"
export namespace Hash {
export function fast(input: string | Buffer): string {
return createHash("sha1").update(input).digest("hex")
}
}

View File

@@ -1,10 +0,0 @@
import whichPkg from "which"
export function which(cmd: string, env?: NodeJS.ProcessEnv) {
const result = whichPkg.sync(cmd, {
nothrow: true,
path: env?.PATH,
pathExt: env?.PATHEXT,
})
return typeof result === "string" ? result : null
}

View File

@@ -474,11 +474,6 @@ export namespace Worktree {
throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
})
const stop = async (target: string) => {
if (!(await exists(target))) return
await $`git fsmonitor--daemon stop`.quiet().nothrow().cwd(target)
}
const list = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
if (list.exitCode !== 0) {
throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" })
@@ -489,13 +484,11 @@ export namespace Worktree {
if (!entry?.path) {
const directoryExists = await exists(directory)
if (directoryExists) {
await stop(directory)
await clean(directory)
}
return true
}
await stop(entry.path)
const removed = await $`git worktree remove --force ${entry.path}`.quiet().nothrow().cwd(Instance.worktree)
if (removed.exitCode !== 0) {
const next = await $`git worktree list --porcelain`.quiet().nothrow().cwd(Instance.worktree)
@@ -644,7 +637,7 @@ export namespace Worktree {
throw new ResetFailedError({ message: errorText(subClean) || "Failed to clean submodules" })
}
const status = await $`git -c core.fsmonitor=false status --porcelain=v1`.quiet().nothrow().cwd(worktreePath)
const status = await $`git status --porcelain=v1`.quiet().nothrow().cwd(worktreePath)
if (status.exitCode !== 0) {
throw new ResetFailedError({ message: errorText(status) || "Failed to read git status" })
}

View File

@@ -1,62 +0,0 @@
import { $ } from "bun"
import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { File } from "../../src/file"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
const wintest = process.platform === "win32" ? test : test.skip
describe("file fsmonitor", () => {
wintest("status does not start fsmonitor for readonly git checks", async () => {
await using tmp = await tmpdir({ git: true })
const target = path.join(tmp.path, "tracked.txt")
await fs.writeFile(target, "base\n")
await $`git add tracked.txt`.cwd(tmp.path).quiet()
await $`git commit -m init`.cwd(tmp.path).quiet()
await $`git config core.fsmonitor true`.cwd(tmp.path).quiet()
await $`git fsmonitor--daemon stop`.cwd(tmp.path).quiet().nothrow()
await fs.writeFile(target, "next\n")
await fs.writeFile(path.join(tmp.path, "new.txt"), "new\n")
const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(before.exitCode).not.toBe(0)
await Instance.provide({
directory: tmp.path,
fn: async () => {
await File.status()
},
})
const after = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(after.exitCode).not.toBe(0)
})
wintest("read does not start fsmonitor for git diffs", async () => {
await using tmp = await tmpdir({ git: true })
const target = path.join(tmp.path, "tracked.txt")
await fs.writeFile(target, "base\n")
await $`git add tracked.txt`.cwd(tmp.path).quiet()
await $`git commit -m init`.cwd(tmp.path).quiet()
await $`git config core.fsmonitor true`.cwd(tmp.path).quiet()
await $`git fsmonitor--daemon stop`.cwd(tmp.path).quiet().nothrow()
await fs.writeFile(target, "next\n")
const before = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(before.exitCode).not.toBe(0)
await Instance.provide({
directory: tmp.path,
fn: async () => {
await File.read("tracked.txt")
},
})
const after = await $`git fsmonitor--daemon status`.cwd(tmp.path).quiet().nothrow()
expect(after.exitCode).not.toBe(0)
})
})

View File

@@ -1,26 +0,0 @@
import { $ } from "bun"
import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import { tmpdir } from "./fixture"
describe("tmpdir", () => {
test("disables fsmonitor for git fixtures", async () => {
await using tmp = await tmpdir({ git: true })
const value = (await $`git config core.fsmonitor`.cwd(tmp.path).quiet().text()).trim()
expect(value).toBe("false")
})
test("removes directories on dispose", async () => {
const tmp = await tmpdir({ git: true })
const dir = tmp.path
await tmp[Symbol.asyncDispose]()
const exists = await fs
.stat(dir)
.then(() => true)
.catch(() => false)
expect(exists).toBe(false)
})
})

View File

@@ -9,27 +9,6 @@ function sanitizePath(p: string): string {
return p.replace(/\0/g, "")
}
function exists(dir: string) {
return fs
.stat(dir)
.then(() => true)
.catch(() => false)
}
function clean(dir: string) {
return fs.rm(dir, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100,
})
}
async function stop(dir: string) {
if (!(await exists(dir))) return
await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
}
type TmpDirOptions<T> = {
git?: boolean
config?: Partial<Config.Info>
@@ -41,7 +20,6 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
await fs.mkdir(dirpath, { recursive: true })
if (options?.git) {
await $`git init`.cwd(dirpath).quiet()
await $`git config core.fsmonitor false`.cwd(dirpath).quiet()
await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet()
}
if (options?.config) {
@@ -53,16 +31,12 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
}),
)
}
const extra = await options?.init?.(dirpath)
const realpath = sanitizePath(await fs.realpath(dirpath))
const extra = await options?.init?.(realpath)
const result = {
[Symbol.asyncDispose]: async () => {
try {
await options?.dispose?.(realpath)
} finally {
if (options?.git) await stop(realpath).catch(() => undefined)
await clean(realpath).catch(() => undefined)
}
await options?.dispose?.(dirpath)
// await fs.rm(dirpath, { recursive: true, force: true })
},
path: realpath,
extra: extra as T,

View File

@@ -3,7 +3,6 @@
import os from "os"
import path from "path"
import fs from "fs/promises"
import { setTimeout as sleep } from "node:timers/promises"
import { afterAll } from "bun:test"
// Set XDG env vars FIRST, before any src/ imports
@@ -16,7 +15,7 @@ afterAll(async () => {
typeof error === "object" && error !== null && "code" in error && error.code === "EBUSY"
const rm = async (left: number): Promise<void> => {
Bun.gc(true)
await sleep(100)
await Bun.sleep(100)
return fs.rm(dir, { recursive: true, force: true }).catch((error) => {
if (!busy(error)) throw error
if (left <= 1) throw error

View File

@@ -7,8 +7,6 @@ import { Worktree } from "../../src/worktree"
import { Filesystem } from "../../src/util/filesystem"
import { tmpdir } from "../fixture/fixture"
const wintest = process.platform === "win32" ? test : test.skip
describe("Worktree.remove", () => {
test("continues when git remove exits non-zero after detaching", async () => {
await using tmp = await tmpdir({ git: true })
@@ -64,33 +62,4 @@ describe("Worktree.remove", () => {
const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
expect(ref.exitCode).not.toBe(0)
})
wintest("stops fsmonitor before removing a worktree", async () => {
await using tmp = await tmpdir({ git: true })
const root = tmp.path
const name = `remove-fsmonitor-${Date.now().toString(36)}`
const branch = `opencode/${name}`
const dir = path.join(root, "..", name)
await $`git worktree add --no-checkout -b ${branch} ${dir}`.cwd(root).quiet()
await $`git reset --hard`.cwd(dir).quiet()
await $`git config core.fsmonitor true`.cwd(dir).quiet()
await $`git fsmonitor--daemon stop`.cwd(dir).quiet().nothrow()
await Bun.write(path.join(dir, "tracked.txt"), "next\n")
await $`git diff`.cwd(dir).quiet()
const before = await $`git fsmonitor--daemon status`.cwd(dir).quiet().nothrow()
expect(before.exitCode).toBe(0)
const ok = await Instance.provide({
directory: root,
fn: () => Worktree.remove({ directory: dir }),
})
expect(ok).toBe(true)
expect(await Filesystem.exists(dir)).toBe(false)
const ref = await $`git show-ref --verify --quiet refs/heads/${branch}`.cwd(root).quiet().nothrow()
expect(ref.exitCode).not.toBe(0)
})
})

View File

@@ -964,205 +964,6 @@ test("getSmallModel respects config small_model override", async () => {
})
})
test("getExploreModel returns preferred explore model", async () => {
await using tmp = await tmpdir({
config: {
provider: {
"custom-provider": {
name: "Custom Provider",
npm: "@ai-sdk/openai-compatible",
api: "https://api.custom.com/v1",
env: ["CUSTOM_API_KEY"],
models: {
"gpt-5-3-codex-spark": {
name: "GPT-5.3 Codex Spark",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
"claude-haiku-4.5": {
name: "Claude Haiku 4.5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
"gemini-3-flash-preview": {
name: "Gemini 3 Flash",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
"MiniMax-M2-5": {
name: "MiniMax M2.5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
"GLM-5": {
name: "GLM-5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
"Kimi-K2-5": {
name: "Kimi K2.5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
},
options: {
apiKey: "custom-key",
},
},
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = await Provider.getExploreModel("custom-provider")
expect(model).toBeDefined()
expect(model?.id).toBe("gpt-5-3-codex-spark")
},
})
})
test("getExploreModel matches fallback models case-insensitively", async () => {
await using tmp = await tmpdir({
config: {
provider: {
"custom-provider": {
name: "Custom Provider",
npm: "@ai-sdk/openai-compatible",
api: "https://api.custom.com/v1",
env: ["CUSTOM_API_KEY"],
models: {
"MiniMax-M2-5": {
name: "MiniMax M2.5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
"GLM-5": {
name: "GLM-5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
"Kimi-K2-5": {
name: "Kimi K2.5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
},
options: {
apiKey: "custom-key",
},
},
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = await Provider.getExploreModel("custom-provider")
expect(model).toBeDefined()
expect(model?.id).toBe("MiniMax-M2-5")
},
})
})
test("getExploreModel matches kimi separator variant", async () => {
await using tmp = await tmpdir({
config: {
provider: {
"custom-provider": {
name: "Custom Provider",
npm: "@ai-sdk/openai-compatible",
api: "https://api.custom.com/v1",
env: ["CUSTOM_API_KEY"],
models: {
"Kimi-K2-5": {
name: "Kimi K2.5",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
},
options: {
apiKey: "custom-key",
},
},
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = await Provider.getExploreModel("custom-provider")
expect(model).toBeDefined()
expect(model?.id).toBe("Kimi-K2-5")
},
})
})
test("getExploreModel returns undefined when no explore model matches", async () => {
await using tmp = await tmpdir({
config: {
provider: {
"custom-provider": {
name: "Custom Provider",
npm: "@ai-sdk/openai-compatible",
api: "https://api.custom.com/v1",
env: ["CUSTOM_API_KEY"],
models: {
"custom-model": {
name: "Custom Model",
tool_call: true,
limit: {
context: 128000,
output: 4096,
},
},
},
options: {
apiKey: "custom-key",
},
},
},
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const model = await Provider.getExploreModel("custom-provider")
expect(model).toBeUndefined()
},
})
})
test("provider.sort prioritizes preferred models", () => {
const models = [
{ id: "random-model", name: "Random" },

View File

@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test"
import { Instance } from "../../src/project/instance"
import { Pty } from "../../src/pty"
import { tmpdir } from "../fixture/fixture"
import { setTimeout as sleep } from "node:timers/promises"
describe("pty", () => {
test("does not leak output when websocket objects are reused", async () => {
@@ -44,7 +43,7 @@ describe("pty", () => {
// Output from a must never show up in b.
Pty.write(a.id, "AAA\n")
await sleep(100)
await Bun.sleep(100)
expect(outB.join("")).not.toContain("AAA")
} finally {
@@ -89,7 +88,7 @@ describe("pty", () => {
}
Pty.write(a.id, "AAA\n")
await sleep(100)
await Bun.sleep(100)
expect(outB.join("")).not.toContain("AAA")
} finally {
@@ -129,7 +128,7 @@ describe("pty", () => {
ctx.connId = 2
Pty.write(a.id, "AAA\n")
await sleep(100)
await Bun.sleep(100)
expect(out.join("")).toContain("AAA")
} finally {

View File

@@ -1,7 +1,6 @@
import { describe, expect, test } from "bun:test"
import type { NamedError } from "@opencode-ai/util/error"
import { APICallError } from "ai"
import { setTimeout as sleep } from "node:timers/promises"
import { SessionRetry } from "../../src/session/retry"
import { MessageV2 } from "../../src/session/message-v2"
@@ -136,7 +135,7 @@ describe("session.message-v2.fromError", () => {
new ReadableStream({
async pull(controller) {
controller.enqueue("Hello,")
await sleep(10000)
await Bun.sleep(10000)
controller.enqueue(" World!")
controller.close()
},

View File

@@ -0,0 +1,231 @@
import { describe, expect, test } from "bun:test"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"
import { Identifier } from "../../src/id/id"
import { SessionStatus } from "../../src/session/status"
import { TaskStatusTool } from "../../src/tool/task_status"
import { MessageV2 } from "../../src/session/message-v2"
const ctx = {
sessionID: "session_test",
messageID: "message_test",
callID: "call_test",
agent: "build",
abort: AbortSignal.any([]),
messages: [],
metadata: () => {},
ask: async () => {},
}
async function user(sessionID: string) {
await Session.updateMessage({
id: Identifier.ascending("message"),
sessionID,
role: "user",
time: {
created: Date.now(),
},
agent: "build",
model: {
providerID: "test-provider",
modelID: "test-model",
},
})
}
async function assistant(input: { sessionID: string; text: string; error?: string }) {
const msg = await Session.updateMessage({
id: Identifier.ascending("message"),
sessionID: input.sessionID,
role: "assistant",
time: {
created: Date.now(),
completed: Date.now(),
},
parentID: Identifier.ascending("message"),
modelID: "test-model",
providerID: "test-provider",
mode: "build",
agent: "build",
path: {
cwd: process.cwd(),
root: process.cwd(),
},
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: {
read: 0,
write: 0,
},
},
finish: "stop",
...(input.error
? {
error: new MessageV2.APIError({
message: input.error,
isRetryable: false,
}).toObject(),
}
: {}),
})
await Session.updatePart({
id: Identifier.ascending("part"),
sessionID: input.sessionID,
messageID: msg.id,
type: "text",
text: input.text,
})
}
describe("tool.task_status", () => {
test("returns running while session status is busy", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const tool = await TaskStatusTool.init()
SessionStatus.set(session.id, { type: "busy" })
const result = await tool.execute({ task_id: session.id }, ctx)
expect(result.output).toContain("state: running")
SessionStatus.set(session.id, { type: "idle" })
},
})
})
test("returns completed with final task output", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const tool = await TaskStatusTool.init()
await assistant({
sessionID: session.id,
text: "all done",
})
const result = await tool.execute({ task_id: session.id }, ctx)
expect(result.output).toContain("state: completed")
expect(result.output).toContain("all done")
},
})
})
test("wait=true blocks until terminal status", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const tool = await TaskStatusTool.init()
SessionStatus.set(session.id, { type: "busy" })
const transition = Bun.sleep(150).then(async () => {
SessionStatus.set(session.id, { type: "idle" })
await assistant({
sessionID: session.id,
text: "finished later",
})
})
const result = await tool.execute(
{
task_id: session.id,
wait: true,
timeout_ms: 4_000,
},
ctx,
)
await transition
expect(result.output).toContain("state: completed")
expect(result.output).toContain("finished later")
},
})
})
test("returns error when child run fails", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const tool = await TaskStatusTool.init()
await assistant({
sessionID: session.id,
text: "",
error: "child failed",
})
const result = await tool.execute({ task_id: session.id }, ctx)
expect(result.output).toContain("state: error")
expect(result.output).toContain("child failed")
expect(result.metadata.state).toBe("error")
},
})
})
test("wait=true times out with timed_out metadata", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const tool = await TaskStatusTool.init()
SessionStatus.set(session.id, { type: "busy" })
const result = await tool.execute(
{
task_id: session.id,
wait: true,
timeout_ms: 80,
},
ctx,
)
expect(result.output).toContain("Timed out after 80ms")
expect(result.metadata.timed_out).toBe(true)
expect(result.metadata.state).toBe("running")
SessionStatus.set(session.id, { type: "idle" })
},
})
})
test("returns running for resumed task with a newer user turn", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const session = await Session.create({})
const tool = await TaskStatusTool.init()
await user(session.id)
await assistant({
sessionID: session.id,
text: "old done",
})
await user(session.id)
const result = await tool.execute({ task_id: session.id }, ctx)
expect(result.output).toContain("state: running")
expect(result.output).toContain("Task is starting.")
},
})
})
})

View File

@@ -1,82 +0,0 @@
import { describe, expect, test } from "bun:test"
import fs from "fs/promises"
import path from "path"
import { which } from "../../src/util/which"
import { tmpdir } from "../fixture/fixture"
async function cmd(dir: string, name: string, exec = true) {
const ext = process.platform === "win32" ? ".cmd" : ""
const file = path.join(dir, name + ext)
const body = process.platform === "win32" ? "@echo off\r\n" : "#!/bin/sh\n"
await fs.writeFile(file, body)
if (process.platform !== "win32") {
await fs.chmod(file, exec ? 0o755 : 0o644)
}
return file
}
function env(PATH: string): NodeJS.ProcessEnv {
return {
PATH,
PATHEXT: process.env["PATHEXT"],
}
}
function same(a: string | null, b: string) {
if (process.platform === "win32") {
expect(a?.toLowerCase()).toBe(b.toLowerCase())
return
}
expect(a).toBe(b)
}
describe("util.which", () => {
test("returns null when command is missing", () => {
expect(which("opencode-missing-command-for-test")).toBeNull()
})
test("finds a command from PATH override", async () => {
await using tmp = await tmpdir()
const bin = path.join(tmp.path, "bin")
await fs.mkdir(bin)
const file = await cmd(bin, "tool")
same(which("tool", env(bin)), file)
})
test("uses first PATH match", async () => {
await using tmp = await tmpdir()
const a = path.join(tmp.path, "a")
const b = path.join(tmp.path, "b")
await fs.mkdir(a)
await fs.mkdir(b)
const first = await cmd(a, "dupe")
await cmd(b, "dupe")
same(which("dupe", env([a, b].join(path.delimiter))), first)
})
test("returns null for non-executable file on unix", async () => {
if (process.platform === "win32") return
await using tmp = await tmpdir()
const bin = path.join(tmp.path, "bin")
await fs.mkdir(bin)
await cmd(bin, "noexec", false)
expect(which("noexec", env(bin))).toBeNull()
})
test("uses PATHEXT on windows", async () => {
if (process.platform !== "win32") return
await using tmp = await tmpdir()
const bin = path.join(tmp.path, "bin")
await fs.mkdir(bin)
const file = path.join(bin, "pathext.CMD")
await fs.writeFile(file, "@echo off\r\n")
expect(which("pathext", { PATH: bin, PATHEXT: ".CMD" })).toBe(file)
})
})

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.2.19",
"version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.2.19",
"version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.2.19",
"version": "1.2.17",
"type": "module",
"license": "MIT",
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.2.19",
"version": "1.2.17",
"type": "module",
"license": "MIT",
"exports": {

View File

@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/util",
"version": "1.2.19",
"version": "1.2.17",
"private": true,
"type": "module",
"license": "MIT",

View File

@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.2.19",
"version": "1.2.17",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View File

@@ -59,7 +59,6 @@ OpenCode Zen هو بوابة للذكاء الاصطناعي تتيح لك ال
| النموذج | معرّف النموذج | نقطة النهاية | حزمة AI SDK |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -142,7 +141,6 @@ https://opencode.ai/zen/v1/models
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -186,19 +184,6 @@ https://opencode.ai/zen/v1/models
---
### نماذج مهملة
| النموذج | تاريخ الإيقاف |
| ---------------- | ------------- |
| Qwen3 Coder 480B | 6 فبراير 2026 |
| Kimi K2 Thinking | 6 مارس 2026 |
| Kimi K2 | 6 مارس 2026 |
| MiniMax M2.1 | 15 مارس 2026 |
| GLM 4.7 | 15 مارس 2026 |
| GLM 4.6 | 15 مارس 2026 |
---
## الخصوصية
تتم استضافة جميع نماذجنا في الولايات المتحدة. يلتزم مزوّدونا بسياسة عدم الاحتفاظ بالبيانات (zero-retention) ولا يستخدمون بياناتك لتدريب النماذج، مع الاستثناءات التالية:

View File

@@ -55,7 +55,6 @@ Nasim modelima mozete pristupiti i preko sljedecih API endpointa.
| Model | Model ID | Endpoint | AI SDK Package |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -137,7 +136,6 @@ Podrzavamo pay-as-you-go model. Ispod su cijene **po 1M tokena**.
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -180,19 +178,6 @@ Na primjer, ako postavite mjesecni limit na $20, Zen nece potrositi vise od $20
---
### Zastarjeli modeli
| Model | Datum ukidanja |
| ---------------- | -------------- |
| Qwen3 Coder 480B | 6. feb. 2026. |
| Kimi K2 Thinking | 6. mart 2026. |
| Kimi K2 | 6. mart 2026. |
| MiniMax M2.1 | 15. mart 2026. |
| GLM 4.7 | 15. mart 2026. |
| GLM 4.6 | 15. mart 2026. |
---
## Privatnost
Svi nasi modeli su hostovani u SAD-u. Provajderi prate zero-retention politiku i ne koriste vase podatke za treniranje modela, uz sljedece izuzetke:

View File

@@ -64,7 +64,6 @@ Du kan også få adgang til vores modeller gennem følgende API-endpoints.
| Model | Model ID | Endpoint | AI SDK Pakke |
| ------------------- | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -148,7 +147,6 @@ Vi støtter en pay-as-you-go-model. Nedenfor er priserne **per 1 million tokens*
| Gemini 3 Pro (≤ 200K tokens) | $2,00 | $12,00 | $0,20 | - |
| Gemini 3 Pro (> 200K tokens) | $4,00 | $18,00 | $0,40 | - |
| Gemini 3 Flash | $0,50 | $3,00 | $0,05 | - |
| GPT 5.4 | $2,50 | $15,00 | $0,25 | - |
| GPT 5.3 Codex | $1,75 | $14,00 | $0,175 | - |
| GPT 5.2 | $1,75 | $14,00 | $0,175 | - |
| GPT 5.2 Codex | $1,75 | $14,00 | $0,175 | - |
@@ -194,19 +192,6 @@ at opkræve dig mere end $20, hvis din saldo går under $5.
---
### Udfasede modeller
| Model | Udfasningsdato |
| ---------------- | -------------- |
| Qwen3-koder 480B | 6. feb. 2026 |
| Kimi K2 Tenker | 6. marts 2026 |
| Kimi K2 | 6. marts 2026 |
| MiniMax M2.1 | 15. marts 2026 |
| GLM 4.7 | 15. marts 2026 |
| GLM 4.6 | 15. marts 2026 |
---
## Privatliv
Alle vores modeller er hostet i USA. Vores udbydere følger en nul-opbevaringspolitik og bruger ikke dine data til modeltræning, med følgende undtagelser:

View File

@@ -57,7 +57,6 @@ Du kannst unsere Modelle auch ueber die folgenden API-Endpunkte aufrufen.
| Model | Model ID | Endpoint | AI SDK Package |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -115,12 +114,12 @@ Unten siehst du die Preise **pro 1 Mio. Tokens**.
| --------------------------------- | ------ | ------ | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
@@ -141,7 +140,6 @@ Unten siehst du die Preise **pro 1 Mio. Tokens**.
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -186,19 +184,6 @@ Mit aktiviertem Auto-Reload kann die Abrechnung dennoch darueber liegen, falls d
---
### Veraltete Modelle
| Model | Datum der Abschaltung |
| ---------------- | --------------------- |
| Qwen3 Coder 480B | 6. Feb. 2026 |
| Kimi K2 Thinking | 6. Maerz 2026 |
| Kimi K2 | 6. Maerz 2026 |
| MiniMax M2.1 | 15. Maerz 2026 |
| GLM 4.7 | 15. Maerz 2026 |
| GLM 4.6 | 15. Maerz 2026 |
---
## Datenschutz
Alle Modelle werden in den USA gehostet.

View File

@@ -62,7 +62,6 @@ También puede acceder a nuestros modelos a través de los siguientes puntos fin
| Modelo | Model ID | Endpoint | AI SDK package |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -146,7 +145,6 @@ Apoyamos un modelo de pago por uso. A continuación se muestran los precios **po
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0,20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0,40 | - |
| Gemini 3 Flash | $0,50 | $3.00 | $0,05 | - |
| GPT 5.4 | $2,50 | $15,00 | $0,25 | - |
| GPT 5.3 Codex | $1,75 | $14.00 | $0,175 | - |
| GPT 5.2 | $1,75 | $14.00 | $0,175 | - |
| GPT 5.2 Codex | $1,75 | $14.00 | $0,175 | - |
@@ -192,19 +190,6 @@ cobrarle más de $20 si su saldo es inferior a $5.
---
### Modelos obsoletos
| Modelo | Fecha de retiro |
| ---------------- | ------------------- |
| Qwen3 Coder 480B | 6 de feb. de 2026 |
| Kimi K2 Thinking | 6 de marzo de 2026 |
| Kimi K2 | 6 de marzo de 2026 |
| MiniMax M2.1 | 15 de marzo de 2026 |
| GLM 4.7 | 15 de marzo de 2026 |
| GLM 4.6 | 15 de marzo de 2026 |
---
## Privacidad
Todos nuestros modelos están alojados en los EE. UU. Nuestros proveedores siguen una política de retención cero y no utilizan sus datos para la capacitación de modelos, con las siguientes excepciones:

View File

@@ -55,7 +55,6 @@ Vous pouvez également accéder à nos modèles via les points de terminaison AP
| Modèle | ID du modèle | Point de terminaison | Package SDK IA |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -137,7 +136,6 @@ Nous soutenons un modèle de paiement à l'utilisation. Vous trouverez ci-dessou
| Gemini 3 Pro (≤ 200K jetons) | 2,00 $ | 12,00 $ | 0,20 $ | - |
| Gemini 3 Pro (> 200K jetons) | 4,00 $ | 18,00 $ | 0,40 $ | - |
| Gemini 3 Flash | 0,50 $ | 3,00 $ | 0,05 $ | - |
| GPT 5.4 | 2,50 $ | 15,00 $ | 0,25 $ | - |
| GPT 5.3 Codex | 1,75 $ | 14,00 $ | 0,175 $ | - |
| GPT 5.2 | 1,75 $ | 14,00 $ | 0,175 $ | - |
| GPT 5.2 Codex | 1,75 $ | 14,00 $ | 0,175 $ | - |
@@ -180,19 +178,6 @@ Par exemple, disons que vous définissez une limite d'utilisation mensuelle à 2
---
### Modèles obsolètes
| Modèle | Date de dépréciation |
| ---------------- | -------------------- |
| Qwen3 Coder 480B | 6 février 2026 |
| Kimi K2 Thinking | 6 mars 2026 |
| Kimi K2 | 6 mars 2026 |
| MiniMax M2.1 | 15 mars 2026 |
| GLM 4.7 | 15 mars 2026 |
| GLM 4.6 | 15 mars 2026 |
---
## Confidentialité
Tous nos modèles sont hébergés aux États-Unis. Nos fournisseurs suivent une politique de rétention zéro et n'utilisent pas vos données pour la formation de modèles, avec les exceptions suivantes :

View File

@@ -55,7 +55,6 @@ Puoi anche accedere ai nostri modelli tramite i seguenti endpoint API.
| Modello | ID modello | Endpoint | Pacchetto AI SDK |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -137,7 +136,6 @@ Supportiamo un modello pay-as-you-go. Qui sotto trovi i prezzi **per 1M token**.
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -180,19 +178,6 @@ Per esempio, se imposti un limite mensile a $20, Zen non usera piu di $20 in un
---
### Modelli deprecati
| Modello | Data di deprecazione |
| ---------------- | -------------------- |
| Qwen3 Coder 480B | 6 feb 2026 |
| Kimi K2 Thinking | 6 mar 2026 |
| Kimi K2 | 6 mar 2026 |
| MiniMax M2.1 | 15 mar 2026 |
| GLM 4.7 | 15 mar 2026 |
| GLM 4.6 | 15 mar 2026 |
---
## Privacy
Tutti i nostri modelli sono ospitati negli US. I nostri provider seguono una policy di zero-retention e non usano i tuoi dati per training dei modelli, con le seguenti eccezioni:

View File

@@ -54,7 +54,6 @@ OpenCode Zen は、OpenCode の他のプロバイダーと同様に機能しま
| Model | Model ID | Endpoint | AI SDK Package |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -138,7 +137,6 @@ https://opencode.ai/zen/v1/models
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -181,19 +179,6 @@ https://opencode.ai/zen/v1/models
---
### 非推奨モデル
| Model | Deprecation date |
| ---------------- | ---------------- |
| Qwen3 Coder 480B | 2026年2月6日 |
| Kimi K2 Thinking | 2026年3月6日 |
| Kimi K2 | 2026年3月6日 |
| MiniMax M2.1 | 2026年3月15日 |
| GLM 4.7 | 2026年3月15日 |
| GLM 4.6 | 2026年3月15日 |
---
## プライバシー
すべてのモデルは米国でホストされています。当社のプロバイダーはゼロ保持ポリシーに従い、次の例外を除いて、モデルのトレーニングにデータを使用しません。

View File

@@ -55,7 +55,6 @@ OpenCode Zen은 OpenCode의 다른 제공자와 동일한 방식으로 작동합
| 모델 | 모델 ID | 엔드포인트 | AI SDK 패키지 |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -112,12 +111,12 @@ https://opencode.ai/zen/v1/models
| --------------------------------- | ------ | ------ | --------- | --------- |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
@@ -138,7 +137,6 @@ https://opencode.ai/zen/v1/models
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -182,19 +180,6 @@ https://opencode.ai/zen/v1/models
---
### 지원 중단 모델
| 모델 | 지원 중단일 |
| ---------------- | --------------- |
| Qwen3 Coder 480B | 2026년 2월 6일 |
| Kimi K2 Thinking | 2026년 3월 6일 |
| Kimi K2 | 2026년 3월 6일 |
| MiniMax M2.1 | 2026년 3월 15일 |
| GLM 4.7 | 2026년 3월 15일 |
| GLM 4.6 | 2026년 3월 15일 |
---
## 개인정보 보호
당사의 모든 모델은 미국에서 호스팅됩니다. 당사 제공자는 데이터 무보존(zero-retention) 정책을 따르며, 아래의 예외를 제외하고는 귀하의 데이터를 모델 학습에 사용하지 않습니다.

View File

@@ -64,7 +64,6 @@ Du kan også få tilgang til modellene våre gjennom følgende API-endepunkter.
| Modell | Modell ID | Endepunkt | AI SDK Pakke |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -122,7 +121,7 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1 million tokens*
| --------------------------------- | ------- | ------ | ------------- | --------------- |
| Big Pickle | Gratis | Gratis | Gratis | - |
| MiniMax M2.5 Free | Gratis | Gratis | Gratis | - |
| MiniMax M2.5 | $0,30 | $1,20 | $0,06 | $0,375 |
| MiniMax M2.5 | $0,30 | $1,20 | $0,06 | - |
| MiniMax M2.1 | $0,30 | $1,20 | $0,10 | - |
| GLM 5 | $1,00 | $3,20 | $0,20 | - |
| GLM 4.7 | $0,60 | $2,20 | $0,10 | - |
@@ -148,7 +147,6 @@ Vi støtter en pay-as-you-go-modell. Nedenfor er prisene **per 1 million tokens*
| Gemini 3 Pro (≤ 200K tokens) | $2,00 | $12,00 | $0,20 | - |
| Gemini 3 Pro (> 200K tokens) | $4,00 | $18,00 | $0,40 | - |
| Gemini 3 Flash | $0,50 | $3,00 | $0,05 | - |
| GPT 5.4 | $2,50 | $15,00 | $0,25 | - |
| GPT 5.3 Codex | $1,75 | $14,00 | $0,175 | - |
| GPT 5.2 | $1,75 | $14,00 | $0,175 | - |
| GPT 5.2 Codex | $1,75 | $14,00 | $0,175 | - |
@@ -194,19 +192,6 @@ belaster deg mer enn $20 hvis saldoen din går under $5.
---
### Utfasede modeller
| Modell | Utfasingdato |
| ---------------- | ------------- |
| Qwen3 Coder 480B | 6. feb. 2026 |
| Kimi K2 Thinking | 6. mars 2026 |
| Kimi K2 | 6. mars 2026 |
| MiniMax M2.1 | 15. mars 2026 |
| GLM 4.7 | 15. mars 2026 |
| GLM 4.6 | 15. mars 2026 |
---
## Personvern
Alle våre modeller er hostet i USA. Leverandørene våre følger retningslinjer om ingen datalagring og bruker ikke dataene dine til modellopplæring, med følgende unntak:

View File

@@ -1,21 +1,21 @@
---
title: Zen
description: Wyselekcjonowana lista modeli dostarczonych przez OpenCode.
description: Wyselekcjonowana lista modeli dostarczonych przez opencode.
---
import config from "../../../../config.mjs"
export const console = config.console
export const email = `mailto:${config.email}`
OpenCode Zen to lista przetestowanych i zweryfikowanych modeli udostępniona przez zespół OpenCode.
OpenCode Zen to lista przetestowanych i zweryfikowanych modeli udostępniona przez zespół opencode.
:::note
OpenCode Zen jest obecnie w wersji beta.
OpenCode Zen is currently in beta.
:::
Zen działa jak każdy inny dostawca w OpenCode. Logujesz się do OpenCode Zen i otrzymujesz
swój klucz API. Jest to **całkowicie opcjonalne** i nie musisz tego używać, aby korzystać z
OpenCode.
Zen działa jak każdy inny dostawca opencode. Logujesz się do OpenCode Zen i dostajesz
Twój klucz API. Jest **całkowicie opcjonalny** i nie musisz go używać, aby z niego korzystać
opencode.
---
@@ -23,23 +23,23 @@ OpenCode.
Istnieje ogromna liczba modeli, ale tylko kilka z nich
działa dobrze jako agenci kodujący. Dodatkowo większość dostawców jest
skonfigurowana bardzo różnie, więc otrzymujesz bardzo różną wydajność i jakość.
skonfigurowana bardzo różnie; więc otrzymujesz zupełnie inną wydajność i jakość.
:::tip
Przetestowaliśmy wybraną grupę modeli i dostawców, którzy dobrze współpracują z OpenCode.
Przetestowaliśmy wybraną grupę modeli i dostawców, którzy dobrze współpracują z opencode.
:::
Jeśli więc używasz modelu za pośrednictwem czegoś takiego jak OpenRouter, nigdy nie możesz być
Jeśli więc używasz modelu za pośrednictwem czegoś takiego jak OpenRouter, nigdy nie będzie to możliwe
pewien, czy otrzymujesz najlepszą wersję modelu, jaki chcesz.
Aby to naprawić, zrobiliśmy kilka rzeczy:
1. Przetestowaliśmy wybraną grupę modeli i rozmawialiśmy z ich zespołami o tym, jak
najlepiej je uruchamiać.
1. Przetestowaliśmy wybraną grupę modeli i rozmawialiśmy z ich zespołami o tym, jak to zrobić
najlepiej je uruchom.
2. Następnie współpracowaliśmy z kilkoma dostawcami, aby upewnić się, że są one obsługiwane
poprawnie.
3. Na koniec sprawdziliśmy wydajność kombinacji modelu/dostawcy i stworzyliśmy
listę, którą z czystym sumieniem polecamy.
correctly.
3. Na koniec porównaliśmy kombinację modelu/dostawcy i otrzymaliśmy wynik
z listą, którą z przyjemnością polecamy.
OpenCode Zen to brama AI, która zapewnia dostęp do tych modeli.
@@ -47,14 +47,14 @@ OpenCode Zen to brama AI, która zapewnia dostęp do tych modeli.
## Jak to działa
OpenCode Zen działa jak każdy inny dostawca w OpenCode.
OpenCode Zen działa jak każdy inny dostawca opencode.
1. Logujesz się do **<a href={console}>OpenCode Zen</a>**, dodajesz dane rozliczeniowe
i kopiujesz swój klucz API.
1. Logujesz się do **<a href={console}>OpenCode Zen</a>**, dodajesz swoje rozliczenia
szczegóły i skopiuj klucz API.
2. Uruchamiasz polecenie `/connect` w TUI, wybierasz OpenCode Zen i wklejasz klucz API.
3. Uruchom `/models` w TUI, aby zobaczyć listę zalecanych przez nas modeli.
Opłata jest pobierana za każde żądanie i możesz dodać środki do swojego konta.
Opłata jest pobierana za każde żądanie i możesz dodać kredyty do swojego konta.
---
@@ -64,7 +64,6 @@ Dostęp do naszych modeli można również uzyskać za pośrednictwem następuj
| Model | Identyfikator modelu | Punkt końcowy | Pakiet SDK AI |
| ------------------ | -------------------- | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -98,9 +97,9 @@ Dostęp do naszych modeli można również uzyskać za pośrednictwem następuj
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
[Identyfikator modelu](/docs/config/#models) w konfiguracji OpenCode
używa formatu `opencode/<model-id>`. Na przykład w przypadku GPT 5.2 Codex użyłbyś
`opencode/gpt-5.2-codex` w swojej konfiguracji.
[Identyfikator modelu](/docs/config/#models) w konfiguracji opencode
używa formatu `opencode/<model-id>`. Na przykład w przypadku Kodeksu GPT 5.2 zrobiłbyś to
użyj `opencode/gpt-5.2-codex` w swojej konfiguracji.
---
@@ -122,12 +121,12 @@ Wspieramy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów**.
| --------------------------------- | ------- | ------- | --------------------------- | -------------------------- |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
@@ -148,7 +147,6 @@ Wspieramy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów**.
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -160,10 +158,10 @@ Wspieramy model pay-as-you-go. Poniżej znajdują się ceny **za 1M tokenów**.
| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - |
| GPT 5 Nano | Free | Free | Free | - |
Możesz zauważyć _Claude Haiku 3.5_ w swojej historii użytkowania. Jest to [tani model](/docs/config/#models), który jest używany do generowania tytułów Twoich sesji.
Możesz zauważyć _Claude Haiku 3.5_ w swojej historii użytkowania. To jest [model niskokosztowy](/docs/config/#models), który służy do generowania tytułów sesji.
:::note
Opłaty za karty kredytowe są przenoszone po kosztach (4,4% + 0,30 USD za transakcję); nie pobieramy nic poza tym.
Opłaty za karty kredytowe są przenoszone na koszt (4,4% + 0,30 USD za transakcję); nie pobieramy żadnych dodatkowych opłat.
:::
Darmowe modele:
@@ -179,31 +177,18 @@ Darmowe modele:
Jeśli Twoje saldo spadnie poniżej 5 USD, Zen automatycznie doładuje 20 USD.
Możesz zmienić kwotę automatycznego doładowania. Możesz także całkowicie wyłączyć automatyczne doładowanie.
Możesz zmienić kwotę automatycznego doładowania. Możesz także całkowicie wyłączyć automatyczne przeładowywanie.
---
### Limity miesięczne
Możesz także ustawić miesięczny limit użytkowania dla całego obszaru roboczego i dla każdego
członka Twojego zespołu.
Możesz także ustawić miesięczny limit wykorzystania dla całego obszaru roboczego i dla każdego z nich
członek Twojego zespołu.
Na przykład, jeśli ustawisz miesięczny limit użytkowania na 20 USD, Zen nie zużyje
więcej niż 20 dolarów w miesiącu. Ale jeśli masz włączone automatyczne doładowanie, Zen może
obciążyć Cię kwotą wyższą niż 20 USD, jeśli saldo spadnie poniżej 5 USD.
---
### Przestarzałe modele
| Model | Data wycofania |
| ---------------- | -------------- |
| Qwen3 Coder 480B | 6 lutego 2026 |
| Kimi K2 Thinking | 6 marca 2026 |
| Kimi K2 | 6 marca 2026 |
| MiniMax M2.1 | 15 marca 2026 |
| GLM 4.7 | 15 marca 2026 |
| GLM 4.6 | 15 marca 2026 |
Załóżmy na przykład, że ustawiłeś miesięczny limit użytkowania na 20 USD, Zen nie będzie z niego korzystał
ponad 20 dolarów miesięcznie. Ale jeśli masz włączone automatyczne przeładowywanie, Zen może się skończyć
obciąży Cię kwotą wyższą niż 20 USD, jeśli saldo spadnie poniżej 5 USD.
---
@@ -213,22 +198,22 @@ Wszystkie nasze modele są hostowane w USA. Nasi dostawcy przestrzegają polityk
- Big Pickle: W okresie bezpłatnym zebrane dane mogą zostać wykorzystane do udoskonalenia modelu.
- MiniMax M2.5 Free: W okresie bezpłatnym zebrane dane mogą zostać wykorzystane do udoskonalenia modelu.
- API OpenAI: Żądania są przechowywane przez 30 dni zgodnie z [Zasadami dotyczącymi danych OpenAI](https://platform.openai.com/docs/guides/your-data).
- API Anthropic: Żądania są przechowywane przez 30 dni zgodnie z [Zasadami dotyczącymi danych Anthropic](https://docs.anthropic.com/en/docs/claude-code/data-usage).
- Interfejsy API OpenAI: żądania są przechowywane przez 30 dni zgodnie z [Zasadami dotyczącymi danych OpenAI](https://platform.openai.com/docs/guides/your-data).
- Interfejsy API Anthropic: żądania są przechowywane przez 30 dni zgodnie z [Zasadami dotyczącymi danych firmy Anthropic](https://docs.anthropic.com/en/docs/claude-code/data-usage).
---
## Dla zespołów
Zen działa świetnie także dla zespołów. Możesz zapraszać członków zespołu, przypisywać role, dobier
Zen świetnie sprawdza się także w zespołach. Możesz zapraszać członków zespołu, przypisywać role, zarządz
modele, z których korzysta Twój zespół i nie tylko.
:::note
Obszary robocze są obecnie bezpłatne dla zespołów w ramach wersji beta.
:::
Zarządzanie obszarem roboczym jest obecnie bezpłatne dla zespołów w ramach wersji beta.
Wkrótce udostępnimy więcej szczegółów na temat cen.
Zarządzanie obszarem roboczym jest obecnie bezpłatne dla zespołów w ramach wersji beta. Będziemy
wkrótce udostępnimy więcej szczegółów na temat cen.
---
@@ -236,8 +221,8 @@ Wkrótce udostępnimy więcej szczegółów na temat cen.
Możesz zapraszać członków zespołu do swojego obszaru roboczego i przypisywać role:
- **Admin**: Zarządzanie modelami, członkami, kluczami API i rozliczeniami
- **Członek**: Zarządzanie tylko własnymi kluczami API
- **Administrator**: Zarządzaj modelami, członkami, kluczami API i rozliczeniami
- **Członek**: Zarządzaj tylko własnymi kluczami API
Administratorzy mogą także ustawić miesięczne limity wydatków dla każdego członka, aby utrzymać koszty pod kontrolą.
@@ -248,7 +233,7 @@ Administratorzy mogą także ustawić miesięczne limity wydatków dla każdego
Administratorzy mogą włączać i wyłączać określone modele w obszarze roboczym. Żądania skierowane do wyłączonego modelu zwrócą błąd.
Jest to przydatne w przypadkach, gdy chcesz wyłączyć korzystanie z modelu, który
zbiera dane.
collects data.
---
@@ -268,6 +253,6 @@ i chcesz go używać zamiast tego, który zapewnia Zen.
Stworzyliśmy OpenCode Zen, aby:
1. **Testować** (Benchmark) najlepsze modele/dostawców dla agentów kodujących.
2. Mieć dostęp do opcji **najwyższej jakości**, a nie obniżać wydajności ani nie kierować do tańszych dostawców.
3. Przekazywać wszelkie **obniżki cen**, sprzedając po kosztach; więc jedyną marżą jest pokrycie naszych opłat manipulacyjnych.
4. Nie **mieć blokady** (no lock-in), umożliwiając używanie go z dowolnym innym agentem kodującym. I zawsze pozwalać na korzystanie z dowolnego innego dostawcy w OpenCode.
2. Miej dostęp do opcji **najwyższej jakości**, a nie obniżaj wydajności ani nie kieruj się do tańszych dostawców.
3. Przekaż wszelkie **obniżki cen**, sprzedając po kosztach; więc jedyną marżą jest pokrycie naszych opłat manipulacyjnych.
4. Nie **nie blokuj**, umożliwiając używanie go z dowolnym innym agentem kodującym. I zawsze pozwalaj na korzystanie z opencode dowolnego innego dostawcy.

View File

@@ -55,7 +55,6 @@ Você também pode acessar nossos modelos através dos seguintes endpoints da AP
| Modelo | ID do Modelo | Endpoint | Pacote AI SDK |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -137,7 +136,6 @@ Nós suportamos um modelo de pagamento conforme o uso. Abaixo estão os preços
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -180,19 +178,6 @@ Por exemplo, digamos que você defina um limite de uso mensal de $20, o Zen não
---
### Modelos obsoletos
| Modelo | Data de descontinuação |
| ---------------- | ---------------------- |
| Qwen3 Coder 480B | 6 de fev. de 2026 |
| Kimi K2 Thinking | 6 de mar. de 2026 |
| Kimi K2 | 6 de mar. de 2026 |
| MiniMax M2.1 | 15 de mar. de 2026 |
| GLM 4.7 | 15 de mar. de 2026 |
| GLM 4.6 | 15 de mar. de 2026 |
---
## Privacidade
Todos os nossos modelos estão hospedados nos EUA. Nossos provedores seguem uma política de zero retenção e não usam seus dados para treinamento de modelos, com as seguintes exceções:

View File

@@ -63,7 +63,6 @@ OpenCode Zen работает так же, как и любой другой п
| Модель | Идентификатор модели | Конечная точка | Пакет AI SDK |
| ------------------ | -------------------- | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -147,7 +146,6 @@ https://opencode.ai/zen/v1/models
| Gemini 3 Pro (≤ 200 тыс. токенов) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200 тыс. токенов) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -193,19 +191,6 @@ https://opencode.ai/zen/v1/models
---
### Устаревшие модели
| Модель | Дата отключения |
| ---------------- | ---------------- |
| Qwen3 Coder 480B | 6 февр. 2026 г. |
| Kimi K2 Thinking | 6 марта 2026 г. |
| Kimi K2 | 6 марта 2026 г. |
| MiniMax M2.1 | 15 марта 2026 г. |
| GLM 4.7 | 15 марта 2026 г. |
| GLM 4.6 | 15 марта 2026 г. |
---
## Конфиденциальность
Все наши модели размещены в США. Наши поставщики придерживаются политики нулевого хранения и не используют ваши данные для обучения моделей, за следующими исключениями:

View File

@@ -64,7 +64,6 @@ OpenCode Zen ทำงานเหมือนกับผู้ให้บร
| Model | Model ID | Endpoint | แพ็คเกจ AI SDK |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -122,12 +121,12 @@ https://opencode.ai/zen/v1/models
| --------------------------------- | ---------- | -------- | ------- | ---------- |
| Big Pickle | ฟรี | ฟรี | ฟรี | - |
| MiniMax M2.5 Free | ฟรี | ฟรี | ฟรี | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | - |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.08 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
@@ -148,7 +147,6 @@ https://opencode.ai/zen/v1/models
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -194,24 +192,11 @@ https://opencode.ai/zen/v1/models
---
### โมเดลที่เลิกใช้แล้ว
| Model | วันที่เลิกใช้ |
| ---------------- | ------------- |
| Qwen3 Coder 480B | 6 ก.พ. 2026 |
| Kimi K2 Thinking | 6 มี.ค. 2026 |
| Kimi K2 | 6 มี.ค. 2026 |
| MiniMax M2.1 | 15 มี.ค. 2026 |
| GLM 4.7 | 15 มี.ค. 2026 |
| GLM 4.6 | 15 มี.ค. 2026 |
---
## ความเป็นส่วนตัว
โมเดลทั้งหมดของเราโฮสต์ในสหรัฐอเมริกา ผู้ให้บริการของเราปฏิบัติตามนโยบายการเก็บรักษาเป็นศูนย์ และไม่ใช้ข้อมูลของคุณสำหรับการฝึกโมเดล โดยมีข้อยกเว้นต่อไปนี้:
- Big Pickle: ในช่วงระยะเวลาฟรี ข้อมูลที่รวบรวมอาจนำไปใช้ในการปรับปรุงโมเดลได้
- Big Pickle: ในช่วงระยะเวลาว่าง ข้อมูลที่รวบรวมอาจนำไปใช้ในการปรับปรุงโมเดลได้
- MiniMax M2.5 Free: ในช่วงระยะเวลาฟรี ข้อมูลที่รวบรวมอาจนำไปใช้ในการปรับปรุงโมเดล
- OpenAI API: คำขอจะถูกเก็บไว้เป็นเวลา 30 วันตาม [นโยบายข้อมูลของ OpenAI](https://platform.openai.com/docs/guides/your-data)
- Anthropic API: คำขอจะถูกเก็บไว้เป็นเวลา 30 วันตาม [นโยบายข้อมูลของ Anthropic](https://docs.anthropic.com/en/docs/claude-code/data-usage)

View File

@@ -55,7 +55,6 @@ Modellerimize aşağıdaki API uç noktaları aracılığıyla da erişebilirsin
| Model | Model ID | Endpoint | AI SDK Package |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -137,7 +136,6 @@ Kullandıkça öde modelini destekliyoruz. Aşağıda **1 milyon token başına*
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -180,19 +178,6 @@ Ayrıca tüm çalışma alanı ve ekibinizin her üyesi için aylık kullanım l
---
### Kullanımdan kaldırılan modeller
| Model | Kullanımdan kaldırılma tarihi |
| ---------------- | ----------------------------- |
| Qwen3 Coder 480B | 6 Şub 2026 |
| Kimi K2 Thinking | 6 Mar 2026 |
| Kimi K2 | 6 Mar 2026 |
| MiniMax M2.1 | 15 Mar 2026 |
| GLM 4.7 | 15 Mar 2026 |
| GLM 4.6 | 15 Mar 2026 |
---
## Gizlilik
Tüm modellerimiz ABD'de barındırılmaktadır. Sağlayıcılarımız sıfır saklama politikasını izler ve aşağıdaki istisnalar dışında verilerinizi model eğitimi için kullanmaz:

View File

@@ -62,47 +62,44 @@ You are charged per request and you can add credits to your account.
You can also access our models through the following API endpoints.
| Model | Model ID | Endpoint | AI SDK Package |
| ------------------- | ------------------- | -------------------------------------------------- | --------------------------- |
| GPT 5.4 Pro | gpt-5.4-pro | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex Spark | gpt-5.3-codex-spark | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Model | Model ID | Endpoint | AI SDK Package |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 | gpt-5.1 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex | gpt-5.1-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex Max | gpt-5.1-codex-max | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.1 Codex Mini | gpt-5.1-codex-mini | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Gemini 3.1 Pro | gemini-3.1-pro | `https://opencode.ai/zen/v1/models/gemini-3.1-pro` | `@ai-sdk/google` |
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.5 Free | minimax-m2.5-free | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| MiniMax M2.1 | minimax-m2.1 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 5 | glm-5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.7 | glm-4.7 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| GLM 4.6 | glm-4.6 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2.5 | kimi-k2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 Thinking | kimi-k2-thinking | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
The [model id](/docs/config/#models) in your OpenCode config
uses the format `opencode/<model-id>`. For example, for GPT 5.3 Codex, you would
use `opencode/gpt-5.3-codex` in your config.
uses the format `opencode/<model-id>`. For example, for GPT 5.2 Codex, you would
use `opencode/gpt-5.2-codex` in your config.
---
@@ -120,49 +117,46 @@ https://opencode.ai/zen/v1/models
We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| Model | Input | Output | Cached Read | Cached Write |
| --------------------------------- | ------ | ------- | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 Pro | $30.00 | $180.00 | $30.00 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex Spark | $1.75 | $14.00 | $0.175 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.1 | $1.07 | $8.50 | $0.107 | - |
| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - |
| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - |
| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - |
| GPT 5 | $1.07 | $8.50 | $0.107 | - |
| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - |
| GPT 5 Nano | Free | Free | Free | - |
| Model | Input | Output | Cached Read | Cached Write |
| --------------------------------- | ------ | ------ | ----------- | ------------ |
| Big Pickle | Free | Free | Free | - |
| MiniMax M2.5 Free | Free | Free | Free | - |
| MiniMax M2.5 | $0.30 | $1.20 | $0.06 | $0.375 |
| MiniMax M2.1 | $0.30 | $1.20 | $0.10 | - |
| GLM 5 | $1.00 | $3.20 | $0.20 | - |
| GLM 4.7 | $0.60 | $2.20 | $0.10 | - |
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2.5 | $0.60 | $3.00 | $0.10 | - |
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
| Kimi K2 | $0.40 | $2.50 | - | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
| Gemini 3.1 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3.1 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.1 | $1.07 | $8.50 | $0.107 | - |
| GPT 5.1 Codex | $1.07 | $8.50 | $0.107 | - |
| GPT 5.1 Codex Max | $1.25 | $10.00 | $0.125 | - |
| GPT 5.1 Codex Mini | $0.25 | $2.00 | $0.025 | - |
| GPT 5 | $1.07 | $8.50 | $0.107 | - |
| GPT 5 Codex | $1.07 | $8.50 | $0.107 | - |
| GPT 5 Nano | Free | Free | Free | - |
You might notice _Claude Haiku 3.5_ in your usage history. This is a [low cost model](/docs/config/#models) that's used to generate the titles of your sessions.

View File

@@ -55,7 +55,6 @@ OpenCode Zen 的工作方式与 OpenCode 中的任何其他提供商相同。
| 模型 | 模型 ID | 端点 | AI SDK 包 |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -137,7 +136,6 @@ https://opencode.ai/zen/v1/models
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -180,19 +178,6 @@ https://opencode.ai/zen/v1/models
---
### 已弃用模型
| 模型 | 弃用日期 |
| ---------------- | ------------------ |
| Qwen3 Coder 480B | 2026 年 2 月 6 日 |
| Kimi K2 Thinking | 2026 年 3 月 6 日 |
| Kimi K2 | 2026 年 3 月 6 日 |
| MiniMax M2.1 | 2026 年 3 月 15 日 |
| GLM 4.7 | 2026 年 3 月 15 日 |
| GLM 4.6 | 2026 年 3 月 15 日 |
---
## 隐私
我们所有的模型都托管在美国。我们的提供商遵循零保留政策,不会将你的数据用于模型训练,但以下情况除外:

View File

@@ -55,7 +55,6 @@ OpenCode Zen 的工作方式與 OpenCode 中的任何其他供應商相同。
| 模型 | 模型 ID | 端點 | AI SDK 套件 |
| ------------------ | ------------------ | -------------------------------------------------- | --------------------------- |
| GPT 5.4 | gpt-5.4 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.3 Codex | gpt-5.3-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 | gpt-5.2 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5.2 Codex | gpt-5.2-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
@@ -137,7 +136,6 @@ https://opencode.ai/zen/v1/models
| Gemini 3 Pro (≤ 200K Token) | $2.00 | $12.00 | $0.20 | - |
| Gemini 3 Pro (> 200K Token) | $4.00 | $18.00 | $0.40 | - |
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
| GPT 5.4 | $2.50 | $15.00 | $0.25 | - |
| GPT 5.3 Codex | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 | $1.75 | $14.00 | $0.175 | - |
| GPT 5.2 Codex | $1.75 | $14.00 | $0.175 | - |
@@ -180,19 +178,6 @@ https://opencode.ai/zen/v1/models
---
### 已棄用的模型
| 模型 | 棄用日期 |
| ---------------- | ------------------ |
| Qwen3 Coder 480B | 2026 年 2 月 6 日 |
| Kimi K2 Thinking | 2026 年 3 月 6 日 |
| Kimi K2 | 2026 年 3 月 6 日 |
| MiniMax M2.1 | 2026 年 3 月 15 日 |
| GLM 4.7 | 2026 年 3 月 15 日 |
| GLM 4.6 | 2026 年 3 月 15 日 |
---
## 隱私
我們所有的模型都託管在美國。我們的供應商遵循零保留政策,不會將你的資料用於模型訓練,但以下情況除外:

View File

@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.2.19",
"version": "1.2.17",
"publisher": "sst-dev",
"repository": {
"type": "git",