llm.ts 6.1 KB

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