messages.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import { Schema } from "effect"
  2. import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
  3. import { CacheHint, GenerationOptions, HttpOptions, ModelRef, ProviderOptions } from "./options"
  4. const isRecord = (value: unknown): value is Record<string, unknown> =>
  5. typeof value === "object" && value !== null && !Array.isArray(value)
  6. const systemPartSchema = Schema.Struct({
  7. type: Schema.Literal("text"),
  8. text: Schema.String,
  9. cache: Schema.optional(CacheHint),
  10. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  11. }).annotate({ identifier: "LLM.SystemPart" })
  12. export type SystemPart = Schema.Schema.Type<typeof systemPartSchema>
  13. const makeSystemPart = (text: string): SystemPart => ({ type: "text", text })
  14. export const SystemPart = Object.assign(systemPartSchema, {
  15. make: makeSystemPart,
  16. content: (input?: string | SystemPart | ReadonlyArray<SystemPart>) => {
  17. if (input === undefined) return []
  18. return typeof input === "string" ? [makeSystemPart(input)] : Array.isArray(input) ? [...input] : [input]
  19. },
  20. })
  21. export const TextPart = Schema.Struct({
  22. type: Schema.Literal("text"),
  23. text: Schema.String,
  24. cache: Schema.optional(CacheHint),
  25. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  26. providerMetadata: Schema.optional(ProviderMetadata),
  27. }).annotate({ identifier: "LLM.Content.Text" })
  28. export type TextPart = Schema.Schema.Type<typeof TextPart>
  29. export const MediaPart = Schema.Struct({
  30. type: Schema.Literal("media"),
  31. mediaType: Schema.String,
  32. data: Schema.Union([Schema.String, Schema.Uint8Array]),
  33. filename: Schema.optional(Schema.String),
  34. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  35. }).annotate({ identifier: "LLM.Content.Media" })
  36. export type MediaPart = Schema.Schema.Type<typeof MediaPart>
  37. const isToolResultValue = (value: unknown): value is ToolResultValue =>
  38. isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error") && "value" in value
  39. export const ToolResultValue = Object.assign(
  40. Schema.Struct({
  41. type: Schema.Literals(["json", "text", "error"]),
  42. value: Schema.Unknown,
  43. }).annotate({ identifier: "LLM.ToolResult" }),
  44. {
  45. make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue =>
  46. isToolResultValue(value) ? value : { type, value },
  47. },
  48. )
  49. export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
  50. export const ToolCallPart = Object.assign(
  51. Schema.Struct({
  52. type: Schema.Literal("tool-call"),
  53. id: Schema.String,
  54. name: Schema.String,
  55. input: Schema.Unknown,
  56. providerExecuted: Schema.optional(Schema.Boolean),
  57. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  58. providerMetadata: Schema.optional(ProviderMetadata),
  59. }).annotate({ identifier: "LLM.Content.ToolCall" }),
  60. {
  61. make: (input: Omit<ToolCallPart, "type">): ToolCallPart => ({ type: "tool-call", ...input }),
  62. },
  63. )
  64. export type ToolCallPart = Schema.Schema.Type<typeof ToolCallPart>
  65. export const ToolResultPart = Object.assign(
  66. Schema.Struct({
  67. type: Schema.Literal("tool-result"),
  68. id: Schema.String,
  69. name: Schema.String,
  70. result: ToolResultValue,
  71. providerExecuted: Schema.optional(Schema.Boolean),
  72. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  73. providerMetadata: Schema.optional(ProviderMetadata),
  74. }).annotate({ identifier: "LLM.Content.ToolResult" }),
  75. {
  76. make: (
  77. input: Omit<ToolResultPart, "type" | "result"> & {
  78. readonly result: unknown
  79. readonly resultType?: ToolResultValue["type"]
  80. },
  81. ): ToolResultPart => ({
  82. type: "tool-result",
  83. id: input.id,
  84. name: input.name,
  85. result: ToolResultValue.make(input.result, input.resultType),
  86. providerExecuted: input.providerExecuted,
  87. metadata: input.metadata,
  88. providerMetadata: input.providerMetadata,
  89. }),
  90. },
  91. )
  92. export type ToolResultPart = Schema.Schema.Type<typeof ToolResultPart>
  93. export const ReasoningPart = Schema.Struct({
  94. type: Schema.Literal("reasoning"),
  95. text: Schema.String,
  96. encrypted: Schema.optional(Schema.String),
  97. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  98. providerMetadata: Schema.optional(ProviderMetadata),
  99. }).annotate({ identifier: "LLM.Content.Reasoning" })
  100. export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
  101. export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe(
  102. Schema.toTaggedUnion("type"),
  103. )
  104. export type ContentPart = Schema.Schema.Type<typeof ContentPart>
  105. export class Message extends Schema.Class<Message>("LLM.Message")({
  106. id: Schema.optional(Schema.String),
  107. role: MessageRole,
  108. content: Schema.Array(ContentPart),
  109. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  110. native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  111. }) {}
  112. export namespace Message {
  113. export type ContentInput = string | ContentPart | ReadonlyArray<ContentPart>
  114. export type Input = Omit<ConstructorParameters<typeof Message>[0], "content"> & {
  115. readonly content: ContentInput
  116. }
  117. export const text = (value: string): ContentPart => ({ type: "text", text: value })
  118. export const content = (input: ContentInput) =>
  119. typeof input === "string" ? [text(input)] : Array.isArray(input) ? [...input] : [input]
  120. export const make = (input: Message | Input) => {
  121. if (input instanceof Message) return input
  122. return new Message({ ...input, content: content(input.content) })
  123. }
  124. export const user = (content: ContentInput) => make({ role: "user", content })
  125. export const assistant = (content: ContentInput) => make({ role: "assistant", content })
  126. export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
  127. make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
  128. }
  129. export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefinition")({
  130. name: Schema.String,
  131. description: Schema.String,
  132. inputSchema: JsonSchema,
  133. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  134. native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  135. }) {}
  136. export namespace ToolDefinition {
  137. export type Input = ToolDefinition | ConstructorParameters<typeof ToolDefinition>[0]
  138. /** Normalize tool definition input into the canonical `ToolDefinition` class. */
  139. export const make = (input: Input) => (input instanceof ToolDefinition ? input : new ToolDefinition(input))
  140. }
  141. export class ToolChoice extends Schema.Class<ToolChoice>("LLM.ToolChoice")({
  142. type: Schema.Literals(["auto", "none", "required", "tool"]),
  143. name: Schema.optional(Schema.String),
  144. }) {}
  145. export namespace ToolChoice {
  146. export type Mode = Exclude<ToolChoice["type"], "tool">
  147. export type Input = ToolChoice | ConstructorParameters<typeof ToolChoice>[0] | ToolDefinition | string
  148. const isMode = (value: string): value is Mode => value === "auto" || value === "none" || value === "required"
  149. /** Select a specific named tool. */
  150. export const named = (value: string) => new ToolChoice({ type: "tool", name: value })
  151. /** Normalize ergonomic tool-choice inputs into the canonical `ToolChoice` class. */
  152. export const make = (input: Input) => {
  153. if (input instanceof ToolChoice) return input
  154. if (input instanceof ToolDefinition) return named(input.name)
  155. if (typeof input === "string") return isMode(input) ? new ToolChoice({ type: input }) : named(input)
  156. return new ToolChoice(input)
  157. }
  158. }
  159. export const ResponseFormat = Schema.Union([
  160. Schema.Struct({ type: Schema.Literal("text") }),
  161. Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }),
  162. Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }),
  163. ]).pipe(Schema.toTaggedUnion("type"))
  164. export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat>
  165. export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
  166. id: Schema.optional(Schema.String),
  167. model: ModelRef,
  168. system: Schema.Array(SystemPart),
  169. messages: Schema.Array(Message),
  170. tools: Schema.Array(ToolDefinition),
  171. toolChoice: Schema.optional(ToolChoice),
  172. generation: Schema.optional(GenerationOptions),
  173. providerOptions: Schema.optional(ProviderOptions),
  174. http: Schema.optional(HttpOptions),
  175. responseFormat: Schema.optional(ResponseFormat),
  176. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  177. }) {}
  178. export namespace LLMRequest {
  179. export type Input = ConstructorParameters<typeof LLMRequest>[0]
  180. export const input = (request: LLMRequest): Input => ({
  181. id: request.id,
  182. model: request.model,
  183. system: request.system,
  184. messages: request.messages,
  185. tools: request.tools,
  186. toolChoice: request.toolChoice,
  187. generation: request.generation,
  188. providerOptions: request.providerOptions,
  189. http: request.http,
  190. responseFormat: request.responseFormat,
  191. metadata: request.metadata,
  192. })
  193. export const update = (request: LLMRequest, patch: Partial<Input>) => {
  194. if (Object.keys(patch).length === 0) return request
  195. return new LLMRequest({
  196. ...input(request),
  197. ...patch,
  198. model: patch.model ?? request.model,
  199. })
  200. }
  201. }