feat(opencode): add native LLM request builder

This commit is contained in:
Kit Langton
2026-04-26 19:43:23 -04:00
parent 769d6123d5
commit 0da7d8a2a1
2 changed files with 180 additions and 0 deletions
@@ -0,0 +1,74 @@
import * as LLMCore from "@opencode-ai/llm/llm"
import type { Message as CoreMessage } from "@opencode-ai/llm/schema"
import { Effect, Schema } from "effect"
import { ProviderLLMBridge } from "@/provider/llm-bridge"
import type { Provider } from "@/provider"
import type { MessageV2 } from "./message-v2"
export class UnsupportedModelError extends Schema.TaggedErrorClass<UnsupportedModelError>()(
"LLMNative.UnsupportedModelError",
{
providerID: Schema.String,
modelID: Schema.String,
},
) {
override get message() {
return `No native LLM route for ${this.providerID}/${this.modelID}`
}
}
export type RequestInput = {
readonly id?: string
readonly provider: Provider.Info
readonly model: Provider.Model
readonly system?: ReadonlyArray<string>
readonly messages: ReadonlyArray<MessageV2.WithParts>
readonly generation?: LLMCore.RequestInput["generation"]
readonly metadata?: Record<string, unknown>
readonly native?: Record<string, unknown>
}
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 content = textContent(input)
if (content.length === 0) return undefined
return LLMCore.message({
id: input.info.id,
role: input.info.role,
content,
native: {
opencodeMessageID: input.info.id,
},
})
}
export const request = Effect.fn("LLMNative.request")(function* (input: RequestInput) {
const model = ProviderLLMBridge.toModelRef({ provider: input.provider, model: input.model })
if (!model) {
return yield* new UnsupportedModelError({
providerID: input.provider.id,
modelID: input.model.id,
})
}
return LLMCore.request({
id: input.id,
model,
system: input.system?.filter((part) => part.trim() !== "").map(LLMCore.system) ?? [],
messages: input.messages.map(message).filter(isDefined),
tools: [],
generation: input.generation,
metadata: input.metadata,
native: {
opencodeProviderID: input.provider.id,
opencodeModelID: input.model.id,
...input.native,
},
})
})
export * as LLMNative from "./llm-native"
@@ -0,0 +1,106 @@
import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { ModelID, ProviderID } from "../../src/provider/schema"
import { LLMNative } from "../../src/session/llm-native"
import { MessageID, PartID, SessionID } from "../../src/session/schema"
import { ProviderTest } from "../fake/provider"
import type { MessageV2 } from "../../src/session/message-v2"
import type { Provider } from "../../src/provider"
const sessionID = SessionID.descending()
const model = (input: Partial<Provider.Model> = {}) =>
ProviderTest.model({
id: ModelID.make("gpt-5"),
providerID: ProviderID.openai,
api: { id: "gpt-5", url: "https://api.openai.com/v1", npm: "@ai-sdk/openai" },
...input,
})
const textPart = (messageID: MessageID, text: string, input: Partial<MessageV2.TextPart> = {}): MessageV2.TextPart => ({
id: PartID.ascending(),
sessionID,
messageID,
type: "text",
text,
...input,
})
const userMessage = (mdl: Provider.Model, id: MessageID, parts: MessageV2.Part[]): MessageV2.WithParts => {
return {
info: {
id,
sessionID,
role: "user",
time: { created: 1 },
agent: "build",
model: { providerID: mdl.providerID, modelID: mdl.id },
},
parts,
}
}
const assistantMessage = (
mdl: Provider.Model,
id: MessageID,
parentID: MessageID,
parts: MessageV2.Part[],
): MessageV2.WithParts => {
return {
info: {
id,
sessionID,
role: "assistant",
time: { created: 2 },
parentID,
modelID: mdl.id,
providerID: mdl.providerID,
mode: "build",
agent: "build",
path: { cwd: "/tmp/project", root: "/tmp/project" },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
},
parts,
}
}
describe("LLMNative.request", () => {
test("builds a text-only native LLM request", async () => {
const mdl = model()
const provider = ProviderTest.info({ id: ProviderID.openai, key: "openai-key" }, mdl)
const userID = MessageID.ascending()
const assistantID = MessageID.ascending()
const request = await Effect.runPromise(
LLMNative.request({
id: "request-1",
provider,
model: mdl,
system: ["You are concise.", ""],
generation: { maxTokens: 123, temperature: 0.2, topP: 0.9 },
messages: [
userMessage(mdl, userID, [textPart(userID, "ignored", { ignored: true }), textPart(userID, "Hello")]),
assistantMessage(mdl, assistantID, userID, [textPart(assistantID, "Hi")]),
],
}),
)
expect(request).toMatchObject({
id: "request-1",
model: {
id: "gpt-5",
provider: "openai",
protocol: "openai-responses",
headers: { authorization: "Bearer openai-key" },
},
system: [{ type: "text", text: "You are concise." }],
generation: { maxTokens: 123, temperature: 0.2, topP: 0.9 },
tools: [],
})
expect(request.messages.map((message) => ({ id: message.id, role: message.role, content: message.content }))).toEqual([
{ id: userID, role: "user", content: [{ type: "text", text: "Hello" }] },
{ id: assistantID, role: "assistant", content: [{ type: "text", text: "Hi" }] },
])
})
})