feat(opencode): convert native LLM message history

This commit is contained in:
Kit Langton
2026-04-26 20:02:13 -04:00
parent 3a94622e76
commit fa2a5d1fdb
2 changed files with 210 additions and 6 deletions
+75 -6
View File
@@ -1,5 +1,5 @@
import * as LLMCore from "@opencode-ai/llm/llm"
import type { Message as CoreMessage } from "@opencode-ai/llm/schema"
import type { ContentPart, Message as CoreMessage } from "@opencode-ai/llm/schema"
import { Effect, Schema } from "effect"
import { ProviderLLMBridge } from "@/provider/llm-bridge"
import { ProviderTransform } from "@/provider"
@@ -27,6 +27,7 @@ export type RequestInput = {
readonly system?: ReadonlyArray<string>
readonly messages: ReadonlyArray<MessageV2.WithParts>
readonly tools?: ReadonlyArray<Tool.Def>
readonly toolChoice?: LLMCore.RequestInput["toolChoice"]
readonly generation?: LLMCore.RequestInput["generation"]
readonly metadata?: Record<string, unknown>
readonly native?: Record<string, unknown>
@@ -37,17 +38,84 @@ const isDefined = <T>(value: T | undefined): value is T => value !== undefined
const textContent = (message: MessageV2.WithParts) =>
message.parts.flatMap((part) => (part.type === "text" && !part.ignored ? [LLMCore.text(part.text)] : []))
const message = (input: MessageV2.WithParts): CoreMessage | undefined => {
const providerMeta = (metadata: Record<string, unknown> | undefined) => {
if (!metadata) return undefined
const { providerExecuted: _, ...rest } = metadata
return Object.keys(rest).length > 0 ? rest : undefined
}
const toolResultValue = (part: MessageV2.ToolPart) => {
if (part.state.status === "completed") {
return {
type: "text" as const,
value: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
}
}
if (part.state.status === "error") {
const output = part.state.metadata?.interrupted === true ? part.state.metadata.output : undefined
if (typeof output === "string") return { type: "text" as const, value: output }
return { type: "error" as const, value: part.state.error }
}
return { type: "error" as const, value: "[Tool execution was interrupted]" }
}
const assistantMessages = (input: MessageV2.WithParts) => {
const content: ContentPart[] = []
const results: CoreMessage[] = []
for (const part of input.parts) {
if (part.type === "text" && !part.ignored) content.push(LLMCore.text(part.text))
if (part.type === "reasoning") content.push({ type: "reasoning", text: part.text, metadata: part.metadata })
if (part.type === "tool") {
const metadata = providerMeta(part.metadata)
content.push(
LLMCore.toolCall({
id: part.callID,
name: part.tool,
input: part.state.input,
providerExecuted: part.metadata?.providerExecuted === true ? true : undefined,
metadata,
}),
)
results.push(
LLMCore.toolMessage({
id: part.callID,
name: part.tool,
result: toolResultValue(part),
providerExecuted: part.metadata?.providerExecuted === true ? true : undefined,
metadata,
}),
)
}
}
return [
content.length === 0
? undefined
: LLMCore.message({
id: input.info.id,
role: "assistant",
content,
native: {
opencodeMessageID: input.info.id,
},
}),
...results,
].filter(isDefined)
}
const message = (input: MessageV2.WithParts): ReadonlyArray<CoreMessage> => {
if (input.info.role === "assistant") return assistantMessages(input)
const content = textContent(input)
if (content.length === 0) return undefined
return LLMCore.message({
if (content.length === 0) return []
return [LLMCore.message({
id: input.info.id,
role: input.info.role,
content,
native: {
opencodeMessageID: input.info.id,
},
})
})]
}
export const toolDefinition = (input: { readonly model: Provider.Model; readonly tool: Tool.Def }) =>
@@ -75,8 +143,9 @@ export const request = Effect.fn("LLMNative.request")(function* (input: RequestI
id: input.id,
model,
system: input.system?.filter((part) => part.trim() !== "").map(LLMCore.system) ?? [],
messages: input.messages.map(message).filter(isDefined),
messages: input.messages.flatMap(message),
tools: input.tools?.map((tool) => toolDefinition({ model: input.model, tool })) ?? [],
toolChoice: input.toolChoice,
generation: input.generation,
metadata: input.metadata,
native: {