235 lines
9.1 KiB
TypeScript
235 lines
9.1 KiB
TypeScript
import { Schema } from "effect"
|
|
import { JsonSchema, MessageRole, ProviderMetadata } from "./ids"
|
|
import { CacheHint, GenerationOptions, HttpOptions, ModelRef, ProviderOptions } from "./options"
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
|
|
const systemPartSchema = Schema.Struct({
|
|
type: Schema.Literal("text"),
|
|
text: Schema.String,
|
|
cache: Schema.optional(CacheHint),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
}).annotate({ identifier: "LLM.SystemPart" })
|
|
export type SystemPart = Schema.Schema.Type<typeof systemPartSchema>
|
|
|
|
const makeSystemPart = (text: string): SystemPart => ({ type: "text", text })
|
|
|
|
export const SystemPart = Object.assign(systemPartSchema, {
|
|
make: makeSystemPart,
|
|
content: (input?: string | SystemPart | ReadonlyArray<SystemPart>) => {
|
|
if (input === undefined) return []
|
|
return typeof input === "string" ? [makeSystemPart(input)] : Array.isArray(input) ? [...input] : [input]
|
|
},
|
|
})
|
|
|
|
export const TextPart = Schema.Struct({
|
|
type: Schema.Literal("text"),
|
|
text: Schema.String,
|
|
cache: Schema.optional(CacheHint),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
providerMetadata: Schema.optional(ProviderMetadata),
|
|
}).annotate({ identifier: "LLM.Content.Text" })
|
|
export type TextPart = Schema.Schema.Type<typeof TextPart>
|
|
|
|
export const MediaPart = Schema.Struct({
|
|
type: Schema.Literal("media"),
|
|
mediaType: Schema.String,
|
|
data: Schema.Union([Schema.String, Schema.Uint8Array]),
|
|
filename: Schema.optional(Schema.String),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
}).annotate({ identifier: "LLM.Content.Media" })
|
|
export type MediaPart = Schema.Schema.Type<typeof MediaPart>
|
|
|
|
const isToolResultValue = (value: unknown): value is ToolResultValue =>
|
|
isRecord(value) && (value.type === "text" || value.type === "json" || value.type === "error") && "value" in value
|
|
|
|
export const ToolResultValue = Object.assign(
|
|
Schema.Struct({
|
|
type: Schema.Literals(["json", "text", "error"]),
|
|
value: Schema.Unknown,
|
|
}).annotate({ identifier: "LLM.ToolResult" }),
|
|
{
|
|
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue =>
|
|
isToolResultValue(value) ? value : { type, value },
|
|
},
|
|
)
|
|
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
|
|
|
|
export const ToolCallPart = Object.assign(
|
|
Schema.Struct({
|
|
type: Schema.Literal("tool-call"),
|
|
id: Schema.String,
|
|
name: Schema.String,
|
|
input: Schema.Unknown,
|
|
providerExecuted: Schema.optional(Schema.Boolean),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
providerMetadata: Schema.optional(ProviderMetadata),
|
|
}).annotate({ identifier: "LLM.Content.ToolCall" }),
|
|
{
|
|
make: (input: Omit<ToolCallPart, "type">): ToolCallPart => ({ type: "tool-call", ...input }),
|
|
},
|
|
)
|
|
export type ToolCallPart = Schema.Schema.Type<typeof ToolCallPart>
|
|
|
|
export const ToolResultPart = Object.assign(
|
|
Schema.Struct({
|
|
type: Schema.Literal("tool-result"),
|
|
id: Schema.String,
|
|
name: Schema.String,
|
|
result: ToolResultValue,
|
|
providerExecuted: Schema.optional(Schema.Boolean),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
providerMetadata: Schema.optional(ProviderMetadata),
|
|
}).annotate({ identifier: "LLM.Content.ToolResult" }),
|
|
{
|
|
make: (
|
|
input: Omit<ToolResultPart, "type" | "result"> & {
|
|
readonly result: unknown
|
|
readonly resultType?: ToolResultValue["type"]
|
|
},
|
|
): ToolResultPart => ({
|
|
type: "tool-result",
|
|
id: input.id,
|
|
name: input.name,
|
|
result: ToolResultValue.make(input.result, input.resultType),
|
|
providerExecuted: input.providerExecuted,
|
|
metadata: input.metadata,
|
|
providerMetadata: input.providerMetadata,
|
|
}),
|
|
},
|
|
)
|
|
export type ToolResultPart = Schema.Schema.Type<typeof ToolResultPart>
|
|
|
|
export const ReasoningPart = Schema.Struct({
|
|
type: Schema.Literal("reasoning"),
|
|
text: Schema.String,
|
|
encrypted: Schema.optional(Schema.String),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
providerMetadata: Schema.optional(ProviderMetadata),
|
|
}).annotate({ identifier: "LLM.Content.Reasoning" })
|
|
export type ReasoningPart = Schema.Schema.Type<typeof ReasoningPart>
|
|
|
|
export const ContentPart = Schema.Union([TextPart, MediaPart, ToolCallPart, ToolResultPart, ReasoningPart]).pipe(
|
|
Schema.toTaggedUnion("type"),
|
|
)
|
|
export type ContentPart = Schema.Schema.Type<typeof ContentPart>
|
|
|
|
export class Message extends Schema.Class<Message>("LLM.Message")({
|
|
id: Schema.optional(Schema.String),
|
|
role: MessageRole,
|
|
content: Schema.Array(ContentPart),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
}) {}
|
|
|
|
export namespace Message {
|
|
export type ContentInput = string | ContentPart | ReadonlyArray<ContentPart>
|
|
export type Input = Omit<ConstructorParameters<typeof Message>[0], "content"> & {
|
|
readonly content: ContentInput
|
|
}
|
|
|
|
export const text = (value: string): ContentPart => ({ type: "text", text: value })
|
|
|
|
export const content = (input: ContentInput) =>
|
|
typeof input === "string" ? [text(input)] : Array.isArray(input) ? [...input] : [input]
|
|
|
|
export const make = (input: Message | Input) => {
|
|
if (input instanceof Message) return input
|
|
return new Message({ ...input, content: content(input.content) })
|
|
}
|
|
|
|
export const user = (content: ContentInput) => make({ role: "user", content })
|
|
|
|
export const assistant = (content: ContentInput) => make({ role: "assistant", content })
|
|
|
|
export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
|
|
make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
|
|
}
|
|
|
|
export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefinition")({
|
|
name: Schema.String,
|
|
description: Schema.String,
|
|
inputSchema: JsonSchema,
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
}) {}
|
|
|
|
export namespace ToolDefinition {
|
|
export type Input = ToolDefinition | ConstructorParameters<typeof ToolDefinition>[0]
|
|
|
|
/** Normalize tool definition input into the canonical `ToolDefinition` class. */
|
|
export const make = (input: Input) => (input instanceof ToolDefinition ? input : new ToolDefinition(input))
|
|
}
|
|
|
|
export class ToolChoice extends Schema.Class<ToolChoice>("LLM.ToolChoice")({
|
|
type: Schema.Literals(["auto", "none", "required", "tool"]),
|
|
name: Schema.optional(Schema.String),
|
|
}) {}
|
|
|
|
export namespace ToolChoice {
|
|
export type Mode = Exclude<ToolChoice["type"], "tool">
|
|
export type Input = ToolChoice | ConstructorParameters<typeof ToolChoice>[0] | ToolDefinition | string
|
|
|
|
const isMode = (value: string): value is Mode => value === "auto" || value === "none" || value === "required"
|
|
|
|
/** Select a specific named tool. */
|
|
export const named = (value: string) => new ToolChoice({ type: "tool", name: value })
|
|
|
|
/** Normalize ergonomic tool-choice inputs into the canonical `ToolChoice` class. */
|
|
export const make = (input: Input) => {
|
|
if (input instanceof ToolChoice) return input
|
|
if (input instanceof ToolDefinition) return named(input.name)
|
|
if (typeof input === "string") return isMode(input) ? new ToolChoice({ type: input }) : named(input)
|
|
return new ToolChoice(input)
|
|
}
|
|
}
|
|
|
|
export const ResponseFormat = Schema.Union([
|
|
Schema.Struct({ type: Schema.Literal("text") }),
|
|
Schema.Struct({ type: Schema.Literal("json"), schema: JsonSchema }),
|
|
Schema.Struct({ type: Schema.Literal("tool"), tool: ToolDefinition }),
|
|
]).pipe(Schema.toTaggedUnion("type"))
|
|
export type ResponseFormat = Schema.Schema.Type<typeof ResponseFormat>
|
|
|
|
export class LLMRequest extends Schema.Class<LLMRequest>("LLM.Request")({
|
|
id: Schema.optional(Schema.String),
|
|
model: ModelRef,
|
|
system: Schema.Array(SystemPart),
|
|
messages: Schema.Array(Message),
|
|
tools: Schema.Array(ToolDefinition),
|
|
toolChoice: Schema.optional(ToolChoice),
|
|
generation: Schema.optional(GenerationOptions),
|
|
providerOptions: Schema.optional(ProviderOptions),
|
|
http: Schema.optional(HttpOptions),
|
|
responseFormat: Schema.optional(ResponseFormat),
|
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
}) {}
|
|
|
|
export namespace LLMRequest {
|
|
export type Input = ConstructorParameters<typeof LLMRequest>[0]
|
|
|
|
export const input = (request: LLMRequest): Input => ({
|
|
id: request.id,
|
|
model: request.model,
|
|
system: request.system,
|
|
messages: request.messages,
|
|
tools: request.tools,
|
|
toolChoice: request.toolChoice,
|
|
generation: request.generation,
|
|
providerOptions: request.providerOptions,
|
|
http: request.http,
|
|
responseFormat: request.responseFormat,
|
|
metadata: request.metadata,
|
|
})
|
|
|
|
export const update = (request: LLMRequest, patch: Partial<Input>) => {
|
|
if (Object.keys(patch).length === 0) return request
|
|
return new LLMRequest({
|
|
...input(request),
|
|
...patch,
|
|
model: patch.model ?? request.model,
|
|
})
|
|
}
|
|
}
|