llm.ts 6.2 KB

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