Repository navigation
@tanstack/ai@0.69.0
Minor Changes
-
#1698
1bfcee3- 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.
-
#1686
412b0fe-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. -
#1739
a0150c2- Text adapters now set one stable code onRUN_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.
- OpenAI-compatible adapters gave
-
#1715
fac0555- New hooks for hosts that runchat()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>.
-
#1713
5bd27f2-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. -
#1714
c20f7b0- AddLogRecordsCapabilityand 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. -
#1697
cef1911- 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.
- Before each model call,
-
#1729
51f72fb- A text adapter can now have an optionalcompact(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. -
#1697
cef1911- Prompt caching is now on by default. Eachchat()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.
- Claude on Anthropic, Bedrock Converse, and OpenRouter gets cache markers.
- A manual
cache_control,cachePoint,prompt_cache_key, orsessionIdwins over the automatic value. - New types:
PromptCacheRetention,PromptCacheOptions, andResolvedPromptCache. Adapters readTextOptions.promptCache.
- New
-
#1692
7532b2b- Add thereasoningoption 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. - Pass a level:
-
#1698
1bfcee3- 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.
- Each assistant message records
-
#1726
78edeb2- Keep each OpenAI Responses answer item'sidandphase(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. -
#1722
44e3a0a- 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. -
#1692
7532b2b- Add thetoolChoiceoption 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' }, })
-
#1717
7a5c0d6- A tool call in a model answer that stopped at the output limit (finish reasonlength) 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. - Behavior change:
Patch Changes
-
#1733
b881627-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. -
#1707
6774e80- Text adapters passchat()'s adapter constraint again when the project enablesexactOptionalPropertyTypes.TextAdapter.inputModalitiesnow accepts an explicitundefined, which the provider adapters' declarations emit for their per-model lookup. -
#1746
6f1e559- The clientStreamProcessorno longer logs aconsole.warnwhen it skips a tool call of an answer that stopped at the output limit. Skipping such a call is expected. -
#1733
b881627- AChatClientthat 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. -
Updated dependencies [
eb4eea0]:- @tanstack/ai-utils@0.4.2