feat(opencode): add native LLM request builder
This commit is contained in:
@@ -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" }] },
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user