Class References
Function References
- text
- textOptions
- combineStrategies
- convertMessagesToModelMessages
- convertZodToJsonSchema
- createReplayStream
- embedding
- generateMessageId
- maxIterations
- messages
- modelMessageToUIMessage
- modelMessagesToUIMessages
- normalizeToUIMessage
- parsePartialJSON
- summarize
- toServerSentEventsStream
- toServerSentEventsResponse
- toolDefinition
- uiMessageToModelMessages
- realtimeToken
- untilFinishReason
Interface References
- AIAdapter
- AIAdapterConfig
- AgentLoopState
- ApprovalRequestedStreamChunk
- AudioPart
- AudioVisualization
- BaseStreamChunk
- TextCompletionChunk
- TextOptions
- ChunkRecording
- ChunkStrategy
- ClientTool
- ContentPartSource
- ContentStreamChunk
- DefaultMessageMetadataByModality
- DocumentPart
- DoneStreamChunk
- EmbeddingOptions
- EmbeddingResult
- ErrorStreamChunk
- ImagePart
- InternalToolCallState
- JSONParser
- ModelMessage
- ProcessorResult
- ProcessorState
- RealtimeAdapter
- RealtimeClientOptions
- RealtimeConnection
- RealtimeMessage
- RealtimeSessionConfig
- RealtimeToken
- RealtimeTokenAdapter
- RealtimeTokenOptions
- ResponseFormat
- ServerTool
- StreamProcessorEvents
- StreamProcessorHandlers
- StreamProcessorOptions
- SummarizationOptions
- SummarizationResult
- TextPart
- ThinkingPart
- ThinkingStreamChunk
- Tool
- ToolCall
- ToolCallPart
- ToolCallStreamChunk
- ToolConfig
- ToolDefinition
- ToolDefinitionConfig
- ToolDefinitionInstance
- ToolInputAvailableStreamChunk
- ToolResultPart
- ToolResultStreamChunk
- UIMessage
- VideoPart
Type Alias References
- AgentLoopStrategy
- AnyClientTool
- TextStreamOptionsForModel
- TextStreamOptionsUnion
- ConstrainedContent
- ConstrainedModelMessage
- ContentPart
- ContentPartForModalities
- ExtractModalitiesForModel
- ExtractModelsFromAdapter
- InferToolInput
- InferToolName
- InferToolOutput
- MessagePart
- RealtimeEvent
- RealtimeMessagePart
- RealtimeMode
- RealtimeStatus
- ModalitiesArrayToUnion
- Modality
- StreamChunk
- StreamChunkType
- ToolCallState
- ToolResultState
Variable References
@tanstack/ai-angular
Angular signal-based bindings for TanStack AI, providing convenient Angular bindings for the headless client.
Injection context requirement: Every inject* function in this package calls Angular's inject() internally. They must be called within an Angular injection context — a component or directive class field initializer, the constructor, or inside runInInjectionContext. Calling them outside an injection context will throw a runtime error.
Installation#
npm i @tanstack/ai-angularpnpm add @tanstack/ai-angularyarn add @tanstack/ai-angularbun add @tanstack/ai-angularinjectRegisterWebMCPTools(tools, options?)#
Register executable client tools for the current Angular injection owner. Angular removes them when it destroys that owner.
For a complete setup and behavior guide, see WebMCP Tools.
import { Component } from "@angular/core";
import {
injectRegisterWebMCPTools,
type InjectRegisterWebMCPToolsOptions,
} from "@tanstack/ai-angular";
import { searchProducts } from "./tools";
const tools = [searchProducts];
const options: InjectRegisterWebMCPToolsOptions<typeof tools> = {
onError(error) {
console.error(error);
},
};
@Component({ selector: "app-products", standalone: true, template: "" })
export class ProductsComponent {
registration = injectRegisterWebMCPTools(tools, options);
}InjectRegisterWebMCPToolsOptions<TTools, TContext> contains toolOptions, context, and onError. The injectable owns the registration signal.
The context field is required when a tool declares a required runtime context. Call this function only in an Angular injection context.
injectPageWebMCPTools(options?)#
Read the WebMCP tools on the page as client tools. The signal starts empty and updates when the page adds or removes a tool. Pass the signal to injectChat as tools. Call it in an injection context.
import { Component } from "@angular/core";
import { injectPageWebMCPTools } from "@tanstack/ai-angular";
@Component({ selector: "app-page-tools", standalone: true, template: "" })
export class PageTools {
pageTools = injectPageWebMCPTools({
filter: (tool) => tool.origin === location.origin,
});
}filter skips a tool when it returns false. onError gets a failed WebMCP read. For a complete guide, see Page WebMCP Tools in Chat.
injectChat(options?)#
Main injectable for managing chat state in Angular with full type safety.
import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
template: `...`,
})
export class ChatComponent {
// injectChat is called in a field initializer — valid injection context.
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}Options#
Extends ChatClientOptions from @tanstack/ai-client (minus internal state callbacks):
- connection - Connection adapter (required, or use fetcher)
- fetcher? - Direct async function for one-shot generation (alternative to connection)
- tools? - Array of client tool implementations (with .client() method). Reactive: pass a Signal or getter to change the tools after the chat is created.
- initialMessages? - Initial messages array
- threadId? - The only identity for this chat. Required when persistence is on. If omitted, minted after mount.
- forwardedProps? - Arbitrary client-controlled JSON forwarded to the server in the AG-UI RunAgentInput.forwardedProps field. Reactive — accepts a plain value, an Angular Signal, or a zero-arg getter; changes sync automatically via effect
- body? - Deprecated. Use forwardedProps instead. Still works for backward compatibility; values are merged into forwardedProps on the wire. Reactive (same forms as forwardedProps)
- byok? - Optional BYOK keyring from defineByok. On each send the client prepares the resolved provider and stamps x-byok-* request headers. Keys never go in the body
- byokProvider? - Optional function that returns the provider slug for this chat. If it returns a slug, only that key is prepared and sent. Otherwise the merged provider from forwardedProps, body, and per-call sendMessage body is used. Later sources win. If no slug resolves, the send throws instead of attaching every stored key
- context? - Typed client-local runtime context passed to client tool implementations. Reactive (same forms). This value is not serialized to the server
- live? - Enable live subscription mode (auto-subscribes/unsubscribes). Reactive (same forms)
- outputSchema? - Standard-schema-compatible schema (Zod, Valibot, ArkType, or JSON Schema). When provided, adds typed partial and final signals to the return value
- persistence? - Persistence configuration
- devtools? - Display options for TanStack AI Devtools
- onResponse? - Callback when response is received
- onChunk? - Callback when stream chunk is received
- onFinish? - Callback when response finishes
- onError? - Callback when error occurs
- onInterruptStateChange? - Callback when interrupt state changes; context source is hydrate for restored state and live for streamed or client-initiated updates
- onCustomEvent? - Callback for custom stream events
- streamProcessor? - Stream processing configuration
Reactive options (body, forwardedProps, context, live) accept a ReactiveOption<T>, which is one of:
import type { Signal } from "@angular/core";
type ReactiveOption<T> = T | Signal<T> | (() => T);A plain value becomes a constant; a Signal is read directly; a zero-arg getter is wrapped in computed so any signals read inside it are tracked.
Note: Client tools are automatically executed — no onToolCall callback needed!
Returns#
import type { Signal } from "@angular/core";
import type {
UIMessage,
MultimodalContent,
DeepPartial,
} from "@tanstack/ai-angular";
import type { ModelMessage, InferSchemaType } from "@tanstack/ai/client";
import type {
ChatClientState,
ConnectionStatus,
SendMessageOptions,
} from "@tanstack/ai-client";
type TSchema = any;
interface InjectChatResult {
messages: Signal<ReadonlyArray<UIMessage>>;
sendMessage: (
content: string | MultimodalContent,
options?: SendMessageOptions,
) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
clear: () => void;
setMessages: (messages: ReadonlyArray<UIMessage>) => void;
isLoading: Signal<boolean>;
error: Signal<Error | undefined>;
status: Signal<ChatClientState>;
isSubscribed: Signal<boolean>;
connectionStatus: Signal<ConnectionStatus>;
sessionGenerating: Signal<boolean>;
isHydrating: Signal<boolean>;
// Only present when outputSchema is supplied:
partial: Signal<DeepPartial<InferSchemaType<TSchema>>>;
final: Signal<InferSchemaType<TSchema> | null>;
}Note: All reactive state (messages, isLoading, error, status, isSubscribed, connectionStatus, sessionGenerating, isHydrating) is exposed as read-only Angular Signals. Read them by calling them as functions (e.g., chat.messages(), chat.isLoading()). Cleanup is automatic via DestroyRef.onDestroy.
injectByok(client)#
Subscribe to a ByokClient snapshot in Angular. Call it in an injection context. The return value is a read-only Signal.
import { Component } from "@angular/core";
import { injectByok } from "@tanstack/ai-angular";
import { byok } from "./byok";
@Component({
selector: "app-key-status",
standalone: true,
template: `<p>{{ last4() }}</p>`,
})
export class KeyStatusComponent {
snapshot = injectByok(byok);
last4() {
const openai = this.snapshot().status.openai;
return openai && "masked" in openai ? openai.masked : "No key";
}
}snapshot() has status, locked, and prompt. Call byok.update(provider, value) from your own UI to save a key. See Bring Your Own Key.
Connection Adapters#
Re-exported from @tanstack/ai-client for convenience:
import {
fetchServerSentEvents,
fetchHttpStream,
fetchJson,
xhrServerSentEvents,
xhrHttpStream,
stream,
rpcStream,
type ConnectionAdapter,
} from "@tanstack/ai-angular";Example: Basic Chat#
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-chat",
standalone: true,
imports: [CommonModule],
template: `
<ul>
@for (message of chat.messages(); track message.id) {
<li>
<strong>{{ message.role }}:</strong>
@for (part of message.parts; track $index) {
@if (part.type === 'thinking') {
<em>Thinking: {{ part.content }}</em>
} @else if (part.type === 'text') {
<span>{{ part.content }}</span>
}
}
</li>
}
</ul>
<input #input placeholder="Type a message..." />
<button
(click)="chat.sendMessage(input.value); input.value = ''"
[disabled]="chat.isLoading()"
>
Send
</button>
@if (chat.isLoading()) {
<p>Thinking...</p>
}
`,
})
export class ChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}Example: Tool Approval#
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-approval-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (
part.type === 'tool-call' &&
part.state === 'approval-requested' &&
part.approval
) {
<div>
<p>Approve: {{ part.name }}</p>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: true })">
Approve
</button>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: false })">
Deny
</button>
</div>
}
}
}
`,
})
export class ApprovalChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}Example: Client Tools with Type Safety#
import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import {
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
import { toolDefinition } from "@tanstack/ai";
import { z } from "zod";
const updateUIDef = toolDefinition({
name: "updateUI",
description: "Show a notification in the UI",
inputSchema: z.object({ message: z.string(), type: z.string() }),
});
const saveToStorageDef = toolDefinition({
name: "saveToStorage",
description: "Save a value to localStorage",
inputSchema: z.object({ key: z.string(), value: z.string() }),
});
@Component({
selector: "app-typed-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (part.type === 'tool-call' && part.name === 'updateUI') {
<div>Tool executed: {{ part.name }}</div>
}
}
}
`,
})
export class TypedChatComponent {
// Create client implementations
private updateUI = updateUIDef.client((input) => {
// input is fully typed!
return { success: true };
});
private saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});
// Create typed tools array (no 'as const' needed!)
private tools = [this.updateUI, this.saveToStorage];
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
tools: this.tools, // Automatic execution, full type safety
});
}Example: Reactive Options with Signals#
import { Component, signal } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
@Component({
selector: "app-reactive-chat",
standalone: true,
template: `
<button (click)="toggleLanguage()">Toggle Language</button>
@for (message of chat.messages(); track message.id) {
<p>{{ message.role }}: {{ message.parts[0]?.content }}</p>
}
`,
})
export class ReactiveChatComponent {
language = signal("en");
// forwardedProps is reactive — the signal is read on every request
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
forwardedProps: () => ({ language: this.language() }),
});
toggleLanguage() {
this.language.set(this.language() === "en" ? "fr" : "en");
}
}Example: Structured Output#
import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import { z } from "zod";
const recipeSchema = z.object({
title: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
});
@Component({
selector: "app-recipe-chat",
standalone: true,
template: `
<button (click)="chat.sendMessage('Give me a pasta recipe')">Ask</button>
@if (chat.partial().title) {
<h2>{{ chat.partial().title }}</h2>
}
@if (chat.final()) {
<ul>
@for (step of chat.final()!.steps; track $index) {
<li>{{ step }}</li>
}
</ul>
}
`,
})
export class RecipeChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
outputSchema: recipeSchema,
});
}Generation Injectables#
Angular injectables for one-shot generation tasks (images, audio, speech, transcription, summarization, video). All share the same pattern: provide a connection or fetcher, call generate(), and read reactive signals.
injectGeneration(options)#
Base injectable for custom generation types. All specialized injectables below are built on this.
import { Component } from "@angular/core";
import { injectGeneration } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({ selector: "app-custom", standalone: true, template: `...` })
export class CustomGenerationComponent {
gen = injectGeneration({
connection: fetchServerSentEvents("/api/generate/custom"),
});
// Call gen.generate(input), read gen.result(), gen.isLoading(), etc.
}Options: connection?, fetcher?, threadId?, body? (reactive), devtools?, onResult?, onError?, onProgress?, onChunk?
Returns: generate, result, isLoading, error, status, stop, reset, runId. All reactive state is a read-only Signal<T>.
injectGenerateImage(options)#
Image generation injectable. generate() accepts ImageGenerateInput, result is ImageGenerationResult.
import { Component } from "@angular/core";
import { injectGenerateImage } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-image",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A mountain at sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<img [src]="gen.result()!.images[0]!.url" alt="Generated image" />
}
`,
})
export class ImageComponent {
gen = injectGenerateImage({
connection: fetchServerSentEvents("/api/generate/image"),
});
}injectGenerateAudio(options)#
Audio generation injectable (music, sound effects). generate() accepts AudioGenerateInput, result is AudioGenerationResult.
import { Component } from "@angular/core";
import { injectGenerateAudio } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-audio",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'An upbeat electronic track', duration: 10 })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<audio [src]="gen.result()!.audio.url" controls></audio>
}
`,
})
export class AudioComponent {
gen = injectGenerateAudio({
connection: fetchServerSentEvents("/api/generate/audio"),
});
}injectGenerateSpeech(options)#
Text-to-speech injectable. generate() accepts SpeechGenerateInput, result is TTSResult.
injectTranscription(options)#
Audio transcription injectable. generate() accepts TranscriptionGenerateInput, result is TranscriptionResult.
injectSummarize(options)#
Text summarization injectable. generate() accepts SummarizeGenerateInput, result is SummarizationResult.
injectGenerateVideo(options)#
Video generation injectable with job polling. Returns additional jobId and videoStatus signals. Accepts extra onJobCreated? and onStatusUpdate? callbacks.
import { Component } from "@angular/core";
import { injectGenerateVideo } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";
@Component({
selector: "app-video",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A time-lapse of a sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.videoStatus()) {
<p>Status: {{ gen.videoStatus()!.status }}</p>
}
@if (gen.result()) {
<video [src]="gen.result()!.url" controls></video>
}
`,
})
export class VideoComponent {
gen = injectGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
onJobCreated: (jobId) => console.log("Job created:", jobId),
});
}Additional returns (video only):
- jobId: Signal<string | null> — The polling job ID, once the server creates it
- videoStatus: Signal<VideoStatusInfo | null> — Real-time status updates from the polling loop
All generation injectables automatically clean up via DestroyRef.onDestroy.
Injection Context#
Angular's DI system requires that inject() is called during component construction. Every inject* function in this package calls inject() internally. Valid call sites:
import { inject, runInInjectionContext, Injector } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
const injector = inject(Injector);
// Field initializer (recommended)
export class MyComponent {
chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
// Constructor
export class MyComponentAlt {
chat: ReturnType<typeof injectChat>;
constructor() {
this.chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
}
// Inside runInInjectionContext
const chat = runInInjectionContext(injector, () =>
injectChat({ connection: fetchServerSentEvents("/api/chat") }),
);createChatClientOptions(options)#
Helper to create typed chat options (re-exported from @tanstack/ai-client).
import {
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
import { fetchServerSentEvents } from "@tanstack/ai-angular";
import { tool1, tool2 } from "./tools";
// Create typed tools array (no 'as const' needed!)
const tools = [tool1, tool2];
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents("/api/chat"),
tools,
});
type Messages = InferChatMessages<typeof chatOptions>;Types#
Re-exported from @tanstack/ai-angular (sourced from @tanstack/ai-client):
- UIMessage<TTools> - Message type with tool type parameter
- InjectChatOptions<TTools, TSchema, TContext> - Chat injectable options
- InjectChatResult<TTools, TSchema> - Chat injectable return type
- ReactiveOption<T> - Union of T | Signal<T> | (() => T) for reactive option fields
- DeepPartial<T> - Recursive partial; used to type the in-flight partial value
- ChatRequestBody - Request body type
- MultimodalContent - Multimodal content type for sendMessage
- ConnectionAdapter - Connection adapter interface
- InferChatMessages<T> - Extract message type from options
- GenerationClientState - Generation lifecycle state
- ImageGenerateInput - Image generation input type
- AudioGenerateInput - Audio generation input type
- SpeechGenerateInput - Speech generation input type
- TranscriptionGenerateInput - Transcription input type
- SummarizeGenerateInput - Summarization input type
- VideoGenerateInput - Video generation input type
- VideoGenerateResult - Video generation result type
- VideoStatusInfo - Video job status info
Tool authoring types — import directly from @tanstack/ai (not re-exported by @tanstack/ai-angular):
- toolDefinition() - Create isomorphic tool definition
- ToolDefinitionInstance - Tool definition type
- ClientTool - Client tool type
- ServerTool - Server tool type
Next Steps#
- Getting Started - Learn the basics
- Tools Guide - Learn about the isomorphic tool system
- Client Tools - Learn about client-side tools