From 2d2be67686a975f47f936ab5b872191259828dbf Mon Sep 17 00:00:00 2001 From: bornmw Date: Sun, 14 Jun 2026 23:45:10 -0400 Subject: [PATCH 1/4] feat(llm): add configurable log levels for message logging Replace experimental.log_messages boolean with a string union ('info' | 'debug' | 'trace') that controls verbosity: - 'info': messages + response text (logInfo) - 'debug': adds generation params (logDebug) - 'trace': adds raw provider-native request body (logDebug) --- packages/core/src/v1/config/config.ts | 6 ++ packages/llm/src/route/client.ts | 22 ++++- packages/llm/src/route/index.ts | 1 + packages/llm/src/route/message-logger.ts | 65 +++++++++++++ packages/llm/test/message-logger.test.ts | 91 +++++++++++++++++++ packages/opencode/src/session/llm.ts | 1 + .../src/session/llm/native-runtime.ts | 2 + 7 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 packages/llm/src/route/message-logger.ts create mode 100644 packages/llm/test/message-logger.test.ts diff --git a/packages/core/src/v1/config/config.ts b/packages/core/src/v1/config/config.ts index 2e773f71e256..c3567bef95be 100644 --- a/packages/core/src/v1/config/config.ts +++ b/packages/core/src/v1/config/config.ts @@ -170,6 +170,12 @@ export const Info = Schema.Struct({ openTelemetry: Schema.optional(Schema.Boolean).annotate({ description: "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)", }), + log_messages: Schema.optional( + Schema.Literals(["info", "debug", "trace"]), + ).annotate({ + description: + "'info' logs messages and response text; 'debug' adds generation params; 'trace' adds the raw provider-native request body", + }), primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({ description: "Tools that should only be available to primary agents.", }), diff --git a/packages/llm/src/route/client.ts b/packages/llm/src/route/client.ts index 5b5bc5ab2d5a..17b603423e03 100644 --- a/packages/llm/src/route/client.ts +++ b/packages/llm/src/route/client.ts @@ -8,6 +8,8 @@ import { HttpTransport } from "./transport" import type { Transport, TransportRuntime } from "./transport" import { WebSocketExecutor } from "./transport" import type { Protocol } from "./protocol" +import { logEvents as logResponseEvents, logRequest as logOutgoingRequest } from "./message-logger" +import type { LogLevel } from "./message-logger" import { applyCachePolicy } from "../cache-policy" import * as ProviderShared from "../protocols/shared" import type { LLMError, LLMEvent, PreparedRequestOf, ProtocolID, ProviderOptions } from "../schema" @@ -343,11 +345,17 @@ const compile = Effect.fn("LLM.compile")(function* (request: LLMRequest) { .pipe(Effect.flatMap(ProviderShared.validateWith(Schema.decodeUnknownEffect(route.body.schema)))) const prepared = yield* route.prepareTransport(body, resolved) + const logMessages = request.metadata?.logMessages + if (logMessages) { + yield* logOutgoingRequest(request, logMessages as LogLevel, body) + } + return { request: resolved, route, body, prepared, + logMessages, } }) @@ -368,13 +376,19 @@ const streamRequestWith = (runtime: TransportRuntime) => (request: LLMRequest) = Stream.unwrap( Effect.gen(function* () { const compiled = yield* compile(request) - return compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime) + const events = compiled.route.streamPrepared(compiled.prepared, compiled.request, runtime) + const logMessages = request.metadata?.logMessages as LogLevel | undefined + if (!logMessages) return events + return events.pipe( + Stream.tap((event) => logResponseEvents(request, [event], logMessages)), + ) }), ) const generateWith = (stream: Interface["stream"]) => Effect.fn("LLM.generate")(function* (request: LLMRequest) { - return new LLMResponse( + const logMessages = request.metadata?.logMessages as LogLevel | undefined + const response = new LLMResponse( yield* stream(request).pipe( Stream.runFold( () => ({ events: [] as LLMEvent[], usage: undefined as LLMResponse["usage"] }), @@ -386,6 +400,10 @@ const generateWith = (stream: Interface["stream"]) => ), ), ) + if (logMessages) { + yield* logResponseEvents(request, response.events, logMessages) + } + return response }) export const prepare = (request: LLMRequest) => diff --git a/packages/llm/src/route/index.ts b/packages/llm/src/route/index.ts index 48f4b7bc3392..773495aa9ebe 100644 --- a/packages/llm/src/route/index.ts +++ b/packages/llm/src/route/index.ts @@ -10,6 +10,7 @@ export type { Service as LLMClientService, } from "./client" export * from "./executor" +export { MessageLogger } from "./message-logger" export { Auth } from "./auth" export { AuthOptions } from "./auth-options" export { Endpoint } from "./endpoint" diff --git a/packages/llm/src/route/message-logger.ts b/packages/llm/src/route/message-logger.ts new file mode 100644 index 000000000000..4545118b87c1 --- /dev/null +++ b/packages/llm/src/route/message-logger.ts @@ -0,0 +1,65 @@ +import { Effect } from "effect" +import type { LLMEvent, LLMRequest } from "../schema" + +export type LogLevel = "info" | "debug" | "trace" + +export const formatMessages = (request: LLMRequest): string => { + const parts: Array = [] + for (const part of request.system) { + if (part.type === "text") parts.push(`system: ${part.text}`) + } + for (const message of request.messages) { + const texts: Array = [] + for (const part of message.content) { + if (part.type === "text") texts.push(part.text) + if (part.type === "tool-call") texts.push(`tool-call(${part.name}): ${JSON.stringify(part.input)}`) + if (part.type === "tool-result") texts.push(`tool-result(${part.name}): ${JSON.stringify(part.result)}`) + } + parts.push(`${message.role}: ${texts.join("\n")}`) + } + return parts.join("\n") +} + +export const formatEvents = (events: ReadonlyArray): string => { + const texts: Array = [] + for (const event of events) { + if (event.type === "text-delta") texts.push(event.text) + if (event.type === "reasoning-delta") texts.push(`[reasoning]: ${event.text}`) + if (event.type === "tool-call") texts.push(`tool-call(${event.name}): ${JSON.stringify(event.input)}`) + if (event.type === "tool-result") texts.push(`tool-result(${event.name}): ${JSON.stringify(event.result)}`) + if (event.type === "finish" && event.usage) { + texts.push(`usage: ${JSON.stringify(event.usage)}`) + } + } + return texts.join("") +} + +const logAtLevel = (level: LogLevel, label: string, data: Record): Effect.Effect => { + switch (level) { + case "info": return Effect.logInfo(label, data) + case "debug": return Effect.logDebug(label, data) + case "trace": return Effect.logDebug(label, data) + } +} + +export const logRequest = (request: LLMRequest, level: LogLevel, body?: unknown): Effect.Effect => { + const model = `${request.model.provider}/${request.model.id}` + const payload: Record = { model, messages: formatMessages(request) } + if (level !== "info" && request.generation) { + payload.generation = Object.fromEntries( + Object.entries(request.generation).filter(([, value]) => value !== undefined), + ) + } + if (level === "trace" && body !== undefined) { + payload.body = JSON.stringify(body) + } + return logAtLevel(level, "LLM request", payload) +} + +export const logEvents = (request: LLMRequest, events: ReadonlyArray, level: LogLevel): Effect.Effect => + logAtLevel(level, "LLM response", { + model: `${request.model.provider}/${request.model.id}`, + response: formatEvents(events), + }) + +export * as MessageLogger from "./message-logger" diff --git a/packages/llm/test/message-logger.test.ts b/packages/llm/test/message-logger.test.ts new file mode 100644 index 000000000000..6cba777fe1dd --- /dev/null +++ b/packages/llm/test/message-logger.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test } from "bun:test" +import { Effect, Layer, Logger, LogLevel } from "effect" +import { LLMClient, MessageLogger } from "../src/route" +import * as OpenAIChat from "../src/protocols/openai-chat" +import { LLM, LLMRequest, Message, Model } from "../src" +import { dynamicResponse } from "./lib/http" +import { deltaChunk, finishChunk } from "./lib/openai-chunks" +import { sseRaw } from "./lib/sse" +import { it } from "./lib/effect" + +const chatRoute = OpenAIChat.route.with({ endpoint: { baseURL: "https://api.openai.test/v1" } }) +const model = Model.make({ id: "gpt-4o-mini", provider: "openai", route: chatRoute }) + +describe("MessageLogger", () => { + describe("formatMessages", () => { + test("formats system and user messages", () => { + const request = LLM.request({ + model, + system: "You are helpful.", + prompt: "Say hello.", + }) + const formatted = MessageLogger.formatMessages(request) + expect(formatted).toContain("system: You are helpful.") + expect(formatted).toContain("user: Say hello.") + }) + + test("formats messages with tool calls and results", () => { + const request = LLM.request({ + model, + messages: [ + Message.user("Check weather"), + Message.assistant([ + { type: "tool-call", id: "call_1", name: "get_weather", input: { city: "Tokyo" } }, + ]), + Message.tool({ id: "call_1", name: "get_weather", result: { temperature: 72 } }), + ], + }) + const formatted = MessageLogger.formatMessages(request) + expect(formatted).toContain('tool-call(get_weather): {"city":"Tokyo"}') + expect(formatted).toContain('tool-result(get_weather): {"type":"json","value":{"temperature":72}}') + }) + }) + + describe("formatEvents", () => { + test("formats text deltas and usage", () => { + const formatted = MessageLogger.formatEvents([ + { type: "text-delta", id: "text-0", text: "Hello" }, + { type: "text-delta", id: "text-0", text: " world" }, + { type: "finish", reason: "stop", usage: { inputTokens: 10, outputTokens: 5, visibleOutputTokens: 3 } }, + ]) + expect(formatted).toBe("Hello worldusage: {\"inputTokens\":10,\"outputTokens\":5,\"visibleOutputTokens\":3}") + }) + + test("formats reasoning deltas", () => { + const formatted = MessageLogger.formatEvents([ + { type: "reasoning-delta", id: "reason-0", text: "thinking step" }, + ]) + expect(formatted).toContain("[reasoning]: thinking step") + }) + + test("formats tool call events", () => { + const formatted = MessageLogger.formatEvents([ + { type: "tool-call", id: "call_1", name: "lookup", input: { query: "weather" } }, + ]) + expect(formatted).toContain('tool-call(lookup): {"query":"weather"}') + }) + }) + + describe("LLMClient integration", () => { + const helloResponse = sseRaw( + `data: ${JSON.stringify(deltaChunk({ role: "assistant", content: "Hello" }))}`, + `data: ${JSON.stringify(finishChunk("stop"))}`, + ) + + it.effect("does not log when metadata.logMessages is not set", () => + Effect.gen(function* () { + const result = yield* LLMClient.generate(LLM.request({ model, prompt: "Say hello." })) + expect(result.text).toBe("Hello") + }).pipe(Effect.provide(dynamicResponse((input) => Effect.succeed(input.respond(helloResponse))))), + ) + + it.effect("logs request when metadata.logMessages is set to info", () => + Effect.gen(function* () { + const result = yield* LLMClient.generate( + LLM.request({ model, prompt: "Say hello.", metadata: { logMessages: "info" as const } }), + ) + expect(result.text).toBe("Hello") + }).pipe(Effect.provide(dynamicResponse((input) => Effect.succeed(input.respond(helloResponse))))), + ) + }) +}) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index adacfc431549..2f09c954fbdb 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -239,6 +239,7 @@ const live: Layer.Layer< providerOptions: prepared.params.options, headers: prepared.headers, abort: input.abort, + logMessages: cfg.experimental?.log_messages, }) if (native.type === "supported") { yield* Effect.logInfo("llm runtime selected", { diff --git a/packages/opencode/src/session/llm/native-runtime.ts b/packages/opencode/src/session/llm/native-runtime.ts index bac385c59137..918e2dbdc313 100644 --- a/packages/opencode/src/session/llm/native-runtime.ts +++ b/packages/opencode/src/session/llm/native-runtime.ts @@ -41,6 +41,7 @@ type StreamInput = { readonly providerOptions?: Record readonly headers: Record readonly abort: AbortSignal + readonly logMessages?: string } export function status(input: Pick): RuntimeStatus { @@ -109,6 +110,7 @@ export function stream(input: StreamInput): StreamResult { .stream( LLMRequest.update(request, { tools: [...request.tools, ...toDefinitions(tools)], + metadata: input.logMessages ? { ...request.metadata, logMessages: input.logMessages } : request.metadata, }), ) .pipe( From e0d8d8adc7935b48ae6fe2bd73e8830dc4454ac8 Mon Sep 17 00:00:00 2001 From: bornmw Date: Sun, 14 Jun 2026 08:33:54 -0400 Subject: [PATCH 2/4] feat(tui): add linux_clipboard_selection config for primary buffer support --- packages/tui/src/app.tsx | 2 +- packages/tui/src/clipboard.ts | 41 +++++++++++++++++++------- packages/tui/src/config/index.tsx | 11 ++++++- packages/tui/src/context/clipboard.tsx | 9 ++++-- packages/tui/test/clipboard.test.ts | 6 ++++ 5 files changed, 54 insertions(+), 15 deletions(-) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 17a9a554c2e4..d087c5fa3d92 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -268,7 +268,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { skipInitialLoading: Boolean(process.env.OPENCODE_FAST_BOOT), }} > - + diff --git a/packages/tui/src/clipboard.ts b/packages/tui/src/clipboard.ts index 08f86f9f7a97..d0e14959aa33 100644 --- a/packages/tui/src/clipboard.ts +++ b/packages/tui/src/clipboard.ts @@ -73,15 +73,18 @@ export async function read() { if (text) return { data: text, mime: "text/plain" } } +export type ClipboardSelection = "clipboard" | "primary" + export function copyCommand( os: NodeJS.Platform, wayland: boolean, has: (name: string) => boolean, + selection: ClipboardSelection = "clipboard", ): string[] | undefined { if (os === "darwin" && has("osascript")) return ["osascript"] - if (os === "linux" && wayland && has("wl-copy")) return ["wl-copy"] - if (os === "linux" && has("xclip")) return ["xclip", "-selection", "clipboard"] - if (os === "linux" && has("xsel")) return ["xsel", "--clipboard", "--input"] + if (os === "linux" && wayland && has("wl-copy")) return selection === "primary" ? ["wl-copy", "-p"] : ["wl-copy"] + if (os === "linux" && has("xclip")) return ["xclip", "-selection", selection] + if (os === "linux" && has("xsel")) return selection === "primary" ? ["xsel", "--primary", "--input"] : ["xsel", "--clipboard", "--input"] if (os === "win32" && has("powershell.exe")) { return [ "powershell.exe", @@ -93,32 +96,48 @@ export function copyCommand( } } -let copyMethod: Promise<(text: string) => Promise> | undefined +let copyMethod: Promise<((text: string, selection?: "clipboard" | "primary" | "both") => Promise)> | undefined function getCopyMethod() { return (copyMethod ??= (async () => { const { which } = await import("@opencode-ai/core/util/which") - const native = copyCommand(platform(), Boolean(process.env.WAYLAND_DISPLAY), (name) => Boolean(which(name))) + const os = platform() + const wayland = Boolean(process.env.WAYLAND_DISPLAY) + const has = (name: string) => Boolean(which(name)) + + const clipboardCmd = copyCommand(os, wayland, has, "clipboard") + const primaryCmd = copyCommand(os, wayland, has, "primary") + const native = clipboardCmd + if (native?.[0] === "osascript") { - return async (text: string) => { + return async (text: string, selection?: "clipboard" | "primary" | "both") => { const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') await command("osascript", ["-e", `set the clipboard to "${escaped}"`]).catch(() => undefined) } } if (native) { - return async (text: string) => { - await command(native[0], native.slice(1), text).catch(() => undefined) + return async (text: string, selection?: "clipboard" | "primary" | "both") => { + if (selection === "both" && primaryCmd) { + await Promise.allSettled([ + command(native[0], native.slice(1), text), + command(primaryCmd[0], primaryCmd.slice(1), text), + ]) + } else if (selection === "primary" && primaryCmd) { + await command(primaryCmd[0], primaryCmd.slice(1), text).catch(() => undefined) + } else { + await command(native[0], native.slice(1), text).catch(() => undefined) + } } } - return async (text: string) => { + return async (text: string, selection?: "clipboard" | "primary" | "both") => { const { default: clipboardy } = await import("clipboardy") await clipboardy.write(text).catch(() => undefined) } })()) } -export async function write(text: string) { +export async function write(text: string, selection?: "clipboard" | "primary" | "both") { writeOsc52(text) const method = await getCopyMethod() - await method(text) + await method(text, selection) } diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index df9239763a68..c5a073b9aa65 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -30,6 +30,10 @@ export const ScrollAcceleration = Schema.Struct({ export const DiffStyle = Schema.Literals(["auto", "stacked"]).annotate({ description: "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", }) +export const LinuxClipboardSelection = Schema.Literals(["clipboard", "primary", "both"]).annotate({ + description: + "Linux clipboard selection: 'clipboard' (Ctrl+C), 'primary' (middle-click), or 'both' (default: 'both')", +}) export const AttentionSounds = Schema.Record(AttentionSoundName, Schema.optionalKey(Schema.String)) export type AttentionSoundPaths = Schema.Schema.Type @@ -63,10 +67,13 @@ export const Info = Schema.Struct({ scroll_acceleration: Schema.optional(ScrollAcceleration), diff_style: Schema.optional(DiffStyle), mouse: Schema.optional(Schema.Boolean).annotate({ description: "Enable or disable mouse capture (default: true)" }), + linux_clipboard_selection: Schema.optional(LinuxClipboardSelection).annotate({ + description: "Linux clipboard selection: 'clipboard' (Ctrl+C), 'primary' (middle-click), or 'both' (default: 'both')", + }), }) export type Info = Schema.Schema.Type -export type Resolved = Omit & { +export type Resolved = Omit & { attention: { enabled: boolean notifications: boolean @@ -78,6 +85,7 @@ export type Resolved = Omit export type ClipboardService = Readonly<{ read?(): Promise @@ -9,8 +11,11 @@ export type ClipboardService = Readonly<{ const clipboard = { read, write } const ClipboardContext = createContext(clipboard) -export function ClipboardProvider(props: { value?: ClipboardService; children: JSX.Element }) { - return {props.children} +export function ClipboardProvider(props: { value?: ClipboardService; children: JSX.Element; linuxClipboardSelection?: ClipboardConfig }) { + const clipboardWithSelection = props.value ?? (props.linuxClipboardSelection + ? { read, write: (text: string) => write(text, props.linuxClipboardSelection!) } + : clipboard) + return {props.children} } export function useClipboard() { diff --git a/packages/tui/test/clipboard.test.ts b/packages/tui/test/clipboard.test.ts index f2d4994c7e2a..eb1d984f17f0 100644 --- a/packages/tui/test/clipboard.test.ts +++ b/packages/tui/test/clipboard.test.ts @@ -3,6 +3,7 @@ import { copyCommand } from "../src/clipboard" test("prefers Wayland clipboard when available", () => { expect(copyCommand("linux", true, (name) => name === "wl-copy")).toEqual(["wl-copy"]) + expect(copyCommand("linux", true, (name) => name === "wl-copy", "primary")).toEqual(["wl-copy", "-p"]) }) test("uses osascript on macOS", () => { @@ -14,6 +15,11 @@ test("falls back through X11 clipboard commands", () => { expect(copyCommand("linux", false, (name) => name === "xsel")).toEqual(["xsel", "--clipboard", "--input"]) }) +test("uses primary selection with xclip and xsel", () => { + expect(copyCommand("linux", true, (name) => name === "xclip", "primary")).toEqual(["xclip", "-selection", "primary"]) + expect(copyCommand("linux", false, (name) => name === "xsel", "primary")).toEqual(["xsel", "--primary", "--input"]) +}) + test("returns undefined when native clipboard is unavailable", () => { expect(copyCommand("linux", false, () => false)).toBeUndefined() }) From aced873673ee011092c17cd998e506db78644caf Mon Sep 17 00:00:00 2001 From: bornmw Date: Mon, 15 Jun 2026 00:37:27 -0400 Subject: [PATCH 3/4] fix(llm): add message logging to default AI SDK runtime The experimental.log_messages config was only wired into the native LLM runtime path. The default AI SDK runtime had no logging support, so the setting appeared broken for most users. - Log request (model, messages, generation params) before streamText - Log response events via Stream.tap in the LLMEvent pipeline - Reuse MessageLogger.formatEvents for response formatting --- packages/opencode/src/session/llm.ts | 42 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 2f09c954fbdb..6d2fb25618ef 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -8,7 +8,7 @@ import { Context, Effect, Layer } from "effect" import * as Stream from "effect/Stream" import { streamText, wrapLanguageModel, type ModelMessage, type Tool } from "ai" import type { LLMEvent } from "@opencode-ai/llm" -import { LLMClient, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" +import { LLMClient, MessageLogger, RequestExecutor, WebSocketExecutor } from "@opencode-ai/llm/route" import type { LLMClientService } from "@opencode-ai/llm/route" import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" import { ProviderTransform } from "@/provider/transform" @@ -269,6 +269,26 @@ const live: Layer.Layer< }) } + const logMessages = cfg.experimental?.log_messages as MessageLogger.LogLevel | undefined + if (logMessages) { + const model = `${input.model.providerID}/${input.model.id}` + const texts: Array = [] + for (const s of prepared.system) texts.push(`system: ${s}`) + for (const m of prepared.messages) { + const content = typeof m.content === "string" + ? m.content + : m.content?.map((p) => (p.type === "text" ? p.text : `[${p.type}]`)).join("\n") ?? "" + texts.push(`${m.role}: ${content}`) + } + const payload: Record = { model, messages: texts.join("\n") } + if (logMessages !== "info") { + payload.generation = Object.fromEntries( + Object.entries(prepared.params).filter(([, v]) => v !== undefined) as Array<[string, unknown]>, + ) + } + yield* logMessages === "info" ? Effect.logInfo("LLM request", payload) : Effect.logDebug("LLM request", payload) + } + yield* Effect.logInfo("llm runtime selected", { "llm.runtime": "ai-sdk", "llm.provider": input.model.providerID, @@ -278,6 +298,7 @@ const live: Layer.Layer< // LLMAISDK.toLLMEvents below normalizes fullStream parts for the processor. return { type: "ai-sdk" as const, + logMessages, result: streamText({ onError(error) { bridge.fork( @@ -371,12 +392,29 @@ const live: Layer.Layer< // Adapter seam: both runtimes expose the same LLMEvent stream. Native // already returns one; AI SDK streams are converted here. const state = LLMAISDK.adapterState() - return Stream.fromAsyncIterable(result.result.fullStream, (e) => + const model = `${input.model.providerID}/${input.model.id}` + let events = Stream.fromAsyncIterable(result.result.fullStream, (e) => e instanceof Error ? e : new Error(String(e)), ).pipe( Stream.mapEffect((event) => LLMAISDK.toLLMEvents(state, event)), Stream.flatMap((events) => Stream.fromIterable(events)), ) + if (result.logMessages) { + events = events.pipe( + Stream.tap((event) => + result.logMessages === "info" + ? Effect.logInfo("LLM response", { + model, + response: MessageLogger.formatEvents([event]), + }) + : Effect.logDebug("LLM response", { + model, + response: MessageLogger.formatEvents([event]), + }), + ), + ) + } + return events }), ), ) From 119ebba96230164232a8b0498cf8503264ae1772 Mon Sep 17 00:00:00 2001 From: bornmw Date: Wed, 24 Jun 2026 00:38:21 -0400 Subject: [PATCH 4/4] fix(llm): log at INFO level regardless of log_messages verbosity --- packages/opencode/src/session/llm.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index 6d2fb25618ef..1dc03759be31 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -286,7 +286,7 @@ const live: Layer.Layer< Object.entries(prepared.params).filter(([, v]) => v !== undefined) as Array<[string, unknown]>, ) } - yield* logMessages === "info" ? Effect.logInfo("LLM request", payload) : Effect.logDebug("LLM request", payload) + yield* Effect.logInfo("LLM request", payload) } yield* Effect.logInfo("llm runtime selected", { @@ -402,15 +402,10 @@ const live: Layer.Layer< if (result.logMessages) { events = events.pipe( Stream.tap((event) => - result.logMessages === "info" - ? Effect.logInfo("LLM response", { - model, - response: MessageLogger.formatEvents([event]), - }) - : Effect.logDebug("LLM response", { - model, - response: MessageLogger.formatEvents([event]), - }), + Effect.logInfo("LLM response", { + model, + response: MessageLogger.formatEvents([event]), + }), ), ) }