- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Extend Adapter
extendAdapter adds your model ids to an existing text adapter. openaiText('my-fine-tuned-model') type-checks after you register that id.
The built-in models keep their types, and your models get the types that you declare.
Quick setup#
1. Declare your models#
Wrap the factory with extendAdapter. Describe each model with createModel: the name, then the input types it can read.
// lib/ai/my-openai.ts
import { createModel, extendAdapter } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export const myOpenai = extendAdapter(openaiText, [
createModel('my-fine-tuned-model', ['text', 'image']),
createModel('my-other-model', ['text']),
])2. Use the new factory#
Use myOpenai where you used openaiText:
import { chat } from '@tanstack/ai'
import { myOpenai } from './lib/ai/my-openai'
const stream = chat({
adapter: myOpenai('my-fine-tuned-model'),
messages: [{ role: 'user', content: 'Hello!' }],
threadId: 'thread-1',
})
for await (const chunk of stream) {
console.log(chunk)
}Your model name now autocompletes, and the request goes to the provider with that name.
Use the built-in models and the factory config#
The new factory still takes every model of the original factory. It also takes the same config argument:
import { createModel, extendAdapter } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const myOpenai = extendAdapter(openaiText, [
createModel('my-fine-tuned-model', ['text', 'image']),
])
// A built-in model, with its normal types
const builtIn = myOpenai('gpt-6.1-sol')
// Your model, with a config for the client
const custom = myOpenai('my-fine-tuned-model', {
baseURL: 'https://my-proxy.com/v1',
timeout: 30000,
})Example: an OpenAI-compatible proxy#
Give types to the models that your proxy serves:
import { createModel, extendAdapter } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const proxyAdapter = extendAdapter(openaiText, [
createModel('llama-3.1-70b', ['text']),
createModel('mixtral-8x7b', ['text']),
])
const adapter = proxyAdapter('llama-3.1-70b', {
baseURL: 'https://my-llm-proxy.com/v1',
})Example: a fine-tuned model on Anthropic#
extendAdapter works with every adapter factory, for example anthropicText:
import { chat, createModel, extendAdapter } from '@tanstack/ai'
import { anthropicText } from '@tanstack/ai-anthropic'
const myAnthropic = extendAdapter(anthropicText, [
createModel('ft:claude-3-opus:my-org:custom-task:abc123', ['text', 'image']),
])
const stream = chat({
adapter: myAnthropic('ft:claude-3-opus:my-org:custom-task:abc123'),
messages: [{ role: 'user', content: 'Analyze this text.' }],
threadId: 'thread-1',
})How it works#
extendAdapter changes only the types. At runtime, it returns the original factory.
- The library does not check your model names. The factory gets exactly what you pass.
- The API of the provider decides if the model name is valid.
- There is no runtime cost.
- A custom model has no reasoning data, so chat() does not take a reasoning option for it.
Check what a model accepts#
A user attaches an image, but the selected model reads only text. You want to know that before you send the request. Read inputModalities on the text adapter:
import { openaiText } from '@tanstack/ai-openai'
const adapter = openaiText('gpt-6.1-sol')
console.log(adapter.inputModalities) // ['image', 'text']
if (!adapter.inputModalities?.includes('image')) {
console.log('This model cannot read images. Pick another model.')
}- The list comes from the model metadata of the provider package.
- It is undefined when the package does not know the model, for example a custom model from extendAdapter. Treat undefined as "not known", not as "text only".
Do you write your own adapter? See Set inputModalities in your own adapter.
Advanced#
Input types#
The second argument of the positional form lists the input types of the model:
import { createModel } from '@tanstack/ai'
const models = [
createModel('text-only-model', ['text']),
createModel('multimodal-model', ['text', 'image', 'audio']),
] as constThe input types are 'text', 'image', 'audio', 'video', and 'document'.
Capabilities-object form#
A model definition (ExtendedModelDef) has three required fields: name, input, and modelOptions. It also has two optional fields: features and tools.
To set the typed modelOptions, the features, or the provider tools, give createModel a capabilities object as the second argument:
import { createModel } from '@tanstack/ai'
import type { OpenAITextProviderOptions } from '@tanstack/ai-openai'
// Type brand for the provider options. The value is not used at runtime.
const modelOptions: OpenAITextProviderOptions = {}
const reasoner = createModel('my-reasoner', {
input: ['text'],
features: ['reasoning', 'structured_outputs'],
tools: ['web_search'],
modelOptions,
})- input: the input types, the same as in the positional form. If you do not set it, the value is ['text'].
- features: the feature flags of the model, for example 'reasoning' or 'structured_outputs'.
- tools: the provider tools of the model, for example 'web_search'.
- modelOptions: a type brand for the provider options of the model. The value is not used at runtime. Declare an empty object with the type of the provider options.
Type errors for unknown names#
The new factory rejects a name that is not in either list:
import { createModel, extendAdapter } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
const myOpenai = extendAdapter(openaiText, [createModel('custom-model', ['text'])])
// OK: a built-in model, with its built-in types
const a1 = myOpenai('gpt-6.1-sol')
// OK: your model, with the types that you declared
const a2 = myOpenai('custom-model')
// Type error: the name is not known.
// The check works when you assign the result to a variable.
const invalid = myOpenai('nonexistent-model')Write your own adapter#
Do you build a full adapter for a new provider? Extend Adapter: Advanced shows how to:
- Set inputModalities.
- Add reasoning levels.
- Send the prompt cache.
- Send mid-conversation changes.
- Send assistant blocks in order.