mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-17 12:56:41 +02:00
209 lines
6.6 KiB
TypeScript
209 lines
6.6 KiB
TypeScript
import type { Auth } from "@/auth"
|
|
import type { RuntimeFlags } from "@/effect/runtime-flags"
|
|
import { InstanceState } from "@/effect/instance-state"
|
|
import { Permission } from "@/permission"
|
|
import type { Agent } from "@/agent/agent"
|
|
import type { MessageV2 } from "../message-v2"
|
|
import type { Provider } from "@/provider/provider"
|
|
import { ProviderTransform } from "@/provider/transform"
|
|
import { SystemPrompt } from "../system"
|
|
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
|
import { Effect, Record } from "effect"
|
|
import { jsonSchema, tool as aiTool, type ModelMessage, type Tool } from "ai"
|
|
import type { Plugin } from "@/plugin"
|
|
import { mergeDeep } from "remeda"
|
|
|
|
const USER_AGENT = `opencode/${InstallationVersion}`
|
|
|
|
type PrepareInput = {
|
|
readonly user: MessageV2.User
|
|
readonly sessionID: string
|
|
readonly parentSessionID?: string
|
|
readonly model: Provider.Model
|
|
readonly agent: Agent.Info
|
|
readonly permission?: Permission.Ruleset
|
|
readonly system: string[]
|
|
readonly messages: ModelMessage[]
|
|
readonly small?: boolean
|
|
readonly tools: Record<string, Tool>
|
|
readonly provider: Provider.Info
|
|
readonly auth: Auth.Info | undefined
|
|
readonly plugin: Plugin.Interface
|
|
readonly flags: RuntimeFlags.Info
|
|
readonly isWorkflow: boolean
|
|
}
|
|
|
|
export type Prepared = {
|
|
readonly isOpenaiOauth: boolean
|
|
readonly system: string[]
|
|
readonly messages: ModelMessage[]
|
|
readonly tools: Record<string, Tool>
|
|
readonly params: {
|
|
readonly temperature?: number
|
|
readonly topP?: number
|
|
readonly topK?: number
|
|
readonly maxOutputTokens?: number
|
|
readonly options: Record<string, any>
|
|
}
|
|
readonly messageTransformOptions: Record<string, any>
|
|
readonly headers: Record<string, string>
|
|
}
|
|
|
|
const mergeOptions = (target: Record<string, any>, source: Record<string, any> | undefined): Record<string, any> =>
|
|
mergeDeep(target, source ?? {}) as Record<string, any>
|
|
|
|
export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: PrepareInput) {
|
|
const isOpenaiOauth = input.provider.id === "openai" && input.auth?.type === "oauth"
|
|
const system = [
|
|
[
|
|
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
|
|
...input.system,
|
|
...(input.user.system ? [input.user.system] : []),
|
|
]
|
|
.filter((x) => x)
|
|
.join("\n"),
|
|
]
|
|
|
|
const header = system[0]
|
|
yield* input.plugin.trigger(
|
|
"experimental.chat.system.transform",
|
|
{ sessionID: input.sessionID, model: input.model },
|
|
{ system },
|
|
)
|
|
if (system.length > 2 && system[0] === header) {
|
|
const rest = system.slice(1)
|
|
system.length = 0
|
|
system.push(header, rest.join("\n"))
|
|
}
|
|
|
|
const variant =
|
|
!input.small && input.model.variants && input.user.model.variant
|
|
? input.model.variants[input.user.model.variant]
|
|
: {}
|
|
const base = input.small
|
|
? ProviderTransform.smallOptions(input.model)
|
|
: ProviderTransform.options({
|
|
model: input.model,
|
|
sessionID: input.sessionID,
|
|
providerOptions: input.provider.options,
|
|
})
|
|
const options = mergeOptions(mergeOptions(mergeOptions(base, input.model.options), input.agent.options), variant)
|
|
if (isOpenaiOauth) options.instructions = system.join("\n")
|
|
|
|
const messages =
|
|
isOpenaiOauth || input.isWorkflow
|
|
? input.messages
|
|
: [
|
|
...system.map(
|
|
(x): ModelMessage => ({
|
|
role: "system",
|
|
content: x,
|
|
}),
|
|
),
|
|
...input.messages,
|
|
]
|
|
|
|
const params = yield* input.plugin.trigger(
|
|
"chat.params",
|
|
{
|
|
sessionID: input.sessionID,
|
|
agent: input.agent.name,
|
|
model: input.model,
|
|
provider: input.provider,
|
|
message: input.user,
|
|
},
|
|
{
|
|
temperature: input.model.capabilities.temperature
|
|
? (input.agent.temperature ?? ProviderTransform.temperature(input.model))
|
|
: undefined,
|
|
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
|
|
topK: ProviderTransform.topK(input.model),
|
|
maxOutputTokens: ProviderTransform.maxOutputTokens(input.model, input.flags.outputTokenMax),
|
|
options,
|
|
},
|
|
)
|
|
|
|
const { headers } = yield* input.plugin.trigger(
|
|
"chat.headers",
|
|
{
|
|
sessionID: input.sessionID,
|
|
agent: input.agent.name,
|
|
model: input.model,
|
|
provider: input.provider,
|
|
message: input.user,
|
|
},
|
|
{
|
|
headers: {},
|
|
},
|
|
)
|
|
|
|
const tools = resolveTools(input)
|
|
if (
|
|
input.model.providerID.includes("github-copilot") &&
|
|
Object.keys(tools).length === 0 &&
|
|
hasToolCalls(input.messages)
|
|
) {
|
|
// Copilot needs a tools field when replaying prior tool calls, even if no tools are currently enabled.
|
|
tools["_noop"] = aiTool({
|
|
description: "Do not call this tool. It exists only for API compatibility and must never be invoked.",
|
|
inputSchema: jsonSchema({
|
|
type: "object",
|
|
properties: {
|
|
reason: { type: "string", description: "Unused" },
|
|
},
|
|
}),
|
|
execute: async () => ({ output: "", title: "", metadata: {} }),
|
|
})
|
|
}
|
|
|
|
const opencodeProjectID = input.model.providerID.startsWith("opencode")
|
|
? (yield* InstanceState.context).project.id
|
|
: undefined
|
|
|
|
return {
|
|
isOpenaiOauth,
|
|
system,
|
|
messages,
|
|
tools: Object.fromEntries(Object.entries(tools).toSorted(([a], [b]) => a.localeCompare(b))),
|
|
params,
|
|
messageTransformOptions: options,
|
|
headers: {
|
|
...(input.model.providerID.startsWith("opencode")
|
|
? {
|
|
...(opencodeProjectID ? { "x-opencode-project": opencodeProjectID } : {}),
|
|
"x-opencode-session": input.sessionID,
|
|
"x-opencode-request": input.user.id,
|
|
"x-opencode-client": input.flags.client,
|
|
"User-Agent": USER_AGENT,
|
|
}
|
|
: {
|
|
"x-session-affinity": input.sessionID,
|
|
...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}),
|
|
"User-Agent": USER_AGENT,
|
|
}),
|
|
...input.model.headers,
|
|
...headers,
|
|
},
|
|
}
|
|
})
|
|
|
|
function resolveTools(input: Pick<PrepareInput, "tools" | "agent" | "permission" | "user">) {
|
|
const disabled = Permission.disabled(
|
|
Object.keys(input.tools),
|
|
Permission.merge(input.agent.permission, input.permission ?? []),
|
|
)
|
|
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
|
|
}
|
|
|
|
export function hasToolCalls(messages: ModelMessage[]): boolean {
|
|
for (const msg of messages) {
|
|
if (!Array.isArray(msg.content)) continue
|
|
for (const part of msg.content) {
|
|
if (part.type === "tool-call" || part.type === "tool-result") return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
export * as LLMRequestPrep from "./request"
|