TanStack AI version
0.2.2
Framework/Library version
"@tanstack/react-start": "^1.149.4",
Describe the bug and the steps to reproduce it
I am following the documentation for implementing a client-side tool with approval using @tanstack/ai.
I have a tool delete_local_data configured with needsApproval: true. When the AI triggers the tool call, the UI correctly renders my approval component. However, when I call addToolApprovalResponse({ id, approved: true }):
- The tool implementation does not execute (the
localStorage item is not deleted). - The AI does not respond back after the approval.
- The UI state seems stuck (likely
isLoading remains active), preventing further input.
Reproduction Code
1. api.chat.ts (Server Handler) I am using OpenAITextAdapter via OpenRouter. Note: I am testing with xiaomi/mimo-v2-flash:free specifically because it is a popular free model that supports tool calling, strictly for testing purposes.
import{chat,toServerSentEventsResponse}from"@tanstack/ai";import{OpenAITextAdapter}from"@tanstack/ai-openai";import{createFileRoute}from"@tanstack/react-router";import{deleteLocalDataDef}from"@/lib/tools/definitions";exportconstRoute=createFileRoute("/api/chat")({server: {handlers: {POST: async({ request })=>{if(!process.env.OPENAI_API_KEY){returnnewResponse(JSON.stringify({error: "No API Key"}),{status: 500});}const{ messages, conversationId }=awaitrequest.json();try{constadapter=newOpenAITextAdapter({baseURL: "https://openrouter.ai/api/v1",apiKey: process.env.OPENAI_API_KEY,},"xiaomi/mimo-v2-flash:free"asany,);conststream=chat({
adapter,
messages,
conversationId,tools: [deleteLocalDataDef],});returntoServerSentEventsResponse(stream);}catch(error){returnnewResponse(JSON.stringify({error: "Error"}),{status: 500});}},},},});2. Definitions.ts (Tool Definition)
import{toolDefinition}from"@tanstack/ai";import{z}from"zod";exportconstdeleteLocalDataDef=toolDefinition({name: "delete_local_data",description: "Delete data from local storage",inputSchema: z.object({key: z.string(),}),outputSchema: z.object({deleted: z.boolean(),}),needsApproval: true,// Requires approval});3. Chat.tsx (Client Component)
// app/routes/chat.tsx (or wherever your Chat component is)import{fetchServerSentEvents,useChat}from"@tanstack/ai-react";import{useState}from"react";import{ApprovalPrompt}from"@/components/ApprovalPrompt";import{deleteLocalDataDef}from"@/lib/tools/definitions";// 1. Import your shared definitionsfunctionChat(){// Client: Create implementationconstdeleteLocalData=deleteLocalDataDef.client((input)=>{// This will only execute after approvallocalStorage.removeItem(input.key);return{deleted: true};});// 4. Pass options to useChat// The hook now handles the streaming AND the tool execution automaticallyconst{ messages, sendMessage, isLoading, addToolApprovalResponse }=useChat({connection: fetchServerSentEvents("/api/chat"),tools: [deleteLocalData],// Automatic execution after approval},);const[input,setInput]=useState("");consthandleSubmit=(e: React.FormEvent)=>{e.preventDefault();if(input.trim()&&!isLoading){sendMessage(input);setInput("");}};return(<divclassName="flex flex-col h-screen">{/* Messages Area */}<divclassName="flex-1 overflow-y-auto p-4">{messages.map((message)=>(<divkey={message.id}className={`mb-4 ${
message.role==="assistant" ? "text-blue-600" : "text-gray-800"}`}><divclassName="font-semibold mb-1">{message.role==="assistant" ? "Assistant" : "You"}</div><div>{message.parts.map((part,idx)=>{// Handle textif(part.type==="text")return<divkey={idx}>{part.content}</div>;// Handle "Thinking" (if you use it)if(part.type==="thinking")return(<divkey={idx}className="italic text-gray-400">Thinking...
</div>);// Handle Tool Calls (Optional visualization)// You don't HAVE to render this, but it helps debuggingif(part.type==="tool-call"&&part.state==="approval-requested"&&part.approval){return(<ApprovalPromptkey={part.id}part={part}onApprove={()=>addToolApprovalResponse({id: part.approval!.id,approved: true,})}onDeny={()=>addToolApprovalResponse({id: part.approval!.id,approved: false,})}/>);}returnnull;})}</div></div>))}</div>{/* Input Area */}<formonSubmit={handleSubmit}className="p-4 border-t"><divclassName="flex gap-2"><inputtype="text"value={input}onChange={(e)=>setInput(e.target.value)}placeholder="Type a message..."className="flex-1 px-4 py-2 border rounded-lg"disabled={isLoading}/><buttontype="submit"
disabled={!input.trim()||isLoading}className="px-6 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50">Send</button></div></form></div>);}exportdefaultChat;Expected Behavior After calling addToolApprovalResponse with approved: true:
- The client-side tool implementation should execute (
localStorage.removeItem). - The result should be sent back to the LLM.
- The LLM should continue streaming a response acknowledging the action.
Actual Behavior The application simply hangs after the approval click. No network activity follows, and the client function is never triggered.
Additional Context
- Model Provider: OpenRouter
- Model:
xiaomi/mimo-v2-flash:free - Stack: TanStack Start, TanStack AI, React
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
None
Screenshots or Videos (Optional)

Do you intend to try to help solve this bug with your own PR?
None
Terms & Code of Conduct
TanStack AI version
0.2.2
Framework/Library version
"@tanstack/react-start": "^1.149.4",
Describe the bug and the steps to reproduce it
I am following the documentation for implementing a client-side tool with approval using
@tanstack/ai.I have a tool
delete_local_dataconfigured withneedsApproval: true. When the AI triggers the tool call, the UI correctly renders my approval component. However, when I calladdToolApprovalResponse({ id, approved: true }):localStorageitem is not deleted).isLoadingremains active), preventing further input.Reproduction Code
1. api.chat.ts (Server Handler) I am using
OpenAITextAdaptervia OpenRouter. Note: I am testing withxiaomi/mimo-v2-flash:freespecifically because it is a popular free model that supports tool calling, strictly for testing purposes.2. Definitions.ts (Tool Definition)
3. Chat.tsx (Client Component)
Expected Behavior After calling
addToolApprovalResponsewithapproved: true:localStorage.removeItem).Actual Behavior The application simply hangs after the approval click. No network activity follows, and the client function is never triggered.
Additional Context
xiaomi/mimo-v2-flash:freeYour Minimal, Reproducible Example - (Sandbox Highly Recommended)
None
Screenshots or Videos (Optional)
Do you intend to try to help solve this bug with your own PR?
None
Terms & Code of Conduct