Compare commits

..
7 Commits
41 changed files with 235 additions and 231 deletions
+31 -31
View File
@@ -29,7 +29,7 @@
},
"packages/app": {
"name": "@opencode-ai/app",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@corvu/drawer": "catalog:",
"@dnd-kit/abstract": "0.5.0",
@@ -96,7 +96,7 @@
},
"packages/cli": {
"name": "@opencode-ai/cli",
"version": "1.18.26",
"version": "1.18.27",
"bin": {
"lildax": "./bin/lildax.cjs",
},
@@ -144,7 +144,7 @@
},
"packages/codemode": {
"name": "@opencode-ai/codemode",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"acorn": "8.15.0",
"effect": "catalog:",
@@ -158,7 +158,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -195,7 +195,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -222,7 +222,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@ai-sdk/anthropic": "3.0.82",
"@ai-sdk/openai": "3.0.48",
@@ -245,7 +245,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -269,7 +269,7 @@
},
"packages/console/support": {
"name": "@opencode-ai/console-support",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@opencode-ai/console-core": "workspace:*",
@@ -289,7 +289,7 @@
},
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.18.26",
"version": "1.18.27",
"bin": {
"opencode": "./bin/opencode",
},
@@ -341,7 +341,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.12.1",
"gitlab-ai-provider": "6.13.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@@ -383,7 +383,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@zip.js/zip.js": "2.7.62",
"drizzle-orm": "catalog:",
@@ -437,7 +437,7 @@
},
"packages/effect-drizzle-sqlite": {
"name": "@opencode-ai/effect-drizzle-sqlite",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"drizzle-orm": "catalog:",
"effect": "catalog:",
@@ -451,7 +451,7 @@
},
"packages/effect-sqlite-node": {
"name": "@opencode-ai/effect-sqlite-node",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"effect": "catalog:",
},
@@ -463,7 +463,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@hono/standard-validator": "catalog:",
"@opencode-ai/core": "workspace:*",
@@ -495,7 +495,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -511,7 +511,7 @@
},
"packages/http-recorder": {
"name": "@opencode-ai/http-recorder",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@effect/platform-node": "4.0.0-beta.83",
"@effect/platform-node-shared": "4.0.0-beta.83",
@@ -542,7 +542,7 @@
},
"packages/llm": {
"name": "@opencode-ai/llm",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@smithy/eventstream-codec": "4.2.14",
@@ -561,7 +561,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.18.26",
"version": "1.18.27",
"bin": {
"opencode": "./bin/opencode",
},
@@ -634,7 +634,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.12.1",
"gitlab-ai-provider": "6.13.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
@@ -692,7 +692,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@ai-sdk/provider": "3.0.8",
"@opencode-ai/sdk": "workspace:*",
@@ -768,7 +768,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"cross-spawn": "catalog:",
},
@@ -783,7 +783,7 @@
},
"packages/server": {
"name": "@opencode-ai/server",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/protocol": "workspace:*",
@@ -798,7 +798,7 @@
},
"packages/session-ui": {
"name": "@opencode-ai/session-ui",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/client": "file:../app/vendor/opencode-ai-client-1.17.13-v2.tgz",
@@ -838,7 +838,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -851,7 +851,7 @@
},
"packages/stats/app": {
"name": "@opencode-ai/stats-app",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@ibm/plex": "6.4.1",
"@kobalte/core": "catalog:",
@@ -878,7 +878,7 @@
},
"packages/stats/core": {
"name": "@opencode-ai/stats-core",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@aws-sdk/client-athena": "3.933.0",
"@planetscale/database": "1.19.0",
@@ -897,7 +897,7 @@
},
"packages/stats/server": {
"name": "@opencode-ai/stats-server",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@aws-sdk/client-firehose": "3.933.0",
"@effect/platform-node": "catalog:",
@@ -939,7 +939,7 @@
},
"packages/tui": {
"name": "@opencode-ai/tui",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/plugin": "workspace:*",
@@ -966,7 +966,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@kobalte/core": "catalog:",
"@pierre/diffs": "catalog:",
@@ -1017,7 +1017,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -3845,7 +3845,7 @@
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.12.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-Qn5iHqvjG8yktI5MWaUgdRR94l7O4WtYW0CAbhsCh1Tj0Fei/DeprOYPVyf4Nht1Ix6U2PXSYM32QOHI6Z2TDw=="],
"gitlab-ai-provider": ["gitlab-ai-provider@6.13.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=3.0.0", "@ai-sdk/provider-utils": ">=4.0.0" } }, "sha512-JDZhNjvoiB7xBfesNegXNDg6ItKf9M2l8/DifuIyuvYmGgaZnDDbl89QrOKSoIzAOpo/+3Om0qviBPm84nGEbg=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
+4 -4
View File
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-SVvFPO+KuS67+6XGPhaB3cIuc3XUyM0XVccy5v8afS4=",
"aarch64-linux": "sha256-HJRrSu5u0TEg214d2RAbM1C+nmrxvIR/d7JlSBOGb9I=",
"aarch64-darwin": "sha256-ytmHSdC0PVGAJSbpt/+YwL5ySF3w6r/9uZpZACPppkI=",
"x86_64-darwin": "sha256-ma7K4K+xn7Jz3+YPA/n8ly1o308UNoM/DO9Z8yZSAKE="
"x86_64-linux": "sha256-2kzFIn42mD7ZDu/+6lWctqjZ/lIVZZfjZhmF/ymhF54=",
"aarch64-linux": "sha256-4HlReGD3gYfyyfnY/FQ46Ov+g3ZGV3sBYQ9p1bS9YAY=",
"aarch64-darwin": "sha256-dkfCoH/sW9XBPZ7XhnUoE54TY1lcfNvZ5wQQLr7gKiQ=",
"x86_64-darwin": "sha256-32t1JEcibZ4OrornIgfWOGQA87FOXjrxrxlDrflh7Ss="
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.26",
"version": "1.18.27",
"description": "",
"type": "module",
"exports": {
@@ -31,7 +31,10 @@ export function OpenInAppV2(props: { directory: () => string }) {
disabled={state.opening()}
aria-label={language.t("session.header.open.ariaLabel", { app: state.current().label })}
>
<Show when={state.opening()} fallback={<AppIcon id={state.current().icon} class="size-[18px]" />}>
<Show
when={state.opening()}
fallback={<AppIcon id={state.current().icon} style={{ width: "20px", height: "20px" }} />}
>
<Spinner class="size-3.5" />
</Show>
</SplitButtonV2Action>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/cli",
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"bin": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/codemode",
"version": "1.18.26",
"version": "1.18.27",
"description": "Effect-native confined code execution over schema-described tools",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.18.26",
"version": "1.18.27",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.18.26",
"version": "1.18.27",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-support",
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"scripts": {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.26",
"version": "1.18.27",
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
@@ -108,7 +108,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.12.1",
"gitlab-ai-provider": "6.13.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
+9 -4
View File
@@ -108,15 +108,20 @@ export const Info = Schema.Struct({
headerTimeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description:
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
"Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.",
}),
).annotate({
description:
"Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.",
"Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.",
}),
chunkTimeout: Schema.optional(PositiveInt).annotate({
chunkTimeout: Schema.optional(
Schema.Union([PositiveInt, Schema.Literal(false)]).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.",
}),
).annotate({
description:
"Timeout in milliseconds between streamed SSE chunks for this provider. If no chunk arrives within this window, the request is aborted.",
"Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.",
}),
}),
[Schema.Record(Schema.String, Schema.Any)],
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/desktop",
"private": true,
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"homepage": "https://opencode.ai",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.26",
"version": "1.18.27",
"name": "@opencode-ai/effect-drizzle-sqlite",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.26",
"version": "1.18.27",
"name": "@opencode-ai/effect-sqlite-node",
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.18.26",
"version": "1.18.27",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.26",
"version": "1.18.27",
"name": "@opencode-ai/http-recorder",
"description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.26",
"version": "1.18.27",
"name": "@opencode-ai/llm",
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.18.26",
"version": "1.18.27",
"name": "opencode",
"type": "module",
"license": "MIT",
@@ -120,7 +120,7 @@
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"gitlab-ai-provider": "6.12.1",
"gitlab-ai-provider": "6.13.0",
"glob": "13.0.5",
"google-auth-library": "10.5.0",
"gray-matter": "4.0.3",
+7 -45
View File
@@ -22,7 +22,7 @@ import { UI } from "../ui"
import { effectCmd } from "../effect-cmd"
import { EOL } from "os"
import { Filesystem } from "@/util/filesystem"
import { createOpencodeClient, type Event, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { createOpencodeClient, type OpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2"
import { FormatError, FormatUnknownError } from "../error"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./run/runtime.stdin"
@@ -690,14 +690,12 @@ export const RunCommand = effectCmd({
return false
}
const toggles = new Map<string, boolean>()
const printed = new Set<string>()
// Consume one subscribed event stream for the active session and mirror it
// to stdout/UI. `client` is passed explicitly because attach mode may
// rebind the SDK to the session's directory after the subscription is
// created, and replies issued from inside the loop must use that client.
async function loop(client: OpencodeClient, events: Awaited<ReturnType<typeof sdk.event.subscribe>>) {
const toggles = new Map<string, boolean>()
const sessions = new Set([sessionID])
let error: string | undefined
@@ -722,16 +720,6 @@ export const RunCommand = effectCmd({
if (event.type === "message.part.updated") {
const part = event.properties.part
if (part.sessionID !== sessionID) continue
if (
args.attach &&
(part.type === "step-start" ||
part.type === "step-finish" ||
((part.type === "text" || part.type === "reasoning") && part.time?.end) ||
(part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")))
) {
if (printed.has(part.id)) continue
printed.add(part.id)
}
if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
if (emit("tool_use", { part })) continue
@@ -843,41 +831,15 @@ export const RunCommand = effectCmd({
await share(client, sessionID)
if (!interactive) {
const controller = new AbortController()
const events = await client.event.subscribe(undefined, { signal: controller.signal })
const events = await client.event.subscribe()
const completed = loop(client, events).catch((e) => {
console.error(e)
process.exitCode = 1
})
async function finish(parentID?: string) {
if (args.attach) controller.abort()
async function finish() {
if (args.attach) return
const error = await completed
if (error) process.exitCode = 1
if (!args.attach || !parentID) return
// The request has completed, but remote events may still be buffered
// or missing. Recover this prompt's output without waiting for idle.
const messages = await client.session.messages({ sessionID }, { throwOnError: true })
async function* replay(): AsyncGenerator<Event> {
for (const message of messages.data) {
if (message.info.role !== "assistant" || message.info.parentID !== parentID) continue
yield { id: message.info.id, type: "message.updated", properties: { sessionID, info: message.info } }
for (const part of message.parts)
yield {
id: part.id,
type: "message.part.updated",
properties: { sessionID, part, time: message.info.time.completed ?? message.info.time.created },
}
if (message.info.error && !error)
yield {
id: message.info.id,
type: "session.error",
properties: { sessionID, error: message.info.error },
}
}
}
const replayError = await loop(client, { stream: replay() })
if (replayError) process.exitCode = 1
}
if (args.command) {
@@ -894,7 +856,7 @@ export const RunCommand = effectCmd({
process.exitCode = 1
return
}
await finish(result.data?.info.parentID)
await finish()
return
}
@@ -911,7 +873,7 @@ export const RunCommand = effectCmd({
process.exitCode = 1
return
}
await finish(result.data?.info.parentID)
await finish()
return
}
+2 -2
View File
@@ -1792,8 +1792,8 @@ const layer = Layer.effect(
if (existing) return existing
const customFetch = options["fetch"]
const chunkTimeout = options["chunkTimeout"]
const headerTimeout = options["headerTimeout"]
const chunkTimeout = options["chunkTimeout"] ?? 300_000
const headerTimeout = options["headerTimeout"] ?? 300_000
delete options["chunkTimeout"]
delete options["headerTimeout"]
@@ -7,7 +7,6 @@ import { describe, expect } from "bun:test"
import { Effect } from "effect"
import { reply } from "../../lib/llm-server"
import { cliIt } from "../../lib/cli-process"
import { testProviderConfig } from "../../lib/test-provider"
describe("opencode run (non-interactive subprocess)", () => {
// Happy path: prompt completes, output reaches stdout, process exits 0.
@@ -300,7 +299,6 @@ describe("opencode run (non-interactive subprocess)", () => {
})
opencode.expectExit(result, 0)
expect(result.stdout).toBe("attachment received\n")
const input = JSON.stringify(yield* llm.inputs)
expect(input).toContain(sentinel)
expect(input).not.toContain(`file://${source}`)
@@ -308,104 +306,6 @@ describe("opencode run (non-interactive subprocess)", () => {
60_000,
)
for (const mode of ["stream", "missing-idle", "command"]) {
cliIt.live(
`attach preserves completed output (${mode})`,
({ llm, opencode }) =>
Effect.gen(function* () {
yield* llm.push(
reply().reason("thinking").text("before").tool("bash", {
command: "printf tool-output",
description: "Print deterministic output",
}),
)
yield* llm.text("after")
const server = yield* opencode.serve({
env: {
OPENCODE_CONFIG_CONTENT: JSON.stringify({
...testProviderConfig(llm.url),
command: { probe: { template: "exercise output" } },
}),
},
})
// Keep SSE connected but withhold its events, including session idle.
const proxy = yield* Effect.acquireRelease(
Effect.sync(() =>
Bun.serve({
hostname: "127.0.0.1",
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/event" && mode !== "stream") {
return new Response(
new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"server.connected","properties":{}}\n\n'),
)
},
}),
{ headers: { "Content-Type": "text/event-stream" } },
)
}
const forwarded = new Request(new URL(url.pathname + url.search, server.url), request)
forwarded.headers.set("Accept-Encoding", "identity")
return fetch(forwarded)
},
}),
),
(proxy) => Effect.sync(() => proxy.stop(true)),
)
const result = yield* opencode.run("exercise output", {
command: mode === "command" ? "probe" : undefined,
format: "json",
extraArgs: ["--attach", proxy.url.origin, "--thinking", "--dangerously-skip-permissions"],
})
opencode.expectExit(result, 0)
const events = opencode.parseJsonEvents(result.stdout)
expect(events.map((event) => event.type)).toEqual([
"step_start",
"reasoning",
"text",
"tool_use",
"step_finish",
"step_start",
"text",
"step_finish",
])
expect(events.filter((event) => event.type === "text").map((event) => event.part)).toEqual([
expect.objectContaining({ text: "before" }),
expect.objectContaining({ text: "after" }),
])
yield* llm.text("next turn")
const resumed = yield* opencode.run("continue", {
format: "json",
extraArgs: ["--attach", proxy.url.origin, "--session", String(events[0].sessionID)],
})
opencode.expectExit(resumed, 0)
expect(opencode.parseJsonEvents(resumed.stdout).map((event) => event.type)).toEqual([
"step_start",
"text",
"step_finish",
])
expect(resumed.stdout).toContain("next turn")
expect(resumed.stdout).not.toContain('"before"')
const rejected = yield* opencode.run("unknown model", {
model: "test/nonexistent-model",
format: "json",
extraArgs: ["--attach", proxy.url.origin],
})
expect(rejected.exitCode).not.toBe(0)
const errors = opencode.parseJsonEvents(rejected.stdout)
expect(errors.length).toBeGreaterThan(0)
expect(errors.every((event) => event.type === "error")).toBe(true)
}),
60_000,
)
}
cliIt.concurrent(
"attach mode rejects local directories before prompt admission",
({ home, opencode }) =>
@@ -13,6 +13,8 @@ import { Env } from "@/env"
import { Plugin } from "@/plugin"
import { Provider } from "@/provider/provider"
import { ProviderError } from "@/provider/error"
import { MessageV2 } from "@/session/message-v2"
import { SessionRetry } from "@/session/retry"
afterEach(async () => {
await disposeAllInstances()
@@ -46,7 +48,48 @@ it.live("headerTimeout does not abort delayed SSE body after headers arrive", ()
}),
)
it.live("chunkTimeout raises a response stream error when SSE body stalls", () =>
for (const timeout of ["chunkTimeout", "headerTimeout"] as const) {
it.live(`default ${timeout} is applied at fetch without changing provider options`, () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedBodyServer(250)),
(server) => Effect.sync(() => server.server.close()),
)
yield* provideTmpdirInstance(
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const configured = yield* provider.getProvider(ProviderV2.ID.make("test"))
const signals: (AbortSignal | null | undefined)[] = []
configured.options.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
signals.push(init?.signal)
return fetch(input, init)
}
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
const language = yield* provider.getLanguage(model)
yield* Effect.acquireRelease(
Effect.promise(() =>
language.doStream({ prompt: [{ role: "user", content: [{ type: "text", text: "hello" }] }] }),
),
(result) => Effect.promise(() => result.stream.cancel()),
)
expect(signals).toHaveLength(1)
expect(signals[0]).toBeInstanceOf(AbortSignal)
expect(configured.options[timeout]).toBeUndefined()
}),
{
config: providerConfig(server.url, {
[timeout === "chunkTimeout" ? "headerTimeout" : "chunkTimeout"]: false,
}),
},
)
}),
)
}
it.live("configured chunkTimeout raises a retryable response stream error when SSE body stalls", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedBodyServer(250)),
@@ -74,12 +117,41 @@ it.live("chunkTimeout raises a response stream error when SSE body stalls", () =
}
})
expect(error).toBeInstanceOf(ProviderError.ResponseStreamError)
expect(
SessionRetry.retryable(MessageV2.fromError(error, { providerID: model.providerID }), model.providerID),
).toEqual({ message: "SSE read timed out" })
}),
{ config: providerConfig(server.url, { chunkTimeout: 50 }) },
)
}),
)
it.live("chunkTimeout can be disabled with false", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedBodyServer(250)),
(server) => Effect.sync(() => server.server.close()),
)
yield* provideTmpdirInstance(
() =>
Effect.gen(function* () {
const provider = yield* Provider.Service
const configured = yield* provider.getProvider(ProviderV2.ID.make("test"))
expect(configured.options.chunkTimeout).toBe(false)
const model = yield* provider.getModel(ProviderV2.ID.make("test"), ModelV2.ID.make("test-model"))
const result = streamText({
model: yield* provider.getLanguage(model),
messages: [{ role: "user", content: "hello" }],
})
expect(yield* Effect.promise(() => result.text)).toBe("late")
}),
{ config: providerConfig(server.url, { chunkTimeout: false }) },
)
}),
)
it.live("headerTimeout aborts when response headers do not arrive", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
@@ -112,7 +184,7 @@ it.live("headerTimeout aborts when response headers do not arrive", () =>
}),
)
it.live("headerTimeout is opt-in for non-OpenAI providers", () =>
it.live("headerTimeout can be disabled with false for non-OpenAI providers", () =>
Effect.gen(function* () {
const server = yield* Effect.acquireRelease(
Effect.promise(() => delayedHeaderServer(100)),
@@ -131,12 +203,12 @@ it.live("headerTimeout is opt-in for non-OpenAI providers", () =>
expect(yield* Effect.promise(() => result.text)).toBe("ok")
}),
{ config: providerConfig(server.url) },
{ config: providerConfig(server.url, { headerTimeout: false }) },
)
}),
)
it.live("OpenAI Codex headerTimeout default can be disabled by config", () =>
it.live("OpenAI Codex header and chunk timeout defaults can be disabled by config", () =>
Effect.gen(function* () {
yield* withAuthContent(
Effect.gen(function* () {
@@ -146,8 +218,9 @@ it.live("OpenAI Codex headerTimeout default can be disabled by config", () =>
const provider = yield* Provider.Service
const openai = yield* provider.getProvider(ProviderV2.ID.openai)
expect(openai.options.headerTimeout).toBe(false)
expect(openai.options.chunkTimeout).toBe(false)
}),
{ config: { provider: { openai: { options: { headerTimeout: false } } } } },
{ config: { provider: { openai: { options: { headerTimeout: false, chunkTimeout: false } } } } },
)
}),
)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"scripts": {
+6 -3
View File
@@ -1751,11 +1751,14 @@ export type ProviderConfig = {
*/
timeout?: number | false
/**
* Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout.
* Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout.
*/
headerTimeout?: number | false
chunkTimeout?: number
[key: string]: unknown | string | boolean | number | false | number | false | number | undefined
/**
* Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout.
*/
chunkTimeout?: number | false
[key: string]: unknown | string | boolean | number | false | number | false | number | false | undefined
}
models?: {
[key: string]: {
+12 -3
View File
@@ -20780,11 +20780,20 @@
"enum": [false]
}
],
"description": "Timeout in milliseconds to wait for response headers. Provider integrations may set defaults. Set to false to disable timeout."
"description": "Timeout in milliseconds to wait for response headers (default: 300000). Set to false to disable timeout."
},
"chunkTimeout": {
"type": "integer",
"exclusiveMinimum": 0
"anyOf": [
{
"type": "integer",
"exclusiveMinimum": 0
},
{
"type": "boolean",
"enum": [false]
}
],
"description": "Timeout in milliseconds between streamed SSE chunks for this provider (default: 300000). If no chunk arrives within this window, the request is aborted. Set to false to disable timeout."
}
},
"additionalProperties": {}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/server",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/session-ui",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-app",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-core",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/stats-server",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/tui",
"version": "1.18.26",
"version": "1.18.27",
"private": true,
"type": "module",
"license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.18.26",
"version": "1.18.27",
"type": "module",
"license": "MIT",
"repository": {
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "@opencode-ai/web",
"type": "module",
"license": "MIT",
"version": "1.18.26",
"version": "1.18.27",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+3 -2
View File
@@ -374,7 +374,7 @@ You can configure the providers and models you want to use in your OpenCode conf
The `small_model` option configures a separate model for lightweight tasks like title generation. By default, OpenCode tries to use a cheaper model if one is available from your provider, otherwise it falls back to your main model.
Provider options can include `timeout`, `chunkTimeout`, and `setCacheKey`:
Provider options can include `timeout`, `headerTimeout`, `chunkTimeout`, and `setCacheKey`:
```json title="opencode.json"
{
@@ -392,7 +392,8 @@ Provider options can include `timeout`, `chunkTimeout`, and `setCacheKey`:
```
- `timeout` - Request timeout in milliseconds (default: 300000). Set to `false` to disable.
- `chunkTimeout` - Timeout in milliseconds between streamed response chunks. If no chunk arrives in time, the request is aborted.
- `headerTimeout` - Timeout in milliseconds to wait for response headers (default: 300000, or 5 minutes). This timer stops once headers arrive and does not limit the streamed response body. Set to `false` to disable.
- `chunkTimeout` - Timeout in milliseconds between streamed response chunks (default: 300000, or 5 minutes). If no chunk arrives in time, the request is aborted. Set to `false` to disable.
- `setCacheKey` - Ensure a cache key is always set for designated provider.
You can also configure [local models](/docs/models#local). [Learn more](/docs/models).
@@ -874,6 +874,54 @@ Selecting a router model is a drop-in replacement for any other model — OpenCo
---
### Eden AI
[Eden AI](https://www.edenai.co/) is an EU-based gateway that serves models from many vendors over a single OpenAI-compatible API, with a separate EU endpoint for teams that need inference to stay in the EU.
1. Head over to the [Eden AI platform](https://app.edenai.run/user/register) to create an account and generate an API key.
2. Run the `/connect` command and search for **Eden AI**.
```txt
/connect
```
3. Enter your Eden AI API key.
```txt
┌ API key
└ enter
```
4. Run the `/models` command to select a model like _Mistral Large 3_ or _Claude Sonnet 5_.
```txt
/models
```
Eden AI model ids are themselves in `vendor/model` form, so a full reference has three segments, for example `edenai/anthropic/claude-sonnet-5`.
5. To keep requests on Eden AI's EU gateway, set its base URL.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"edenai": {
"options": {
"baseURL": "https://api.eu.edenai.run/v3"
}
}
}
}
```
The default is `https://api.edenai.run/v3`, so this swaps the global endpoint for the EU one. The EU endpoint serves the subset of the catalog that is available in the EU, so a model chosen in step 4 may not be reachable through it.
---
### FrogBot
1. Head over to the [FrogBot dashboard](https://app.frogbot.ai/signup), create an account, and generate an API key.
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.18.26",
"version": "1.18.27",
"publisher": "sst-dev",
"repository": {
"type": "git",