From 843f126ce5e4ecf231673ebd705ab102b7293628 Mon Sep 17 00:00:00 2001 From: CrytsalTraveler Date: Mon, 20 Apr 2026 14:42:38 +0800 Subject: [PATCH 1/3] fix(provider): enable image support for custom OpenAI-compatible providers Custom OpenAI-compatible providers (Ollama, longent, etc.) were unable to process image attachments because capabilities.input.image defaulted to false. Now defaults to true for @ai-sdk/openai-compatible providers. Closes #20802 --- packages/opencode/src/provider/provider.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 2142f843404c..25b8dbc3608f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -1413,7 +1413,10 @@ export const layer = Layer.effect( input: { text: model.modalities?.input?.includes("text") ?? existingModel?.capabilities.input.text ?? true, audio: model.modalities?.input?.includes("audio") ?? existingModel?.capabilities.input.audio ?? false, - image: model.modalities?.input?.includes("image") ?? existingModel?.capabilities.input.image ?? false, + image: + model.modalities?.input?.includes("image") ?? + existingModel?.capabilities.input.image ?? + (provider.npm === "@ai-sdk/openai-compatible" ? true : false), video: model.modalities?.input?.includes("video") ?? existingModel?.capabilities.input.video ?? false, pdf: model.modalities?.input?.includes("pdf") ?? existingModel?.capabilities.input.pdf ?? false, }, From d8329abd505155a69b38493f5d2b9aa43ad228dc Mon Sep 17 00:00:00 2001 From: CrytsalTraveler Date: Mon, 20 Apr 2026 14:57:51 +0800 Subject: [PATCH 2/3] fix: handle stream interruption for OpenAI-compatible providers - Add SSE timeout, connection reset, abort, and stream truncation patterns to retryable() matching for automatic retry - Classify 'SSE read timed out' as APIError(isRetryable: true) - Emit error event in flush() when stream ends without finish_reason while output is still active, instead of silently accepting truncation Closes #20466 --- .../chat/openai-compatible-chat-language-model.ts | 9 +++++++++ packages/opencode/src/provider/provider.ts | 1 + packages/opencode/src/session/message-v2.ts | 9 +++++++++ packages/opencode/src/session/retry.ts | 8 ++++++-- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts b/packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts index 280970c41b4f..52df3624ea72 100644 --- a/packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts +++ b/packages/core/src/github-copilot/chat/openai-compatible-chat-language-model.ts @@ -645,6 +645,15 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { }, flush(controller) { + // If the stream ended without a finish_reason and output is still + // active, the stream was truncated — emit an error to trigger retry + const hasActiveOutput = isActiveReasoning || isActiveText || toolCalls.some((tc) => !tc.hasFinished) + if (finishReason.unified === "other" && finishReason.raw === undefined && hasActiveOutput) { + controller.enqueue({ + type: "error", + error: new Error("Stream ended unexpectedly — no finish reason received while output was still active"), + }) + } if (isActiveReasoning) { controller.enqueue({ type: "reasoning-end", diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 25b8dbc3608f..276ce5f28826 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -970,6 +970,7 @@ const ProviderCapabilities = Schema.Struct({ input: ProviderModalities, output: ProviderModalities, interleaved: ProviderInterleaved, + systemMessage: Schema.optional(Schema.Union([Schema.Literal("single"), Schema.Literal("multiple")])), }) const ProviderCacheCost = Schema.Struct({ diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index c57fcd53c96f..193fe8e27a19 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -711,6 +711,15 @@ export function fromError( }, { cause: e }, ).toObject() + case e instanceof Error && e.message === "SSE read timed out": + return new APIError( + { + message: "Stream read timed out — no data received within the chunk timeout window", + isRetryable: true, + metadata: { code: "SSE_TIMEOUT", message: e.message }, + }, + { cause: e }, + ).toObject() case e instanceof Error: return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject() default: diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 4139665bd2bd..b3039d6e02f6 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -122,14 +122,18 @@ export function retryable(error: Err, provider: string) { return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } } - // Check for rate limit patterns in plain text error messages + // Check for stream interruption and rate limit patterns in plain text error messages const msg = isRecord(error.data) ? error.data.message : undefined if (typeof msg === "string") { const lower = msg.toLowerCase() if ( lower.includes("rate increased too quickly") || lower.includes("rate limit") || - lower.includes("too many requests") + lower.includes("too many requests") || + lower.includes("sse read timed out") || + lower.includes("connection reset") || + lower.includes("aborted") || + lower.includes("stream ended unexpectedly") ) { return { message: msg } } From e6c37d594623e2f36cb13f8e7e157e674b5959bc Mon Sep 17 00:00:00 2001 From: CrytsalTraveler Date: Fri, 5 Jun 2026 14:49:40 +0800 Subject: [PATCH 3/3] build: add --no-orphans to compiled binary execArgv --- packages/opencode/script/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index fd5712333c57..c44ef8bdf8d5 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -180,7 +180,7 @@ for (const item of targets) { autoloadPackageJson: true, target: name.replace(pkg.name, "bun") as any, outfile: `dist/${name}/bin/opencode`, - execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"], + execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--no-orphans", "--"], windows: {}, }, files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},