From 3c9867ad7934d64f3f83b127bff21da297ee896d Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sun, 24 May 2026 13:28:55 -0400 Subject: [PATCH 01/11] fix: await event loop in non-interactive `opencode run` In non-interactive mode, the event processing loop was started with fire-and-forget (Promise.catch but not awaited). When the prompt() call returned its HTTP acknowledgment, execute() returned immediately, and the Effect framework's cleanup disposed the in-process server, killing the SSE stream before the model finished generating. Only the step_start event was emitted (it arrives before prompt() returns). All text, tool_use, and step_finish events were lost because the stream was torn down mid-generation. Fix: store the loop promise and await it after the prompt/command call completes. This keeps the process alive until the session reaches idle and all events have been processed. Tested: `opencode run 'what is 2+2?' --format json` now correctly emits step_start + text + step_finish events (previously only step_start). --- packages/opencode/src/cli/cmd/run.ts | 34 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index b80a2389ef24..1b99cb5d3e54 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -767,10 +767,7 @@ export const RunCommand = effectCmd({ if (!args.interactive) { const events = await client.event.subscribe() - loop(client, events).catch((e) => { - console.error(e) - process.exit(1) - }) + const loopDone = loop(client, events) if (args.command) { const result = await client.session.command({ @@ -785,19 +782,26 @@ export const RunCommand = effectCmd({ if (!emit("error", { error: result.error })) UI.error(formatRunError(result.error)) process.exitCode = 1 } - return + } else { + const model = pick(args.model) + const result = await client.session.prompt({ + sessionID, + agent, + model, + variant: args.variant, + parts: [...files, { type: "text", text: message }], + }) + if (result.error) { + if (!emit("error", { error: result.error })) UI.error(formatRunError(result.error)) + process.exitCode = 1 + } } - const model = pick(args.model) - const result = await client.session.prompt({ - sessionID, - agent, - model, - variant: args.variant, - parts: [...files, { type: "text", text: message }], - }) - if (result.error) { - if (!emit("error", { error: result.error })) UI.error(formatRunError(result.error)) + try { + const loopError = await loopDone + if (loopError) process.exitCode = 1 + } catch (e) { + console.error(e) process.exitCode = 1 } return From e751ac6f55128afb316b637b0b75d1adcbb150e7 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 13:17:30 -0400 Subject: [PATCH 02/11] fix: add 'reasoning' field fallback for Ollama streaming deltas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ollama's openai-compatible endpoint sends reasoning in delta.reasoning (not delta.reasoning_content). The AI SDK already has a fallback for this, but opencode's native LLM protocol (openai-chat.ts) only checked reasoning_content — causing model reasoning to be silently dropped. Fixes: - Delta schema: add reasoning as optional field - Parser: reasoning_content ?? reasoning fallback - Assistant message schema: add reasoning field - lowerAssistantMessage: fallback when reading from providerOptions --- packages/llm/src/protocols/openai-chat.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/llm/src/protocols/openai-chat.ts b/packages/llm/src/protocols/openai-chat.ts index 6a85c37d5935..4631debc8f7e 100644 --- a/packages/llm/src/protocols/openai-chat.ts +++ b/packages/llm/src/protocols/openai-chat.ts @@ -58,6 +58,7 @@ const OpenAIChatMessage = Schema.Union([ content: Schema.NullOr(Schema.String), tool_calls: optionalArray(OpenAIChatAssistantToolCall), reasoning_content: Schema.optional(Schema.String), + reasoning: Schema.optional(Schema.String), }), Schema.Struct({ role: Schema.Literal("tool"), tool_call_id: Schema.String, content: Schema.String }), ]).pipe(Schema.toTaggedUnion("role")) @@ -128,6 +129,7 @@ type OpenAIChatToolCallDelta = Schema.Schema.Type ({ }, }) -const openAICompatibleReasoningContent = (native: unknown) => - isRecord(native) && typeof native.reasoning_content === "string" ? native.reasoning_content : undefined +const openAICompatibleReasoningContent = (native: unknown) => { + if (!isRecord(native)) return undefined + if (typeof native.reasoning_content === "string") return native.reasoning_content + if (typeof native.reasoning === "string") return native.reasoning + return undefined +} const lowerUserMessage = Effect.fn("OpenAIChat.lowerUserMessage")(function* (message: OpenAIChatRequestMessage) { const content: TextPart[] = [] @@ -325,8 +331,9 @@ const step = (state: ParserState, event: OpenAIChatEvent) => let lifecycle = state.lifecycle - if (delta?.reasoning_content) - lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", delta.reasoning_content) + const reasoningText = delta?.reasoning_content ?? delta?.reasoning + if (reasoningText) + lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", reasoningText) if (delta?.content) lifecycle = Lifecycle.textDelta(lifecycle, events, "text-0", delta.content) From 497420a2a97d6c08f3fbd231966bc05f8923a499 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 13:44:42 -0400 Subject: [PATCH 03/11] debug: add interleaved transform logging to diagnose kimi reasoning_content passthrough --- packages/opencode/src/provider/transform.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 08b2f4922104..97e059bc0928 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -310,6 +310,7 @@ function normalizeMessages( model.capabilities.interleaved.field && model.api.npm !== "@openrouter/ai-sdk-provider" ) { + console.log("[interleaved DEBUG] FIRING for model:", model.id, "field:", model.capabilities.interleaved.field) const field = model.capabilities.interleaved.field return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { @@ -337,6 +338,8 @@ function normalizeMessages( return msg }) + } else { + console.log("[interleaved DEBUG] SKIPPED for model:", model.id, "interleaved:", JSON.stringify(model.capabilities.interleaved), "npm:", model.api.npm) } return msgs From a1022a75bd58836c47d541df2794722acb62090e Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 13:49:37 -0400 Subject: [PATCH 04/11] debug: log reasoning parts in messages before AI SDK request --- packages/opencode/src/provider/transform.ts | 3 --- packages/opencode/src/session/llm.ts | 10 +++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 97e059bc0928..08b2f4922104 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -310,7 +310,6 @@ function normalizeMessages( model.capabilities.interleaved.field && model.api.npm !== "@openrouter/ai-sdk-provider" ) { - console.log("[interleaved DEBUG] FIRING for model:", model.id, "field:", model.capabilities.interleaved.field) const field = model.capabilities.interleaved.field return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { @@ -338,8 +337,6 @@ function normalizeMessages( return msg }) - } else { - console.log("[interleaved DEBUG] SKIPPED for model:", model.id, "interleaved:", JSON.stringify(model.capabilities.interleaved), "npm:", model.api.npm) } return msgs diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index ea2efc99d007..874391c9ac62 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -307,7 +307,15 @@ const live: Layer.Layer< abortSignal: input.abort, headers: prepared.headers, maxRetries: input.retries ?? 0, - messages: prepared.messages, + messages: (() => { + for (const m of prepared.messages) { + if (m.role === "assistant" && Array.isArray(m.content)) { + const rp = m.content.filter((p: any) => p.type === "reasoning") + if (rp.length) console.log("[REASONING-PASSTHROUGH] assistant msg has", rp.length, "reasoning parts, total chars:", rp.reduce((s: number, p: any) => s + p.text.length, 0)) + } + } + return prepared.messages + })(), model: wrapLanguageModel({ model: language, middleware: [ From 864435490cc63a318471ece35aeb6236a9b2543c Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 22:27:31 -0400 Subject: [PATCH 05/11] debug: interleaved transform entry/exit logging to diagnose reasoning passthrough --- packages/opencode/src/provider/transform.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 08b2f4922104..4657ee561712 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -311,6 +311,13 @@ function normalizeMessages( model.api.npm !== "@openrouter/ai-sdk-provider" ) { const field = model.capabilities.interleaved.field + // DEBUG: dump reasoning parts in all assistant messages + for (const msg of msgs) { + if (msg.role === "assistant" && Array.isArray(msg.content)) { + const rp = msg.content.filter((p: any) => p.type === "reasoning") + if (rp.length) console.log("[INTERLEAVED] FIRING for", model.id, "| assistant msg has", rp.length, "reasoning parts, total chars:", rp.reduce((s: number, p: any) => s + (p.text?.length || 0), 0), "| setting field:", field) + } + } return msgs.map((msg) => { if (msg.role === "assistant" && Array.isArray(msg.content)) { const reasoningParts = msg.content.filter((part: any) => part.type === "reasoning") @@ -337,6 +344,8 @@ function normalizeMessages( return msg }) + } else { + console.log("[INTERLEAVED] SKIPPED for", model.id, "| interleaved:", model.capabilities.interleaved, "| npm:", model.api.npm) } return msgs From 2a38f810cbc00ed6eeb4bc8c328f56924b2b7247 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 22:30:12 -0400 Subject: [PATCH 06/11] debug: dump raw prompt after transform to /tmp/opencode-prompt-*.json for kimi models --- packages/opencode/src/session/llm.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 874391c9ac62..d7aaab58bdc6 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -307,15 +307,7 @@ const live: Layer.Layer< abortSignal: input.abort, headers: prepared.headers, maxRetries: input.retries ?? 0, - messages: (() => { - for (const m of prepared.messages) { - if (m.role === "assistant" && Array.isArray(m.content)) { - const rp = m.content.filter((p: any) => p.type === "reasoning") - if (rp.length) console.log("[REASONING-PASSTHROUGH] assistant msg has", rp.length, "reasoning parts, total chars:", rp.reduce((s: number, p: any) => s + p.text.length, 0)) - } - } - return prepared.messages - })(), + messages: prepared.messages, model: wrapLanguageModel({ model: language, middleware: [ @@ -329,6 +321,22 @@ const live: Layer.Layer< input.model, prepared.messageTransformOptions, ) + // Dump raw prompt after transform for kimi-k2.7-code + if (input.model.id.includes("kimi-k2")) { + const fs = require("fs") + const dump = args.params.prompt.map((m: any) => ({ + role: m.role, + content: Array.isArray(m.content) + ? m.content.map((p: any) => ({ type: p.type, text: p.text?.slice(0, 200), hasProviderOptions: !!p.providerOptions })) + : m.content?.slice(0, 200), + providerOptions: m.providerOptions ? Object.keys(m.providerOptions) : undefined, + hasReasoningContent: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, + })) + const ts = Date.now() + const path = `/tmp/opencode-prompt-${ts}.json` + fs.writeFileSync(path, JSON.stringify(dump, null, 2)) + console.log("[RAW-PROMPT] wrote to", path) + } } return args.params }, From 46a4f13f77147a1f0f242e4be64009ececae2a06 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 22:32:00 -0400 Subject: [PATCH 07/11] debug: dump raw prompt to console.log for ALL models (not just kimi) --- packages/opencode/src/session/llm.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index d7aaab58bdc6..d683eb1f8d92 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -321,22 +321,15 @@ const live: Layer.Layer< input.model, prepared.messageTransformOptions, ) - // Dump raw prompt after transform for kimi-k2.7-code - if (input.model.id.includes("kimi-k2")) { - const fs = require("fs") - const dump = args.params.prompt.map((m: any) => ({ - role: m.role, - content: Array.isArray(m.content) - ? m.content.map((p: any) => ({ type: p.type, text: p.text?.slice(0, 200), hasProviderOptions: !!p.providerOptions })) - : m.content?.slice(0, 200), - providerOptions: m.providerOptions ? Object.keys(m.providerOptions) : undefined, - hasReasoningContent: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, - })) - const ts = Date.now() - const path = `/tmp/opencode-prompt-${ts}.json` - fs.writeFileSync(path, JSON.stringify(dump, null, 2)) - console.log("[RAW-PROMPT] wrote to", path) - } + // Dump raw prompt after transform + console.log("[RAW-PROMPT]", JSON.stringify(args.params.prompt.map((m: any) => ({ + role: m.role, + contentLen: Array.isArray(m.content) ? m.content.length : (m.content?.length || 0), + hasReasoningPart: Array.isArray(m.content) && m.content.some((p: any) => p.type === "reasoning"), + providerKeys: m.providerOptions ? Object.keys(m.providerOptions) : [], + hasRc: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, + rcPreview: ((m.providerOptions as any)?.openaiCompatible?.reasoning_content || "").slice(0, 80), + })))) } return args.params }, From 82a6067ae5e9871ee424f7a76970e4899154df00 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 22:34:48 -0400 Subject: [PATCH 08/11] debug: use Bun.write to dump raw prompt to /tmp/opencode-prompt-*.json --- packages/opencode/src/session/llm.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index d683eb1f8d92..d1edac2d8552 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -321,15 +321,20 @@ const live: Layer.Layer< input.model, prepared.messageTransformOptions, ) - // Dump raw prompt after transform - console.log("[RAW-PROMPT]", JSON.stringify(args.params.prompt.map((m: any) => ({ - role: m.role, - contentLen: Array.isArray(m.content) ? m.content.length : (m.content?.length || 0), - hasReasoningPart: Array.isArray(m.content) && m.content.some((p: any) => p.type === "reasoning"), - providerKeys: m.providerOptions ? Object.keys(m.providerOptions) : [], - hasRc: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, - rcPreview: ((m.providerOptions as any)?.openaiCompatible?.reasoning_content || "").slice(0, 80), - })))) + // Dump raw prompt to /tmp for inspection + try { + const dump = args.params.prompt.map((m: any) => ({ + role: m.role, + contentLen: Array.isArray(m.content) ? m.content.length : (m.content?.length || 0), + hasReasoningPart: Array.isArray(m.content) && m.content.some((p: any) => p.type === "reasoning"), + providerKeys: m.providerOptions ? Object.keys(m.providerOptions) : [], + hasRc: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, + rcPreview: ((m.providerOptions as any)?.openaiCompatible?.reasoning_content || "").slice(0, 80), + })) + const ts = Date.now() + const path = `/tmp/opencode-prompt-${ts}.json` + await Bun.write(path, JSON.stringify(dump, null, 2)) + } catch(e) {} } return args.params }, From 404836ea64182fa6e6f26a992354806f04fd8668 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 22:35:40 -0400 Subject: [PATCH 09/11] debug: use Bun.writeFileSync for prompt dump --- packages/opencode/src/session/llm.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index d1edac2d8552..82eb001fed7c 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -321,20 +321,18 @@ const live: Layer.Layer< input.model, prepared.messageTransformOptions, ) - // Dump raw prompt to /tmp for inspection - try { - const dump = args.params.prompt.map((m: any) => ({ - role: m.role, - contentLen: Array.isArray(m.content) ? m.content.length : (m.content?.length || 0), - hasReasoningPart: Array.isArray(m.content) && m.content.some((p: any) => p.type === "reasoning"), - providerKeys: m.providerOptions ? Object.keys(m.providerOptions) : [], - hasRc: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, - rcPreview: ((m.providerOptions as any)?.openaiCompatible?.reasoning_content || "").slice(0, 80), - })) - const ts = Date.now() - const path = `/tmp/opencode-prompt-${ts}.json` - await Bun.write(path, JSON.stringify(dump, null, 2)) - } catch(e) {} + // Dump raw prompt to /tmp + const dump = args.params.prompt.map((m: any) => ({ + role: m.role, + contentLen: Array.isArray(m.content) ? m.content.length : (m.content?.length || 0), + hasReasoningPart: Array.isArray(m.content) && m.content.some((p: any) => p.type === "reasoning"), + providerKeys: m.providerOptions ? Object.keys(m.providerOptions) : [], + hasRc: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, + rcPreview: ((m.providerOptions as any)?.openaiCompatible?.reasoning_content || "").slice(0, 80), + })) + const ts = Date.now() + const path = `/tmp/opencode-prompt-${ts}.json` + Bun.writeFileSync(path, JSON.stringify(dump, null, 2)) } return args.params }, From dbe96d262d713aed04cb95424c938eeee3b12152 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 22:36:17 -0400 Subject: [PATCH 10/11] debug: use Bun.write for prompt dump --- packages/opencode/src/session/llm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 82eb001fed7c..cb7a8fc2fc00 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -332,7 +332,7 @@ const live: Layer.Layer< })) const ts = Date.now() const path = `/tmp/opencode-prompt-${ts}.json` - Bun.writeFileSync(path, JSON.stringify(dump, null, 2)) + Bun.write(`/tmp/opencode-prompt-${Date.now()}.json`, JSON.stringify(dump, null, 2)) } return args.params }, From 0401bffaa74aceaeb5e5a2146e848d51db5e0256 Mon Sep 17 00:00:00 2001 From: William Hardesty Date: Sat, 13 Jun 2026 22:36:59 -0400 Subject: [PATCH 11/11] debug: use console.error for prompt dump (simplest) --- packages/opencode/src/session/llm.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index cb7a8fc2fc00..8069ccda8f5d 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -321,18 +321,14 @@ const live: Layer.Layer< input.model, prepared.messageTransformOptions, ) - // Dump raw prompt to /tmp - const dump = args.params.prompt.map((m: any) => ({ + // Dump raw prompt to stderr + const dump = JSON.stringify(args.params.prompt.map((m: any) => ({ role: m.role, - contentLen: Array.isArray(m.content) ? m.content.length : (m.content?.length || 0), - hasReasoningPart: Array.isArray(m.content) && m.content.some((p: any) => p.type === "reasoning"), - providerKeys: m.providerOptions ? Object.keys(m.providerOptions) : [], - hasRc: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, - rcPreview: ((m.providerOptions as any)?.openaiCompatible?.reasoning_content || "").slice(0, 80), - })) - const ts = Date.now() - const path = `/tmp/opencode-prompt-${ts}.json` - Bun.write(`/tmp/opencode-prompt-${Date.now()}.json`, JSON.stringify(dump, null, 2)) + cl: Array.isArray(m.content) ? m.content.length : String(m.content||"").length, + rp: Array.isArray(m.content) && m.content.some((p: any) => p.type === "reasoning"), + rc: !!(m.providerOptions as any)?.openaiCompatible?.reasoning_content, + }))) + console.error("[RAW-PROMPT]", dump) } return args.params },