@tanstack/ai
Version:
Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.
388 lines (342 loc) • 9.64 kB
text/typescript
import type {
AudioGenerationOptions,
ImageGenerationOptions,
TTSOptions,
TranscriptionOptions,
VideoGenerationOptions,
} from './types'
export type GenerationKind =
| 'image'
| 'audio'
| 'tts'
| 'video'
| 'transcription'
type GenerationInputByKind = {
image: Omit<ImageGenerationOptions, 'logger' | 'model'>
audio: Omit<AudioGenerationOptions, 'logger' | 'model'>
tts: Omit<TTSOptions, 'logger' | 'model'>
video: Omit<VideoGenerationOptions, 'logger' | 'model'>
transcription: Omit<TranscriptionOptions, 'logger' | 'model'>
}
export interface GenerationParams<TKind extends GenerationKind> {
input: GenerationInputByKind[TKind]
forwardedProps: Record<string, unknown>
threadId?: string
runId?: string
}
const generationKinds = [
'image',
'audio',
'tts',
'video',
'transcription',
] as const satisfies ReadonlyArray<GenerationKind>
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function hasOwnKey(value: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(value, key)
}
function isGenerationEnvelope(body: unknown): body is Record<string, unknown> {
return (
isRecord(body) &&
(hasOwnKey(body, 'data') || hasOwnKey(body, 'forwardedProps'))
)
}
function assertGenerationKind(kind: unknown): asserts kind is GenerationKind {
if (!generationKinds.includes(kind as GenerationKind)) {
throw new Error(
`Unsupported generation kind: ${String(
kind,
)}. Expected one of ${generationKinds.join(', ')}.`,
)
}
}
function assertInputForKind(
kind: GenerationKind,
input: unknown,
): asserts input is GenerationInputByKind[GenerationKind] {
if (!isRecord(input)) {
throw new Error(`Generation ${kind} input must be an object.`)
}
const requiredKey =
kind === 'tts' ? 'text' : kind === 'transcription' ? 'audio' : 'prompt'
if (!hasOwnKey(input, requiredKey)) {
throw new Error(`Generation ${kind} input must include ${requiredKey}.`)
}
}
function isInputForKind(kind: GenerationKind, input: unknown): boolean {
if (!isRecord(input)) return false
const requiredKey =
kind === 'tts' ? 'text' : kind === 'transcription' ? 'audio' : 'prompt'
return hasOwnKey(input, requiredKey)
}
function forwardedPropsFromEnvelope(
envelope: Record<string, unknown>,
): Record<string, unknown> {
if (!hasOwnKey(envelope, 'forwardedProps')) {
return {}
}
if (!isRecord(envelope.forwardedProps)) {
throw new Error('Generation envelope forwardedProps must be an object.')
}
return envelope.forwardedProps
}
function optionalStringField(
envelope: Record<string, unknown>,
key: 'threadId' | 'runId',
): string | undefined {
if (!hasOwnKey(envelope, key)) {
return undefined
}
const value = envelope[key]
if (typeof value !== 'string') {
throw new Error(`Generation envelope ${key} must be a string.`)
}
return value
}
function generationIdentityFields(envelope: Record<string, unknown>): {
threadId?: string
runId?: string
} {
const identity: {
threadId?: string
runId?: string
} = {}
const threadId = optionalStringField(envelope, 'threadId')
const runId = optionalStringField(envelope, 'runId')
if (threadId !== undefined) identity.threadId = threadId
if (runId !== undefined) identity.runId = runId
return identity
}
export function generationParamsFromBody<TKind extends GenerationKind>(
kind: TKind,
body: unknown,
): GenerationParams<TKind> {
assertGenerationKind(kind)
if (isInputForKind(kind, body)) {
assertInputForKind(kind, body)
return {
input: body as GenerationInputByKind[TKind],
forwardedProps: {},
}
}
if (!isGenerationEnvelope(body)) {
assertInputForKind(kind, body)
return {
input: body as GenerationInputByKind[TKind],
forwardedProps: {},
}
}
if (!hasOwnKey(body, 'data')) {
throw new Error(`Generation ${kind} envelope must include data.`)
}
const input = body.data
assertInputForKind(kind, input)
const forwardedProps = forwardedPropsFromEnvelope(body)
return {
input: input as GenerationInputByKind[TKind],
forwardedProps,
...generationIdentityFields(body),
}
}
export async function generationParamsFromRequest<TKind extends GenerationKind>(
kind: TKind,
request: Request,
): Promise<GenerationParams<TKind>> {
let body: unknown
try {
body = await request.json()
} catch (error) {
throw new Error('Invalid JSON request body.', { cause: error })
}
if (!isRecord(body)) {
throw new Error('Generation request body must be a JSON object.')
}
return generationParamsFromBody(kind, body)
}
export enum EventType {
TEXT_MESSAGE_START = 'TEXT_MESSAGE_START',
TEXT_MESSAGE_CONTENT = 'TEXT_MESSAGE_CONTENT',
TEXT_MESSAGE_END = 'TEXT_MESSAGE_END',
TEXT_MESSAGE_CHUNK = 'TEXT_MESSAGE_CHUNK',
TOOL_CALL_START = 'TOOL_CALL_START',
TOOL_CALL_ARGS = 'TOOL_CALL_ARGS',
TOOL_CALL_END = 'TOOL_CALL_END',
TOOL_CALL_CHUNK = 'TOOL_CALL_CHUNK',
TOOL_CALL_RESULT = 'TOOL_CALL_RESULT',
THINKING_START = 'THINKING_START',
THINKING_END = 'THINKING_END',
THINKING_TEXT_MESSAGE_START = 'THINKING_TEXT_MESSAGE_START',
THINKING_TEXT_MESSAGE_CONTENT = 'THINKING_TEXT_MESSAGE_CONTENT',
THINKING_TEXT_MESSAGE_END = 'THINKING_TEXT_MESSAGE_END',
STATE_SNAPSHOT = 'STATE_SNAPSHOT',
STATE_DELTA = 'STATE_DELTA',
MESSAGES_SNAPSHOT = 'MESSAGES_SNAPSHOT',
ACTIVITY_SNAPSHOT = 'ACTIVITY_SNAPSHOT',
ACTIVITY_DELTA = 'ACTIVITY_DELTA',
RAW = 'RAW',
CUSTOM = 'CUSTOM',
RUN_STARTED = 'RUN_STARTED',
RUN_FINISHED = 'RUN_FINISHED',
RUN_ERROR = 'RUN_ERROR',
STEP_STARTED = 'STEP_STARTED',
STEP_FINISHED = 'STEP_FINISHED',
REASONING_START = 'REASONING_START',
REASONING_MESSAGE_START = 'REASONING_MESSAGE_START',
REASONING_MESSAGE_CONTENT = 'REASONING_MESSAGE_CONTENT',
REASONING_MESSAGE_END = 'REASONING_MESSAGE_END',
REASONING_MESSAGE_CHUNK = 'REASONING_MESSAGE_CHUNK',
REASONING_END = 'REASONING_END',
REASONING_ENCRYPTED_VALUE = 'REASONING_ENCRYPTED_VALUE',
}
export {
toolDefinition,
type AnyClientTool,
type ClientTool,
type InferToolInput,
type InferToolName,
type InferToolOutput,
type ApprovalCapabilityOf,
type ApprovalSchemaConfig,
type ApprovalSchemaOf,
type InputSchemaOf,
type OutputSchemaOf,
type NoSchema,
type ToolDefinition,
type ToolDefinitionConfig,
type ToolDefinitionInstance,
} from './activities/chat/tools/tool-definition'
export {
hashSchemaInput,
normalizeApprovalSchema,
type NormalizedApprovalSchema,
type NormalizedSchemaInput,
} from './activities/chat/tools/approval-schema'
export {
canonicalInterruptJson,
cloneAndDeepFreezeJson,
digestInterruptJson,
} from './interrupt-serialization'
export {
convertSchemaToJsonSchema,
isStandardSchema,
parseWithStandardSchema,
validateWithStandardSchema,
} from './activities/chat/tools/schema-converter'
export {
convertMessagesToModelMessages,
generateMessageId,
modelMessageToUIMessage,
modelMessagesToUIMessages,
normalizeToUIMessage,
uiMessageToModelMessages,
} from './activities/chat/messages'
export {
BatchStrategy,
CompositeStrategy,
defaultJSONParser,
ImmediateStrategy,
parsePartialJSON,
PartialJSONParser,
PunctuationStrategy,
StreamProcessor,
WordBoundaryStrategy,
} from './activities/chat/stream/index'
export type {
ChunkRecording,
ChunkStrategy,
InternalToolCallState,
JSONParser,
ProcessorResult,
ProcessorState,
StreamProcessorEvents,
StreamProcessorOptions,
ToolCallState,
ToolResultState,
} from './activities/chat/stream/index'
export { uiMessagesToWire } from './utilities/ag-ui-wire'
export type { WireMessage } from './utilities/ag-ui-wire'
export type {
AudioPart,
ContentPart,
ContentPartDataSource,
ContentPartSource,
ContentPartUrlSource,
CustomEvent,
DocumentPart,
ImagePart,
MediaInputMetadata,
MediaInputRole,
MediaPrompt,
MediaPromptPart,
MessagePart,
ModelMessage,
PersistedArtifactActivity,
PersistedArtifactRef,
PersistedArtifactRole,
Interrupt,
RunAgentResumeItem,
RunErrorEvent,
RunFinishedEvent,
RunFinishedOutcome,
SchemaInput,
StreamChunk,
StructuredOutputPart,
TextPart,
ThinkingPart,
ToolCall,
ToolCallPart,
ToolResultPart,
UIMessage,
UIResourcePart,
VideoPart,
InferSchemaType,
} from './types'
// Enumerated, not `export *` — see the matching note in `index.ts`. The
// interrupt protocol surface is a commitment, so it is published field by
// field.
export {
INTERRUPT_BINDING_VERSION,
canonicalizeInterruptResolutions,
} from './interrupts'
export type {
BatchInterruptError,
BatchInterruptErrorCode,
InterruptBinding,
InterruptCorrelation,
InterruptSubmissionError,
ItemInterruptError,
ItemInterruptErrorCode,
ToolApprovalResolution,
UnopenedInterruptBinding,
} from './interrupts'
export {
INTERRUPT_BINDING_METADATA_KEY,
readInterruptBinding,
readUnopenedInterruptBinding,
withInterruptBinding,
withoutInterruptBinding,
} from './interrupt-resume'
export type {
AudioVisualization,
RealtimeAdapter,
RealtimeConnection,
RealtimeError,
RealtimeErrorCode,
RealtimeEvent,
RealtimeEventHandler,
RealtimeEventPayloads,
RealtimeMessage,
RealtimeMessagePart,
RealtimeMode,
RealtimeSessionConfig,
RealtimeStatus,
RealtimeToken,
RealtimeAudioPart,
RealtimeImagePart,
RealtimeTextPart,
RealtimeToolCallPart,
RealtimeToolResultPart,
VADConfig,
} from './realtime/types'