Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions packages/opencode/src/session/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,7 @@ const live: Layer.Layer<
: isWorkflow
? input.messages
: [
...system.map(
(x): ModelMessage => ({
role: "system",
content: x,
}),
),
{ role: "system" as const, content: system.join("\n") } satisfies ModelMessage,
...input.messages,
]

Expand Down
97 changes: 97 additions & 0 deletions packages/opencode/test/session/llm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "path"
import { tool, type ModelMessage } from "ai"
import { Cause, Effect, Exit, Stream } from "effect"
import z from "zod"
import { pathToFileURL } from "url"
import { makeRuntime } from "../../src/effect/run-service"
import { LLM } from "../../src/session/llm"
import { Instance } from "../../src/project/instance"
Expand Down Expand Up @@ -561,6 +562,102 @@ describe("session.llm.stream", () => {
})
})

test("combines plugin-added system prompts into one message for non-anthropic providers", async () => {
const server = state.server
if (!server) {
throw new Error("Server not initialized")
}

const providerID = "alibaba"
const modelID = "qwen-plus"
const fixture = await loadFixture(providerID, modelID)
const model = fixture.model

const request = waitRequest(
"/chat/completions",
new Response(createChatStream("Hello"), {
status: 200,
headers: { "Content-Type": "text/event-stream" },
}),
)

await using tmp = await tmpdir({
init: async (dir) => {
const pluginFile = path.join(dir, "plugin.ts")
await Bun.write(
pluginFile,
[
"export default async () => ({",
' "experimental.chat.system.transform": (_input, output) => {',
' output.system.push("Relevant memory goes here.")',
" },",
"})",
"",
].join("\n"),
)

await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: [providerID],
plugin: [pathToFileURL(pluginFile).href],
provider: {
[providerID]: {
options: {
apiKey: "test-key",
baseURL: `${server.url.origin}/v1`,
},
},
},
}),
)
},
})

await Instance.provide({
directory: tmp.path,
fn: async () => {
const resolved = await getModel(ProviderID.make(providerID), ModelID.make(model.id))
const sessionID = SessionID.make("session-test-system-merge")
const agent = {
name: "test",
mode: "primary",
options: {},
permission: [{ permission: "*", pattern: "*", action: "allow" }],
} satisfies Agent.Info

const user = {
id: MessageID.make("user-system-merge"),
sessionID,
role: "user",
time: { created: Date.now() },
agent: agent.name,
model: { providerID: ProviderID.make(providerID), modelID: resolved.id },
} satisfies MessageV2.User

await drain({
user,
sessionID,
model: resolved,
agent,
system: ["You are a helpful assistant."],
messages: [{ role: "user", content: "Hello" }],
tools: {},
})

const capture = await request
const messages = capture.body.messages as Array<{ role: string; content: unknown }>
const systemMessages = messages.filter((message) => message.role === "system")
const systemContent = String(systemMessages[0]?.content ?? "")

expect(systemMessages).toHaveLength(1)
expect(systemContent).toContain("You are a helpful assistant.")
expect(systemContent).toContain("Relevant memory goes here.")
},
})
})

test("sends responses API payload for OpenAI models", async () => {
const server = state.server
if (!server) {
Expand Down
Loading