diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 0c6fe6ec91c8..5f9e971fa9fe 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -2,11 +2,10 @@ import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider/provider" import { ModelID, ProviderID } from "../provider/schema" -import { generateObject, streamObject, type ModelMessage } from "ai" +import type { ModelMessage } from "ai" import { Instance } from "../project/instance" import { Truncate } from "../tool/truncate" import { Auth } from "../auth" -import { ProviderTransform } from "../provider/transform" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -17,11 +16,11 @@ import { Permission } from "@/permission" import { mergeDeep, pipe, sortBy, values } from "remeda" import { Global } from "@/global" import path from "path" -import { Plugin } from "@/plugin" import { Skill } from "../skill" import { Effect, ServiceMap, Layer } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRuntime } from "@/effect/run-service" +import { LLM } from "@/session/llm" export namespace Agent { export const Info = z @@ -73,7 +72,6 @@ export namespace Agent { Service, Effect.gen(function* () { const config = yield* Config.Service - const auth = yield* Auth.Service const skill = yield* Skill.Service const provider = yield* Provider.Service @@ -330,64 +328,31 @@ export namespace Agent { description: string model?: { providerID: ProviderID; modelID: ModelID } }) { - const cfg = yield* config.get() const model = input.model ?? (yield* provider.defaultModel()) const resolved = yield* provider.getModel(model.providerID, model.modelID) - const language = yield* provider.getLanguage(resolved) const system = [PROMPT_GENERATE] - yield* Effect.promise(() => - Plugin.trigger("experimental.chat.system.transform", { model: resolved }, { system }), - ) const existing = yield* InstanceState.useEffect(state, (s) => s.list()) - const params = { - experimental_telemetry: { - isEnabled: cfg.experimental?.openTelemetry, - metadata: { - userId: cfg.username ?? "unknown", - }, - }, - temperature: 0.3, - messages: [ - ...system.map( - (item): ModelMessage => ({ - role: "system", - content: item, - }), - ), - { - role: "user", - content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, - }, - ], - model: language, - schema: z.object({ - identifier: z.string(), - whenToUse: z.string(), - systemPrompt: z.string(), - }), - } satisfies Parameters[0] + const USER_MESSAGE_CONTENT = { + role: "user", + content: `Create an agent configuration based on this request: \"${input.description}\".\n\nIMPORTANT: The following identifiers already exist and must NOT be used: ${existing.map((i) => i.name).join(", ")}\n Return ONLY the JSON object, no other text, do not wrap in backticks`, + } as ModelMessage - // TODO: clean this up so provider specific logic doesnt bleed over - const authInfo = yield* auth.get(model.providerID).pipe(Effect.orDie) - if (model.providerID === "openai" && authInfo?.type === "oauth") { - return yield* Effect.promise(async () => { - const result = streamObject({ - ...params, - providerOptions: ProviderTransform.providerOptions(resolved, { - store: false, - }), - onError: () => {}, - }) - for await (const part of result.fullStream) { - if (part.type === "error") throw part.error - } - return result.object - }) - } - - return yield* Effect.promise(() => generateObject(params).then((r) => r.object)) + return yield* Effect.promise((abort) => + LLM.generateObject({ + abort, + temperature: 0.3, + messages: [USER_MESSAGE_CONTENT], + model: resolved, + system, + schema: z.object({ + identifier: z.string(), + whenToUse: z.string(), + systemPrompt: z.string(), + }), + }), + ) }), }) }), diff --git a/packages/opencode/src/session/llm.ts b/packages/opencode/src/session/llm.ts index d55424f91ede..38df666fb588 100644 --- a/packages/opencode/src/session/llm.ts +++ b/packages/opencode/src/session/llm.ts @@ -3,7 +3,16 @@ import { Log } from "@/util/log" import { Cause, Effect, Layer, Record, ServiceMap } from "effect" import * as Queue from "effect/Queue" import * as Stream from "effect/Stream" -import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai" +import { + generateObject as generateObjectAI, + streamText, + streamObject, + wrapLanguageModel, + type ModelMessage, + type Tool, + tool, + jsonSchema, +} from "ai" import { mergeDeep, pipe } from "remeda" import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider" import { ProviderTransform } from "@/provider/transform" @@ -21,6 +30,7 @@ import { Wildcard } from "@/util/wildcard" import { SessionID } from "@/session/schema" import { Auth } from "@/auth" import { Installation } from "@/installation" +import z from "zod" export namespace LLM { const log = Log.create({ service: "llm" }) @@ -45,6 +55,18 @@ export namespace LLM { abort: AbortSignal } + export type ObjectInput = { + model: Provider.Model + system: string[] + messages: ModelMessage[] + schema: Schema + temperature?: number + } + + export type ObjectRequest = ObjectInput & { + abort: AbortSignal + } + export type Event = Awaited>["fullStream"] extends AsyncIterable ? T : never export interface Interface { @@ -390,6 +412,58 @@ export namespace LLM { }) } + export async function generateObject(input: ObjectRequest) { + const [language, cfg, provider, auth] = await Promise.all([ + Provider.getLanguage(input.model), + Config.get(), + Provider.getProvider(input.model.providerID), + Auth.get(input.model.providerID), + ]) + const system = [...input.system] + await Plugin.trigger("experimental.chat.system.transform", { model: input.model }, { system }) + const params = { + experimental_telemetry: { + isEnabled: cfg.experimental?.openTelemetry, + metadata: { + userId: cfg.username ?? "unknown", + }, + }, + temperature: input.temperature, + messages: + provider.id === "openai" && auth?.type === "oauth" + ? input.messages + : [ + ...system.map( + (x): ModelMessage => ({ + role: "system", + content: x, + }), + ), + ...input.messages, + ], + model: language, + schema: input.schema, + abortSignal: input.abort, + } satisfies Parameters>[0] + + if (provider.id === "openai" && auth?.type === "oauth") { + const result = streamObject({ + ...params, + providerOptions: ProviderTransform.providerOptions(input.model, { + instructions: system.join("\n"), + store: false, + }), + onError: () => {}, + }) + for await (const part of result.fullStream) { + if (part.type === "error") throw part.error + } + return result.object + } + + return generateObjectAI(params).then((x) => x.object) + } + function resolveTools(input: Pick) { const disabled = Permission.disabled( Object.keys(input.tools),