import { Effect, JsonSchema, Schema } from "effect" import { LLMClient, modelLimits, modelRef, type ModelRefInput } from "./route/client" import { GenerationOptions, HttpOptions, InvalidProviderOutputReason, LLMError, LLMEvent, LLMRequest, LLMResponse, Message, SystemPart, ToolChoice, ToolDefinition, type ContentPart, ToolCallPart, ToolResultPart, } from "./schema" import { make as makeTool, type ToolSchema } from "./tool" export type ModelInput = ModelRefInput export type MessageInput = Message.Input export type ToolChoiceInput = ToolChoice.Input export type ToolChoiceMode = ToolChoice.Mode export type ToolResultInput = Parameters[0] /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */ export type RequestInput = Omit< ConstructorParameters[0], "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions" > & { readonly system?: string | SystemPart | ReadonlyArray readonly prompt?: string | ContentPart | ReadonlyArray readonly messages?: ReadonlyArray readonly tools?: ReadonlyArray readonly toolChoice?: ToolChoiceInput readonly generation?: GenerationOptions.Input readonly providerOptions?: ConstructorParameters[0]["providerOptions"] readonly http?: HttpOptions.Input } export const limits = modelLimits export const text = Message.text export const system = SystemPart.make export const message = Message.make export const user = Message.user export const assistant = Message.assistant export const model = modelRef export const toolDefinition = ToolDefinition.make export const toolCall = ToolCallPart.make export const toolResult = ToolResultPart.make export const toolMessage = Message.tool export const toolChoiceName = ToolChoice.named export const toolChoice = ToolChoice.make export const generation = GenerationOptions.make export const generate = LLMClient.generate export const stream = LLMClient.stream export const stepCountIs = LLMClient.stepCountIs export const requestInput = (input: LLMRequest): RequestInput => ({ ...LLMRequest.input(input), }) export const request = (input: RequestInput) => { const { system: requestSystem, prompt, messages, tools, toolChoice: requestToolChoice, generation: requestGeneration, providerOptions: requestProviderOptions, http: requestHttp, ...rest } = input return new LLMRequest({ ...rest, system: SystemPart.content(requestSystem), messages: [...(messages?.map(message) ?? []), ...(prompt === undefined ? [] : [user(prompt)])], tools: tools?.map(toolDefinition) ?? [], toolChoice: requestToolChoice ? toolChoice(requestToolChoice) : undefined, generation: requestGeneration === undefined ? undefined : generation(requestGeneration), providerOptions: requestProviderOptions, http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp), }) } export const updateRequest = (input: LLMRequest, patch: Partial) => request({ ...requestInput(input), ...patch }) const GENERATE_OBJECT_TOOL_NAME = "generate_object" const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool." type GenerateObjectBase = Omit export class GenerateObjectResponse { constructor( readonly object: T, readonly response: LLMResponse, ) {} get events() { return this.response.events } get usage() { return this.response.usage } } export interface GenerateObjectOptions> extends GenerateObjectBase { readonly schema: S } export interface GenerateObjectDynamicOptions extends GenerateObjectBase { /** Raw JSON Schema object describing the expected output shape. */ readonly jsonSchema: JsonSchema.JsonSchema } const runGenerateObject = Effect.fn("LLM.generateObject")(function* ( options: GenerateObjectBase, tool: ReturnType, ) { const baseRequest = request(options) const generateRequest = LLMRequest.update(baseRequest, { toolChoice: ToolChoice.named(GENERATE_OBJECT_TOOL_NAME), }) const response = yield* LLMClient.generate({ request: generateRequest, tools: { [GENERATE_OBJECT_TOOL_NAME]: tool }, toolExecution: "none", }) const call = response.toolCalls.find( (event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME, ) if (!call || !LLMEvent.is.toolCall(call)) return yield* new LLMError({ module: "LLM", method: "generateObject", reason: new InvalidProviderOutputReason({ message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`, }), }) const object = yield* tool._decode(call.input).pipe( Effect.mapError( (error) => new LLMError({ module: "LLM", method: "generateObject", reason: new InvalidProviderOutputReason({ message: `generateObject: tool input failed schema decode: ${error.message}`, }), }), ), ) return new GenerateObjectResponse(object, response) }) /** * Run a model and decode its output against `schema`. Works on every protocol * because it forces a synthetic tool call internally — provider-native JSON * modes are intentionally avoided so behaviour is uniform. * * Two input modes: * * 1. `schema: EffectSchema` — `.object` is decoded and typed as `T`. * Decode failures surface as `LLMError`. * 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when * the schema is only available at runtime (MCP, plugin manifests). Caller validates. */ export function generateObject>( options: GenerateObjectOptions, ): Effect.Effect>, LLMError> export function generateObject( options: GenerateObjectDynamicOptions, ): Effect.Effect, LLMError> export function generateObject(options: GenerateObjectOptions> | GenerateObjectDynamicOptions) { if ("schema" in options) { const { schema, ...rest } = options return runGenerateObject( rest, makeTool({ description: GENERATE_OBJECT_TOOL_DESCRIPTION, parameters: schema, success: Schema.Unknown as ToolSchema, execute: () => Effect.void, }), ) } const { jsonSchema, ...rest } = options return runGenerateObject( rest, makeTool({ description: GENERATE_OBJECT_TOOL_DESCRIPTION, jsonSchema, execute: () => Effect.void, }), ) }