| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- import { Effect, JsonSchema, Schema } from "effect"
- import { LLMClient } from "./route/client"
- import {
- GenerationOptions,
- HttpOptions,
- InvalidProviderOutputReason,
- LLMError,
- LLMEvent,
- LLMRequest,
- LLMResponse,
- Message,
- type ModelInput as SchemaModelInput,
- SystemPart,
- ToolChoice,
- ToolDefinition,
- type ContentPart,
- ToolResultPart,
- } from "./schema"
- import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
- export type ModelInput = SchemaModelInput
- export type MessageInput = Message.Input
- export type ToolChoiceInput = ToolChoice.Input
- export type ToolChoiceMode = ToolChoice.Mode
- export type ToolResultInput = Parameters<typeof ToolResultPart.make>[0]
- /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
- export type RequestInput = Omit<
- ConstructorParameters<typeof LLMRequest>[0],
- "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
- > & {
- readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
- readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
- readonly messages?: ReadonlyArray<Message | MessageInput>
- readonly tools?: ReadonlyArray<ToolDefinition.Input>
- readonly toolChoice?: ToolChoiceInput
- readonly generation?: GenerationOptions.Input
- readonly providerOptions?: ConstructorParameters<typeof LLMRequest>[0]["providerOptions"]
- readonly http?: HttpOptions.Input
- }
- export const generate = LLMClient.generate
- export const stream = LLMClient.stream
- 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.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])],
- tools: tools?.map(ToolDefinition.make) ?? [],
- toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined,
- generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration),
- providerOptions: requestProviderOptions,
- http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp),
- })
- }
- export const updateRequest = (input: LLMRequest, patch: Partial<RequestInput>) =>
- 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<RequestInput, "tools" | "toolChoice" | "responseFormat">
- export class GenerateObjectResponse<T> {
- constructor(
- readonly object: T,
- readonly response: LLMResponse,
- ) {}
- get events() {
- return this.response.events
- }
- get usage() {
- return this.response.usage
- }
- }
- export interface GenerateObjectOptions<S extends ToolSchema<any>> 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<typeof makeTool>,
- ) {
- const baseRequest = request(options)
- const generateRequest = LLMRequest.update(baseRequest, {
- tools: toDefinitions({ [GENERATE_OBJECT_TOOL_NAME]: tool }),
- toolChoice: ToolChoice.named(GENERATE_OBJECT_TOOL_NAME),
- })
- const response = yield* LLMClient.generate(generateRequest)
- 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<T>` — `.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<S extends ToolSchema<any>>(
- options: GenerateObjectOptions<S>,
- ): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
- export function generateObject(
- options: GenerateObjectDynamicOptions,
- ): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
- export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | 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<unknown>,
- execute: () => Effect.void,
- }),
- )
- }
- const { jsonSchema, ...rest } = options
- return runGenerateObject(
- rest,
- makeTool({
- description: GENERATE_OBJECT_TOOL_DESCRIPTION,
- jsonSchema,
- execute: () => Effect.void,
- }),
- )
- }
|