llm.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { Effect, JsonSchema, Schema } from "effect"
  2. import { LLMClient } from "./route/client"
  3. import {
  4. GenerationOptions,
  5. HttpOptions,
  6. InvalidProviderOutputReason,
  7. LLMError,
  8. LLMEvent,
  9. LLMRequest,
  10. LLMResponse,
  11. Message,
  12. type ModelInput as SchemaModelInput,
  13. SystemPart,
  14. ToolChoice,
  15. ToolDefinition,
  16. type ContentPart,
  17. ToolResultPart,
  18. } from "./schema"
  19. import { make as makeTool, toDefinitions, type ToolSchema } from "./tool"
  20. export type ModelInput = SchemaModelInput
  21. export type MessageInput = Message.Input
  22. export type ToolChoiceInput = ToolChoice.Input
  23. export type ToolChoiceMode = ToolChoice.Mode
  24. export type ToolResultInput = Parameters<typeof ToolResultPart.make>[0]
  25. /** Input accepted by `LLM.request`, normalized into the canonical `LLMRequest` class. */
  26. export type RequestInput = Omit<
  27. ConstructorParameters<typeof LLMRequest>[0],
  28. "system" | "messages" | "tools" | "toolChoice" | "generation" | "http" | "providerOptions"
  29. > & {
  30. readonly system?: string | SystemPart | ReadonlyArray<SystemPart>
  31. readonly prompt?: string | ContentPart | ReadonlyArray<ContentPart>
  32. readonly messages?: ReadonlyArray<Message | MessageInput>
  33. readonly tools?: ReadonlyArray<ToolDefinition.Input>
  34. readonly toolChoice?: ToolChoiceInput
  35. readonly generation?: GenerationOptions.Input
  36. readonly providerOptions?: ConstructorParameters<typeof LLMRequest>[0]["providerOptions"]
  37. readonly http?: HttpOptions.Input
  38. }
  39. export const generate = LLMClient.generate
  40. export const stream = LLMClient.stream
  41. export const requestInput = (input: LLMRequest): RequestInput => ({
  42. ...LLMRequest.input(input),
  43. })
  44. export const request = (input: RequestInput) => {
  45. const {
  46. system: requestSystem,
  47. prompt,
  48. messages,
  49. tools,
  50. toolChoice: requestToolChoice,
  51. generation: requestGeneration,
  52. providerOptions: requestProviderOptions,
  53. http: requestHttp,
  54. ...rest
  55. } = input
  56. return new LLMRequest({
  57. ...rest,
  58. system: SystemPart.content(requestSystem),
  59. messages: [...(messages?.map(Message.make) ?? []), ...(prompt === undefined ? [] : [Message.user(prompt)])],
  60. tools: tools?.map(ToolDefinition.make) ?? [],
  61. toolChoice: requestToolChoice ? ToolChoice.make(requestToolChoice) : undefined,
  62. generation: requestGeneration === undefined ? undefined : GenerationOptions.make(requestGeneration),
  63. providerOptions: requestProviderOptions,
  64. http: requestHttp === undefined ? undefined : HttpOptions.make(requestHttp),
  65. })
  66. }
  67. export const updateRequest = (input: LLMRequest, patch: Partial<RequestInput>) =>
  68. request({ ...requestInput(input), ...patch })
  69. const GENERATE_OBJECT_TOOL_NAME = "generate_object"
  70. const GENERATE_OBJECT_TOOL_DESCRIPTION = "Return the structured result by calling this tool."
  71. type GenerateObjectBase = Omit<RequestInput, "tools" | "toolChoice" | "responseFormat">
  72. export class GenerateObjectResponse<T> {
  73. constructor(
  74. readonly object: T,
  75. readonly response: LLMResponse,
  76. ) {}
  77. get events() {
  78. return this.response.events
  79. }
  80. get usage() {
  81. return this.response.usage
  82. }
  83. }
  84. export interface GenerateObjectOptions<S extends ToolSchema<any>> extends GenerateObjectBase {
  85. readonly schema: S
  86. }
  87. export interface GenerateObjectDynamicOptions extends GenerateObjectBase {
  88. /** Raw JSON Schema object describing the expected output shape. */
  89. readonly jsonSchema: JsonSchema.JsonSchema
  90. }
  91. const runGenerateObject = Effect.fn("LLM.generateObject")(function* (
  92. options: GenerateObjectBase,
  93. tool: ReturnType<typeof makeTool>,
  94. ) {
  95. const baseRequest = request(options)
  96. const generateRequest = LLMRequest.update(baseRequest, {
  97. tools: toDefinitions({ [GENERATE_OBJECT_TOOL_NAME]: tool }),
  98. toolChoice: ToolChoice.named(GENERATE_OBJECT_TOOL_NAME),
  99. })
  100. const response = yield* LLMClient.generate(generateRequest)
  101. const call = response.toolCalls.find(
  102. (event) => LLMEvent.is.toolCall(event) && event.name === GENERATE_OBJECT_TOOL_NAME,
  103. )
  104. if (!call || !LLMEvent.is.toolCall(call))
  105. return yield* new LLMError({
  106. module: "LLM",
  107. method: "generateObject",
  108. reason: new InvalidProviderOutputReason({
  109. message: `generateObject: model did not call the forced \`${GENERATE_OBJECT_TOOL_NAME}\` tool`,
  110. }),
  111. })
  112. const object = yield* tool._decode(call.input).pipe(
  113. Effect.mapError(
  114. (error) =>
  115. new LLMError({
  116. module: "LLM",
  117. method: "generateObject",
  118. reason: new InvalidProviderOutputReason({
  119. message: `generateObject: tool input failed schema decode: ${error.message}`,
  120. }),
  121. }),
  122. ),
  123. )
  124. return new GenerateObjectResponse(object, response)
  125. })
  126. /**
  127. * Run a model and decode its output against `schema`. Works on every protocol
  128. * because it forces a synthetic tool call internally — provider-native JSON
  129. * modes are intentionally avoided so behaviour is uniform.
  130. *
  131. * Two input modes:
  132. *
  133. * 1. `schema: EffectSchema<T>` — `.object` is decoded and typed as `T`.
  134. * Decode failures surface as `LLMError`.
  135. * 2. `jsonSchema: JsonSchema.JsonSchema` — `.object` is `unknown`. Use when
  136. * the schema is only available at runtime (MCP, plugin manifests). Caller validates.
  137. */
  138. export function generateObject<S extends ToolSchema<any>>(
  139. options: GenerateObjectOptions<S>,
  140. ): Effect.Effect<GenerateObjectResponse<Schema.Schema.Type<S>>, LLMError>
  141. export function generateObject(
  142. options: GenerateObjectDynamicOptions,
  143. ): Effect.Effect<GenerateObjectResponse<unknown>, LLMError>
  144. export function generateObject(options: GenerateObjectOptions<ToolSchema<any>> | GenerateObjectDynamicOptions) {
  145. if ("schema" in options) {
  146. const { schema, ...rest } = options
  147. return runGenerateObject(
  148. rest,
  149. makeTool({
  150. description: GENERATE_OBJECT_TOOL_DESCRIPTION,
  151. parameters: schema,
  152. success: Schema.Unknown as ToolSchema<unknown>,
  153. execute: () => Effect.void,
  154. }),
  155. )
  156. }
  157. const { jsonSchema, ...rest } = options
  158. return runGenerateObject(
  159. rest,
  160. makeTool({
  161. description: GENERATE_OBJECT_TOOL_DESCRIPTION,
  162. jsonSchema,
  163. execute: () => Effect.void,
  164. }),
  165. )
  166. }