Skip to content

@tanstack/ai@0.69.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 17:47
· 134 commits to main since this release
Immutable release. Only release title and notes can be modified.
293aea9

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.
    • ModelMessage has a new optional blockOrder field. 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_END and REASONING_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. A UIMessage that 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. Pass retry: { 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 maxRetries or maxWaitMs that is not valid throws a TypeError.

    retry is off by default.

    chat() now also sends RUN_STARTED before a RUN_ERROR when the adapter failed before it sent a start.

  • #1739 a0150c2 - Text adapters now set one stable code on RUN_ERROR when the input does not fit in the model's context window: code: 'context_overflow'. @tanstack/ai exports it as CONTEXT_OVERFLOW_CODE. The provider's own text stays in message.

    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'. rawEvent still 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_ERROR with this code when the model stops with stop_reason: 'model_context_window_exceeded'. Before, it sent RUN_FINISHED with finishReason: 'stop', so a cut-off answer looked complete.
  • #1715 fac0555 - New hooks for hosts that run chat() for a long session.

    • @tanstack/ai exports runAgentStream (runs a defineAgent agent outside a parent chat turn), createSubagentId, createSubagentSink, CapabilityRegistry, and the SubagentsBag and SubagentSink types. Pass the sink to keep interrupt objects and token usage. The finished event has interruptIds only.
    • MetadataStore has optional getVersioned and setIf for compare-and-set writes. The memory store of @tanstack/ai-persistence has 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 detach field. A host sets it when it can move the call to the background. A tool passes its work to detach and 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 stays mcp_input_<toolCallId>.
  • #1713 5bd27f2 - toJsonResponse now accepts the object from chat({ outputSchema }). It sends the object as a complete structured output, so useChat with fetchJson shows it as a structured-output part. Before this change, the call failed with Cannot read properties of undefined (reading 'map'). toJsonResponse also awaits a Promise input, so toJsonResponse(chat({ ... })) works.

  • #1714 c20f7b0 - Add LogRecordsCapability and the LogRecord and LogRecordsWriter types. A chat middleware can append records to a session log when a host middleware provides one. Read the writer with getLogRecords(ctx, { optional: true }). It is undefined when no host provides the log, and getLogRecords(ctx) throws. A host provides the writer in setup with provideLogRecords(ctx, writer). @tanstack/ai does 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.
    • openaiText on gpt-5.4-mini, gpt-5.5, gpt-5.6-luna, gpt-5.6-sol, gpt-5.6-terra, gpt-6-astra, gpt-6-luna, and gpt-6-sol: an added tool is an additional_tools input item, and an added prompt is a developer message. The Responses adapter of @tanstack/openai-base sends these when a subclass sets midConversationChannels, and it sends the namespace of such a tool's call back on the next request.
    • anthropicText on claude-opus-4-8, claude-opus-5, claude-opus-5-5, claude-fable-5, and claude-fable-5-1: every request with tools has the mid-conversation-tool-changes-2026-07-01 beta and one placeholder tool, unless a provider tool such as webSearchTool() takes part. An added tool has defer_loading: true and a tool_addition block in a system message.
    • 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 custom fetch, an injected client, or the OPENAI_BASE_URL / ANTHROPIC_BASE_URL environment variable turns the default off. Set midConversationChannels: true to turn the channels on anyway. Set false to 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, and TextAdapter.midConversationChannels, with the types MidConversationChange, MidConversationChanges, and MidConversationChannels. splitMidConversationChanges helps adapter authors read the changes.
  • #1729 51f72fb - 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 are TextCompactOptions: messages, model, signal, wrapFetch, and systemPrompts.

    The OpenAI Responses adapters implement compact with POST /responses/compact. The request sends the system prompts as instructions. It sends no tools, because the endpoint does not take them. The encrypted compaction item goes back to the same model as it is on later requests. A compact result with no compaction item throws.

    withCompaction has a new native option. Pass the adapter of the chat() call. When the adapter has compact, it runs in place of strategy. When compact fails, strategy runs, and onCompact and compaction:ended get the failure in error. 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 tries compact again. Without the option, nothing changes.

  • #1697 cef1911 - 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() option promptCache: 'none' | 'short' | 'long', or { retention, key }. The default is 'short'. The key is promptCache.key, else the threadId or conversationId that you give. A threadId that chat() 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: onConfig gets config.promptCache and 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, and prompt_cache_retention or prompt_cache_options on 'long'.
      • Mistral gets prompt_cache_key, and usage now reports cache reads.
      • OpenRouter gets sessionId.
    • A manual cache_control, cachePoint, prompt_cache_key, or sessionId wins over the automatic value.
    • New types: PromptCacheRetention, PromptCacheOptions, and ResolvedPromptCache. Adapters read TextOptions.promptCache.
  • #1692 7532b2b - Add the reasoning option to chat(). Set how hard the model thinks in one way for every provider.

    • Pass a level: off, minimal, low, medium, high, xhigh, or max. Or pass { level, summary?, budgetTokens? }.
    • The types allow only the levels of the selected model. budgetTokens is 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 types ReasoningLevel, ReasoningLevels, ModelReasoning, ReasoningCapability, ReasoningOption, ReasoningOptionFor, ReasoningRequest, and AdapterReasoning. @tanstack/ai/adapter-internals exports resolveReasoning, reasoningBudget, DEFAULT_REASONING_BUDGETS, and the ModelReasoningCapability type for adapter authors. TextAdapter and BaseTextAdapter take a new last type parameter for the reasoning levels. Its default is never, so an adapter that does not set it takes no reasoning.

  • #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 }), and responseId and the resolved model when the provider sends them. A failed or aborted answer gets stopReason: 'error' or 'aborted'. RUN_FINISHED has a new responseId field. TextAdapter has new optional provider and api fields.
    • 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 provided result for an unanswered tool call. The saved history does not change.
    • RUN_FINISHED and RUN_ERROR metadata now merge onto the assistant messages of that model call in StreamProcessor.
    • Anthropic: the new allowEmptySignature option (default false) replays unsigned thinking from the same source, for gateways. The new provider option names a gateway.
    • Azure OpenAI reports the azure-openai-responses API.
    • Gemini, Mistral, and Bedrock Converse send same-source thinking back to the model, in block order.
    • withPersistence writes the source and the stop reason onto streaming snapshot rows.
    • Adapter authors get transformMessagesForReplay() and hashToolCallId() from @tanstack/ai/adapter-internals.
  • #1726 78edeb2 - Keep each OpenAI Responses answer item's id and phase (commentary or final_answer) on the assistant message, in metadata.tanstack.responseItems. A same-model replay sends each item back with its id and phase. 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 }, so subagents.router can start an agent with inputSchema. chat() checks the input against the schema before any child starts, and run reads it as ctx.input. The input stays on the server: the plan on SUBAGENT_STARTED has the agent names and the order only. subagentRoute gets needsInput(result), which lists the picked agents with an inputSchema, and pick(result, { inputs }), which puts each input on its name. inputs is typed from each schema. chat() throws when a pick gives an input to main or to an agent with no inputSchema, or gives one agent an input twice in one step. chat() also throws when a router picks an agent whose inputSchema is 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 new SubagentPick type is exported.

  • #1692 7532b2b - Add the toolChoice option to chat().

    • chat({ toolChoice }) takes 'auto', 'none', 'required', or { type: 'tool', name }.
    • In chat(), name suggests the names of the tools and still takes any string. The ToolChoice type takes an optional name type for this.
    • A middleware can change it for one model call in onConfig.
    • A tool choice in modelOptions wins.
    • No tool choice goes out when the request has no tools.
    • @tanstack/openai-base sends tool_choice from Chat Completions and Responses, and exports toChatCompletionsToolChoice and toResponsesToolChoice.
    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 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 next chat() request.
    • Each cut call gets an error tool result. The default text tells the model to send the call again.
    • The default agentLoopStrategy is now combineStrategies([maxIterations(5), untilFinishReason(['length'])]). By default, a cut answer ends the run with the finish reason length, as before.
    • With a strategy that goes on after length (for example maxIterations(5)), the model is called again. The RUN_FINISHED event of the cut answer then has the finish reason tool_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 onAfterToolCall gets each cut call, with ok: false and an Error with 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 in metadata.tanstack.finishReason.

    Migration: if you pass your own agentLoopStrategy and want a cut answer to end the run, add untilFinishReason(['length']) to it with combineStrategies.

Patch Changes

  • #1733 b881627 - fakeText from @tanstack/ai/testing now stops on request.signal, the abort signal chat() passes to adapters. Before, it read only abortController, 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 pass chat()'s adapter constraint again when the project enables exactOptionalPropertyTypes. TextAdapter.inputModalities now accepts an explicit undefined, which the provider adapters' declarations emit for their per-model lookup.

  • #1746 6f1e559 - The client StreamProcessor no longer logs a console.warn when it skips a tool call of an answer that stopped at the output limit. Skipping such a call is expected.

  • #1733 b881627 - A ChatClient that only watches a thread (a second tab, with live: true) now returns to ready after every run. Before, it stayed streaming after the second run, because the stream processor ended only the first run it saw.

  • Updated dependencies [eb4eea0]:

    • @tanstack/ai-utils@0.4.2