@tanstack/ai
0.71.0 · 21 Oct 2026MIT593.9k / weekRepositoryHomepagenpmAtom feed
Latest releases
All 103 releases- 0.71.x · latest line
0.71.0
21 Oct 2026Add computer use that runs the same code on OpenAI and on Anthropic. A driver controls the machine. The Playwright and sandbox drivers give each thread its own machine.
New packages:
Changes in existing packages:
const computerUse = createComputerUse({ driver: playwrightComputer({ startUrl: 'https://tanstack.com' }), }) chat({ adapter, messages, threadId, tools: computerUse.tools, middleware: [computerUse.middleware], interrupts: [computerUse.interrupt], }) await computerUse.close(threadId) // one thread await computerUse.close() // every thread, and the browserThe
ai-coreagent skill routes computer use to@tanstack/ai-computer-useand its three drivers.
- 0.70.x · maintenance
0.70.0
19 Oct 2026Subagent limits, and provider keys for agents.
A subagent can now return a plain value and call any activity.
Check a raw JSON Schema tool input before the tool runs. This covers plain JSON Schema tools and every MCP tool.
Behavior change:
Migration: if a JSON Schema tool must accept input that its schema rejects, widen the schema.
Minor Changes
Subagent limits, and provider keys for agents.
subagents.limits:maxDepth,maxConcurrent,maxCalls, andtimeoutMsfor the whole tree of children the model starts through tools. The tree shares one budget (SubagentBudget), which a child'sctx.chat({ subagents })passes on, so a child cannot reset it. A refused start reaches the model as a tool error.subagents.keysandctx.keys(ProviderKeys:get,require, andadapter). Pass an object by provider id, for example{ openai: userKey }, or a function, for example(provider) => getByokKey(request, provider). An agent builds akeyedAdapterwithawait ctx.keys.adapter(adapter). A missing key falls back to the provider'senvnames. When there is no key at all, the call throws an error that names the env var. A child'sctx.chat({ subagents })passes the keys on to its own children. Keys that the child sets itself win for those providers.
A subagent can now return a plain value and call any activity.
defineAgent'sruncan return a promise of any value, such as an image result. The value arrives onSUBAGENT_FINISHED.result, and the parent model gets it as the tool result. A very long string inside an object result (for example a base64 image) reaches the parent model as a short note (compactForModel). A string result also streams as the child's text. The result ofctx.chat({ stream: false })streams like the chat stream.rungets the activities onctx(ctx.chat,ctx.generateImage,ctx.generateVideo, and the rest), which fill in the thread id, a run id, and the abort signal.ctx.forwardholds the fields for a plainchat()call. The optionalproducesfield says what an agent makes.A host can set
subagents.binding(SubagentBinding) to add chat and generation middleware to every activity call of a child. A child'sctx.chat({ subagents })passes it down to its own children.Children inherit the prompt cache retention of the parent call.
The chat middleware context has
subagentNameandparentSubagentRunId.chat()takes both as options.RunRecordgets optional fields for host sessions (kind,activity,agent,result,artifacts,principal,leaseOwner,leaseExpiresAt,checkpoint). The memory run stores keep them.
Check a raw JSON Schema tool input before the tool runs. This covers plain JSON Schema tools and every MCP tool.
Behavior change:
A tool with a JSON Schema
inputSchemanow gets a tool error (stateoutput-error) when the input does not fit the schema. The tool does not run. Before, any input ran.The check runs at the same place as the Standard Schema check: after approval edits and middleware
onBeforeToolCall.The draft comes from
$schema(draft-04, draft-07, 2019-09, 2020-12). With no$schema, the check uses 2020-12.formatis not checked.OpenAPI 3.0 schemas work:
nullable: truelets the field benull, and a booleanexclusiveMinimumorexclusiveMaximumworks in every draft.The error names each failed location (up to 10) and shows the received arguments:
Input validation failed for tool <name>: #/count: ....A schema that the check cannot use, such as one with a remote
$ref, gives a tool error on each call.The check uses
@cfworker/json-schema. It does not compile code, so it runs on Cloudflare Workers.
Migration: if a JSON Schema tool must accept input that its schema rejects, widen the schema.
Add the
onModelErrormiddleware hook andctx.retry(). When a model call fails before it streams output,onModelErrorgets theRUN_ERROR. Callctx.retry()there to drop the failed call, runonConfigagain, and call the model again. A model call retries at most 2 times this way.ctx.retry()throws in any other hook.Keep the provider's raw tool argument string for replay.
Adapters now send the raw argument string as
argsonTOOL_CALL_END.The tool call in the history keeps that exact string when it is the same JSON value as the parsed input, so the next request sends the provider's own bytes back (for example
1.50, or an id pastNumber.MAX_SAFE_INTEGER). When the parsed input differs, only the changed values are written again.The
TOOL_CALL_ENDthat reaches the client carries the same string inmetadata.tanstack.args, so the client history matches the server history. It no longer also carriesmetadata.tanstack.input: in-processchat()andStreamProcessorparseinputfromargs. Run the client and the server on the same release: an older client ignoresargs, so it builds the tool input from the streamed deltas only, and a tool call with no deltas (for example an Anthropic server tool) gets no input.History converters for Bedrock and the Gemini Interactions API send malformed tool arguments as
{}instead of failing the request.Gemini Interactions: the initial
step.argumentssnapshot goes out as aTOOL_CALL_ARGSdelta only when no argument deltas follow, so the deltas always add up to the final arguments.Malformed tool arguments are no longer turned into
{}by an adapter. The tool gets a "Failed to parse tool arguments as JSON" error that the model can repair. Mistral still ends the run with an error on malformed arguments, as before.
Continue a child. With
withPersistenceon the parentchat(), an agent tool result gives the model the child'ssubagentRunId, and a later call of the same agent tool withsessionIdcontinues that child from its stored transcript. With a host that setssubagents.binding.start,background: truestarts the child and returns at once. These fields sit next to the agent's own fields, and each agent tool has them only when the run can use them. When the run usessessionIdorbackground, that name is reserved:chat()throws when an agent'sinputSchemauses it. An agent withoutinputSchematakes the task as text inprompt, and the child gets it as the last user message. Agent tools also support thedetachfield of the tool context.The card keeps the checked input of an agent with
inputSchema. On the client,part.subagent.inputhas it, typed frominputSchemawhen the agent declaration goes touseChat({ subagents }).@tanstack/aiaddsLoadChildCapability,getLoadChild, andprovideLoadChild, so a persistence middleware can load a stored child. A provider listsLoadChildCapabilityinprovides.withPersistencein@tanstack/ai-persistenceprovides it: it stores the task of each child's first call and the thread that started it, and loads only the children of the calling thread.Repair common model mistakes in a tool input before the final input check.
When a JSON Schema tool's input (and every MCP tool's input) fails the check, it is repaired and checked again:
"5"as5(decimal text only),"true"astrue,5as"5"for a string, andnullas"",0, orfalsefor a required field. Anullfor an optional field that does not allownullis removed. The repair follows unions,$ref,if/then/else,prefixItems,patternProperties, andadditionalProperties.A Standard Schema runs first as written. When it fails and the schema can export JSON Schema (Zod, ArkType, Valibot), the input is repaired and the schema runs again. A failure reports the issues of the first run.
The received arguments do not change. The tool gets the repaired copy.
New
chat({ repairToolInput })option.falseturns the repair off. A function replaces the built-in repair: it gets{ tool, toolCall, input, error }and returns the input to check again, orundefinedto send the validation error to the model.
Behavior change: a call that failed the check only because of these mistakes now runs with the repaired input.
Check a tool input at the last moment, just before the tool runs or the client gets the call.
Behavior change:
The tool's Standard Schema
inputSchemanow checks the input after approvaleditedArgsand after middlewareonBeforeToolCall. Before, atransformArgsvalue reachedexecuteor the client without a check. An input that does not fit the schema now becomes a tool error (stateoutput-error), and the tool does not run. An approval edit is also still checked when the run resumes.onBeforeToolCallgets the parsed input before the schema check. It has no schema defaults or transforms yet, and it can be invalid.Async Standard Schema input validators are awaited. Before, every call to such a tool failed with an input validation error.
onBeforeToolCallnow also runs for a client tool before the server sends the call to the client. This includes a client request at an interrupt boundary. It does not run while a call waits for approval or for a denied call. When the client result comes back, the result is kept, with no second hook run and no second check.otelMiddlewarerecords noexecute_toolspan for a client tool, because the server does not run it.A validation error after the hook runs
onAfterToolCallwithok: false.The tool error that the model sees now also shows the arguments that the model sent. When an approval edit or middleware changed them, the error says so.
While a call waits for approval, an approved client tool in the same batch is now sent to the client. Before, it waited for the next round trip.
When the only call that waits for approval fails its input check, the other calls of the batch now run. Before, they got no result.
Migration:
If a middleware
transformArgsor an approval edit sends a value that the schema rejects, make the value fit the schema, or widen the schema.If
onBeforeToolCallreadshookCtx.args, check its shape first. Do not expect schema defaults there.If a middleware counts or blocks calls in
onBeforeToolCalland must see only server tools, return early whenhookCtx.tool?.executeis missing.
Patch Changes
decide()now reports token usage when the provider answered but the answers cannot be mapped, for example when an answer is missing, has the wrong type, or has no probability for its value. Before,decide()threw without reporting the usage that the provider already billed. TheonUsagemiddleware hook and theevaluate:usageevent now get that usage once, beforeonErrorruns.onFinishstill does not run for a failed call, anddecide()throws the mapping error. If anonUsagehook throws, that error is thrown instead, as it is for a successful call. The mapping error is still logged.Keep custom metadata from
REASONING_MESSAGE_STARTon the assistantUIMessage. The thinking text was already kept. Metadata that arrived only on the start event was dropped.File each model call on its own assistant message. Reasoning that arrives before the call's text id, a tool call whose parent id is new, and a signature that names an earlier step stay on the message that owns them.
Fix subagent limit and continue edges.
A background
startthat throws gives themaxCallsslot back.maxConcurrentcounts that child untildonesettles.startnow returnsdone.A schema result that is not an object does not drop
sessionIdorbackground. A top-levelanyOfschema still shows those fields.A suspended child with no stored
basekeeps its transcript.When the parent deadline is already past, the model gets
subagent limit reached (no time left).
- 0.69.x · maintenance
0.69.0
13 Oct 2026Keep the order of Claude's thinking, text, and tool calls in one answer.
chat()can now retry a rate-limited model call by itself. Passretry: { maxRetries, maxWaitMs? }. When the provider sends a wait time (retryAfterMs) with an error,chat()waits that long, then calls the model again.retryis off by default.chat()now also sendsRUN_STARTEDbefore aRUN_ERRORwhen the adapter failed before it sent a start.Text adapters now set one stable code on
RUN_ERRORwhen the input does not fit in the model's context window:code: 'context_overflow'.@tanstack/aiexports it asCONTEXT_OVERFLOW_CODE. The provider's own text stays inmessage.if (chunk.type === 'RUN_ERROR' && chunk.code === CONTEXT_OVERFLOW_CODE) { // shorten the messages, then call chat() again }These adapters set it: OpenAI and the other OpenAI-compatible adapters (from
code: 'context_length_exceeded'), Anthropic, Gemini, Bedrock (Converse, Chat Completions, and Responses), OpenRouter, Grok, Cloudflare Workers AI, and BytePlus.Changes to code that reads the old values:
Minor Changes
Keep the order of Claude's thinking, text, and tool calls in one answer.
Claude can answer with thinking, a tool call, more thinking, and another tool call. The stream, the stored messages, the client wire, and the next request to Claude keep that order.
ModelMessagehas a new optionalblockOrderfield. The library writes it only when the order is not the default (thinking, then text, then tool calls), so a message in the default order does not change.orderedAssistantBlocks(message)gives adapter authors the blocks in order.Each Claude thinking block ends with its own
REASONING_MESSAGE_ENDandREASONING_END.Text after a second thinking block starts a new text part.
On the client wire, an answer in another order goes out as ordered rows. A row that exists only for the order has
metadata.tanstack.continues, and the server joins it back into one message. AUIMessagethat holds two model calls with a tool result between them reaches the server as assistant, tool, assistant, for every adapter.
chat()can now retry a rate-limited model call by itself. Passretry: { maxRetries, maxWaitMs? }. When the provider sends a wait time (retryAfterMs) with an error,chat()waits that long, then calls the model again.It retries only when the provider sent a wait time.
It does not retry when the wait is longer than
maxWaitMs(default 60 000).It does not retry a call that already streamed output, so no output repeats.
An abort stops the wait. You then get the
RUN_ERROR.It does not retry the structured-output call.
A
maxRetriesormaxWaitMsthat is not valid throws aTypeError.
retryis off by default.chat()now also sendsRUN_STARTEDbefore aRUN_ERRORwhen the adapter failed before it sent a start.Text adapters now set one stable code on
RUN_ERRORwhen the input does not fit in the model's context window:code: 'context_overflow'.@tanstack/aiexports it asCONTEXT_OVERFLOW_CODE. The provider's own text stays inmessage.if (chunk.type === 'RUN_ERROR' && chunk.code === CONTEXT_OVERFLOW_CODE) { // shorten the messages, then call chat() again }These adapters set it: OpenAI and the other OpenAI-compatible adapters (from
code: 'context_length_exceeded'), Anthropic, Gemini, Bedrock (Converse, Chat Completions, and Responses), OpenRouter, Grok, Cloudflare Workers AI, and BytePlus.Changes to code that reads the old values:
OpenAI-compatible adapters gave
code: 'context_length_exceeded'. They now give'context_overflow'.rawEventstill has the provider's code.Anthropic, Grok, OpenRouter, and BytePlus gave the HTTP status or a general bad-request code for this error. Gemini and Bedrock Converse gave no code.
The Anthropic adapter now sends
RUN_ERRORwith this code when the model stops withstop_reason: 'model_context_window_exceeded'. Before, it sentRUN_FINISHEDwithfinishReason: 'stop', so a cut-off answer looked complete.
New hooks for hosts that run
chat()for a long session.@tanstack/aiexportsrunAgentStream(runs adefineAgentagent outside a parent chat turn),createSubagentId,createSubagentSink,CapabilityRegistry, and theSubagentsBagandSubagentSinktypes. Pass the sink to keep interrupt objects and token usage. The finished event hasinterruptIdsonly.MetadataStorehas optionalgetVersionedandsetIffor compare-and-set writes. The memory store of@tanstack/ai-persistencehas both, and the conformance testkit checks them.toolDefinition({ replay: 'safe' | 'never' })tells a host if it can run the tool again after a crash.The tool context has an optional
detachfield. A host sets it when it can move the call to the background. A tool passes its work todetachand races the two promises.A tool that throws the MCP input-required shape can set the interrupt
reason. The default is'mcp_input'. The interrupt id staysmcp_input_<toolCallId>.
toJsonResponsenow accepts the object fromchat({ outputSchema }). It sends the object as a complete structured output, souseChatwithfetchJsonshows it as astructured-outputpart. Before this change, the call failed withCannot read properties of undefined (reading 'map').toJsonResponsealso awaits a Promise input, sotoJsonResponse(chat({ ... }))works.Add
LogRecordsCapabilityand theLogRecordandLogRecordsWritertypes. A chat middleware can append records to a session log when a host middleware provides one. Read the writer withgetLogRecords(ctx, { optional: true }). It isundefinedwhen no host provides the log, andgetLogRecords(ctx)throws. A host provides the writer insetupwithprovideLogRecords(ctx, writer).@tanstack/aidoes not provide the log.Keep the prompt cache when tools or system prompts are added during a conversation.
Before each model call,
chat()compares the tools and the system prompts with the earlier calls. On a model with a mid-conversation channel, an added tool, or a system prompt added at the end, goes out in the conversation. The start of the request stays the same.openaiTextongpt-5.4-mini,gpt-5.5,gpt-5.6-luna,gpt-5.6-sol,gpt-5.6-terra,gpt-6-astra,gpt-6-luna, andgpt-6-sol: an added tool is anadditional_toolsinput item, and an added prompt is adevelopermessage. The Responses adapter of@tanstack/openai-basesends these when a subclass setsmidConversationChannels, and it sends thenamespaceof such a tool's call back on the next request.anthropicTextonclaude-opus-4-8,claude-opus-5,claude-opus-5-5,claude-fable-5, andclaude-fable-5-1: every request with tools has themid-conversation-tool-changes-2026-07-01beta and one placeholder tool, unless a provider tool such aswebSearchTool()takes part. An added tool hasdefer_loading: trueand atool_additionblock in asystemmessage.Every other model and adapter sends the same request as before.
The channels are on by default only with the provider's own API. A custom
baseURL, a customfetch, an injected client, or theOPENAI_BASE_URL/ANTHROPIC_BASE_URLenvironment variable turns the default off. SetmidConversationChannels: trueto turn the channels on anyway. Setfalseto turn them off anywhere.With prompt caching on (the default), the automatic Claude tool cache marker goes on the last start tool, not on the placeholder or an added tool.
New optional fields:
ModelMessage.midConversationChange,TextOptions.midConversationChanges, andTextAdapter.midConversationChannels, with the typesMidConversationChange,MidConversationChanges, andMidConversationChannels.splitMidConversationChangeshelps adapter authors read the changes.
A text adapter can now have an optional
compact(options)method. It sends the history to the provider's own compaction endpoint, and resolves to the history for the next calls. The options areTextCompactOptions:messages,model,signal,wrapFetch, andsystemPrompts.The OpenAI Responses adapters implement
compactwithPOST /responses/compact. The request sends the system prompts asinstructions. It sends no tools, because the endpoint does not take them. The encryptedcompactionitem goes back to the same model as it is on later requests. A compact result with no compaction item throws.withCompactionhas a newnativeoption. Pass the adapter of thechat()call. When the adapter hascompact, it runs in place ofstrategy. Whencompactfails,strategyruns, andonCompactandcompaction:endedget the failure inerror. After a 404, 405, or 501, that adapter and model use the strategy for the rest of the process. After any other failure, the next compaction triescompactagain. Without the option, nothing changes.Prompt caching is now on by default. Each
chat()call asks the provider to cache the stable start of the request (system prompt, tools, and earlier messages), so repeated requests cost less and answer sooner.New
chat()optionpromptCache:'none' | 'short' | 'long', or{ retention, key }. The default is'short'. The key ispromptCache.key, else thethreadIdorconversationIdthat you give. AthreadIdthatchat()makes up is never the key.To turn it off, pass
promptCache: 'none'. Do this for a large prompt that you send to Claude one time only, because Claude bills the cache write.A middleware can change it for the next model call:
onConfiggetsconfig.promptCacheand can return a new value.Each adapter sends its own fields:
Claude on Anthropic, Bedrock Converse, and OpenRouter gets cache markers.
'long'asks for the 1-hour TTL.OpenAI (Responses and Chat Completions) gets
prompt_cache_key, andprompt_cache_retentionorprompt_cache_optionson'long'.Mistral gets
prompt_cache_key, and usage now reports cache reads.OpenRouter gets
sessionId.
A manual
cache_control,cachePoint,prompt_cache_key, orsessionIdwins over the automatic value.New types:
PromptCacheRetention,PromptCacheOptions, andResolvedPromptCache. Adapters readTextOptions.promptCache.
Add the
reasoningoption tochat(). Set how hard the model thinks in one way for every provider.Pass a level:
off,minimal,low,medium,high,xhigh, ormax. Or pass{ level, summary?, budgetTokens? }.The types allow only the levels of the selected model.
budgetTokensis allowed only on a model that thinks with a token budget.At run time, a level that the model does not have moves to the nearest level that it has.
Without
reasoning, the provider default applies.A middleware can change the value in
onConfig.
Breaking: none. Each adapter package removes its own reasoning fields from
modelOptions. See the changeset of each adapter.chat({ adapter: openaiText('gpt-5.5'), messages, reasoning: 'high' }) chat({ adapter: anthropicText('claude-haiku-4-5'), messages, reasoning: { level: 'medium', budgetTokens: 4000 }, })New exports:
REASONING_LEVELS,supportedReasoningLevels,clampReasoningLevel, and the typesReasoningLevel,ReasoningLevels,ModelReasoning,ReasoningCapability,ReasoningOption,ReasoningOptionFor,ReasoningRequest, andAdapterReasoning.@tanstack/ai/adapter-internalsexportsresolveReasoning,reasoningBudget,DEFAULT_REASONING_BUDGETS, and theModelReasoningCapabilitytype for adapter authors.TextAdapterandBaseTextAdaptertake a new last type parameter for the reasoning levels. Its default isnever, so an adapter that does not set it takes noreasoning.Keep a conversation valid when the next request goes to a different model.
Each assistant message records
metadata.tanstack.source({ provider, api, model }), andresponseIdand the resolvedmodelwhen the provider sends them. A failed or aborted answer getsstopReason: 'error'or'aborted'.RUN_FINISHEDhas a newresponseIdfield.TextAdapterhas new optionalproviderandapifields.When the next request goes to a different source, the adapter removes signatures and redacted thinking, turns readable thinking into text, and rewrites tool call IDs to the shape the target API accepts. Each call stays paired with its result.
Every request drops failed and aborted assistant batches and adds a
No result providedresult for an unanswered tool call. The saved history does not change.RUN_FINISHEDandRUN_ERRORmetadata now merge onto the assistant messages of that model call inStreamProcessor.Anthropic: the new
allowEmptySignatureoption (defaultfalse) replays unsigned thinking from the same source, for gateways. The newprovideroption names a gateway.Azure OpenAI reports the
azure-openai-responsesAPI.Gemini, Mistral, and Bedrock Converse send same-source thinking back to the model, in block order.
withPersistencewrites the source and the stop reason onto streaming snapshot rows.Adapter authors get
transformMessagesForReplay()andhashToolCallId()from@tanstack/ai/adapter-internals.
Keep each OpenAI Responses answer item's
idandphase(commentaryorfinal_answer) on the assistant message, inmetadata.tanstack.responseItems. A same-model replay sends each item back with itsidandphase. Another model gets the plain text, as before.A router pick can give an agent its input. Anywhere a pick has a name, it can have
{ name, input }, sosubagents.routercan start an agent withinputSchema.chat()checks the input against the schema before any child starts, andrunreads it asctx.input. The input stays on the server: the plan onSUBAGENT_STARTEDhas the agent names and the order only.subagentRoutegetsneedsInput(result), which lists the picked agents with aninputSchema, andpick(result, { inputs }), which puts each input on its name.inputsis typed from each schema.chat()throws when a pick gives an input tomainor to an agent with noinputSchema, or gives one agent an input twice in one step.chat()also throws when a router picks an agent whoseinputSchemais a plain JSON Schema, because it cannot check that input. A turn with a router input cannot resume after an interrupt, and a resume also fails when its saved plan is absent. The newSubagentPicktype is exported.Add the
toolChoiceoption tochat().chat({ toolChoice })takes'auto','none','required', or{ type: 'tool', name }.In
chat(),namesuggests the names of thetoolsand still takes any string. TheToolChoicetype takes an optional name type for this.A middleware can change it for one model call in
onConfig.A tool choice in
modelOptionswins.No tool choice goes out when the request has no tools.
@tanstack/openai-basesendstool_choicefrom Chat Completions and Responses, and exportstoChatCompletionsToolChoiceandtoResponsesToolChoice.
chat({ adapter, messages, tools, toolChoice: 'required' }) chat({ adapter, messages, tools, toolChoice: { type: 'tool', name: 'getWeather' }, })A tool call in a model answer that stopped at the output limit (finish reason
length) never runs. The model can send it again when the loop strategy goes on.Behavior change:
chat()never runs a tool call of an answer that stopped at the output limit. The input of such a call can be incomplete JSON. The call asks for no approval, and the client does not run a client tool for it. Before, such a call ran in some cases, for example as a client tool with{}input, or as a pending call of the nextchat()request.Each cut call gets an error tool result. The default text tells the model to send the call again.
The default
agentLoopStrategyis nowcombineStrategies([maxIterations(5), untilFinishReason(['length'])]). By default, a cut answer ends the run with the finish reasonlength, as before.With a strategy that goes on after
length(for examplemaxIterations(5)), the model is called again. TheRUN_FINISHEDevent of the cut answer then has the finish reasontool_calls, so the client keeps the stream open.In a history message with
metadata.tanstack.finishReason: 'length', a call input that is not JSON goes to the provider as{}. Before, Bedrock threw on such a cut input.Middleware
onAfterToolCallgets each cut call, withok: falseand anErrorwith the code'max_tokens'. Return{ type: 'replaceResult', result }to change the error text.Each assistant message that
chat()stores on the server keeps the finish reason of its model call inmetadata.tanstack.finishReason.
Migration: if you pass your own
agentLoopStrategyand want a cut answer to end the run, adduntilFinishReason(['length'])to it withcombineStrategies.
Patch Changes
fakeTextfrom@tanstack/ai/testingnow stops onrequest.signal, the abort signalchat()passes to adapters. Before, it read onlyabortController, so code that reads the adapter stream directly (a harness, for example) saw a paced fake keep streaming after a cancel.Text adapters pass
chat()'s adapter constraint again when the project enablesexactOptionalPropertyTypes.TextAdapter.inputModalitiesnow accepts an explicitundefined, which the provider adapters' declarations emit for their per-model lookup.The client
StreamProcessorno longer logs aconsole.warnwhen it skips a tool call of an answer that stopped at the output limit. Skipping such a call is expected.A
ChatClientthat only watches a thread (a second tab, withlive: true) now returns toreadyafter every run. Before, it stayedstreamingafter the second run, because the stream processor ended only the first run it saw.
- 0.68.x · maintenance
0.68.0
9 Oct 2026fakeText()takes aninputoption, for examplefakeText({ input: ['text', 'image'] }). It sets the fake'sinputModalities, so you can test code that checks what a model accepts. It also sets the adapter's input modality type.Fix a server tool call that the next turn sent twice when you use
withPersistence. When a model call starts with a tool call and no text (often after thinking),StreamProcessorgave that message the id of the next model call. The next turn then replaced the stored answer with a copy that also held the tool call, so the provider got the sametool_usetwice. The client now keeps the id of the message the tool call belongs to.
- 0.67.x · maintenance
0.67.0
9 Oct 2026Text adapters can now give
inputModalitiesat run time: the input kinds that the model reads, for example['text', 'image', 'document'].undefinedmeans that the adapter does not know.A middleware
onAfterToolCallhook can now replace the result of a tool call. Return{ type: 'replaceResult', result }from the hook.Add
fakeText()at the new@tanstack/ai/testingsubpath. It is a text adapter that answers from a script. Use it to testchat(), tools, and middleware with no network and no API key.
Minor Changes
Text adapters can now give
inputModalitiesat run time: the input kinds that the model reads, for example['text', 'image', 'document'].undefinedmeans that the adapter does not know.TextAdapterhas the new optionalinputModalitiesproperty. ABaseTextAdaptersubclass sets it from its model metadata.The text adapters of OpenAI, Anthropic, Gemini, Mistral, Groq, BytePlus, Grok, OpenRouter, and LLM Gateway set it. A known model gives its input kinds. An unknown model gives
undefined.
A middleware
onAfterToolCallhook can now replace the result of a tool call. Return{ type: 'replaceResult', result }from the hook.The model and the stream (
TOOL_CALL_RESULT) get the new result.Middleware run in order. Each hook gets the result of the hook before it as
info.result.An error result stays an error.
A hook that returns
undefined, or any value that is not areplaceResultdecision, keeps the result.onAfterToolCallafter anonBeforeToolCallskip now gets the parsed skip result, the same value that goes to the model.The new
AfterToolCallDecisiontype is exported from@tanstack/ai.
Add
fakeText()at the new@tanstack/ai/testingsubpath. It is a text adapter that answers from a script. Use it to testchat(), tools, and middleware with no network and no API key.Queue answers with
setResponsesandappendResponses. An answer is{ text?, thinking?, toolCalls?, finishReason?, error? }, or a function that gets{ request, state }. An empty queue fails with "No more fake responses queued".Token usage is an estimate:
ceil(characters / 4). Withcache: true, the part of a request that matches the previous request of the same thread counts as cached tokens.Options:
model,contextWindow, andtokensPerSecondfor stream pacing. The stream stops when the request aborts.RUN_STARTEDcarriesparentRunIdwhen the call has one.The default tool call id is
fake-call-<fake>-<call>-<index>. It is unique acrossfakeText()instances.
Add
keyedAdapter(provider, create)andisKeyedAdapter(value). Import them from@tanstack/aior@tanstack/ai/byok.keyedAdapterwraps an adapter factory that needs a provider key.provideris a BYOK descriptor (for exampleopenaiByok) or a provider id. It throws when the provider id is not valid.A host finds the key, for example with
getByokKey(request, keyed.provider), and callskeyed.create(key)just before the call. The key is not in your code.isKeyedAdaptertells a keyed adapter apart from a plain adapter.It works for every adapter kind: text, image, speech, audio, video, and the rest.
For several providers, use
keyedAdapters({ openai: (key) => ..., anthropic: keyedAdapter(anthropicByok, ...) }). The map key is the provider id, so a key goes only to its own provider. An entry that is a keyed adapter for another provider throws at startup. In a route,keyedAdapterFromRequest(request, models)from@tanstack/ai/byok/serverbuilds the adapter of the provider that has a key. The user'sx-byok-<id>header wins, then the env names of descriptor entries, in map order. It givesnullwhen no provider has a key.A
RUN_ERRORnow tells you how long the provider asks you to wait before a retry.@tanstack/ai:RunErrorEventhas a newretryAfterMsfield, in milliseconds.chat()moves it tometadata.tanstack.retryAfterMs. The newtoRetryAfterMs(error)helper on@tanstack/ai/adapter-internalsreads theretry-after-msheader first, then theretry-afterheader in seconds or as an HTTP date.@tanstack/openai-baseand@tanstack/ai-anthropic: the text adapters setretryAfterMson aRUN_ERRORwhen the error response has one of these headers. This includes every adapter that uses@tanstack/openai-base.
Add
wrapFetchtochat(), to the middlewareonConfigconfig, and toTextOptions. A wrapper gets the next fetch and gives back a new fetch. Use it to change the URL, the headers, the request, or the response of a model call. The newFetchWrappertype names the wrapper.The engine chains the
chat()wrapper and the middleware wrappers into one function. A middleware wrapper runs inside thechat()wrapper and applies to one model call only.These text adapters send their requests through the wrapper: every adapter on
@tanstack/openai-base(OpenAI, the OpenAI-compatible adapters, Grok, Groq, BytePlus, LLM Gateway, Lovable, Vercel AI Gateway, Cloudflare, and the Bedrock Chat Completions and Responses APIs), Anthropic, Mistral, Ollama, and OpenRouter. The wrapper wraps the fetch of the adapter config, not the global fetch. An Anthropic or Ollama adapter with an injected client ignoreswrapFetch. In Cloudflare binding mode, the wrapper runs, but the binding does not send the URL or the headers. WithoutwrapFetch, the requests do not change.
Dependency tracking · free for 3 projects
Know when your own dependencies move
@tanstack/ai published 103 releases in the last 12 months. Inlinr folds the routine ones into a line and keeps the few that need you.
Advisories reach you
An email when a critical or high advisory affects a project, before the next install.
Upgrades checked first
Peer ranges, Node, ESM-only and types changes read before you bump.
Only what matters
Majors and breaking notes stay; patches and sibling bumps fold into one line.
Registry facts last checked 22 Oct 2026. Releases and advisories last read 22 Oct 2026.