From 635fc96cacd1cb5a461c08ad79e288b2e79ebce6 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Thu, 4 Dec 2025 02:51:02 +1000 Subject: [PATCH] feat: implement playground app with AI SDK v6 tool execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create new Next.js app at apps/playground for testing TPMJS tools - Implement AI SDK v6 patterns with DefaultChatTransport and UIMessage format - Create template tool package at packages/tools/hello with hello-world and hello-name tools - Use tool() and jsonSchema() helpers to avoid Zod 4 conversion issues with OpenAI - Add static tool loading system with switch statement (Next.js/webpack compatible) - Implement chat interface with tool call visualization showing inputs/outputs - Support multi-step tool execution with stepCountIs(5) - Stream responses with toUIMessageStreamResponse() for full tool support - Add sidebar showing available tools (static list) - Use parts-based message rendering for text and tool calls - Integrate firecrawl-aisdk tools (scrape, crawl, search) - Add theme toggle in header (defaults to light mode) - Fix responsive layout with max-width for message bubbles - Use biome-ignore comments for legitimate any types in tool loading 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- OPENAI_SCHEMA_ERROR.md | 522 ++++++++++++++++++ STREAMING_EMPTY_RESPONSE.md | 363 ++++++++++++ USECHAT_INPUT_UNDEFINED.md | 400 ++++++++++++++ apps/playground/.env.local.example | 2 + apps/playground/next-env.d.ts | 6 + apps/playground/next.config.ts | 8 + apps/playground/package.json | 46 ++ apps/playground/postcss.config.mjs | 6 + apps/playground/src/app/api/chat/route.ts | 70 +++ apps/playground/src/app/globals.css | 3 + apps/playground/src/app/layout.tsx | 42 ++ apps/playground/src/app/page.tsx | 22 + .../src/components/chat/ChatHeader.tsx | 52 ++ .../src/components/chat/ChatInput.tsx | 52 ++ .../src/components/chat/ChatInterface.tsx | 38 ++ .../src/components/chat/ChatMessages.tsx | 50 ++ .../src/components/chat/MessageBubble.tsx | 125 +++++ .../src/components/sidebar/ToolsSidebar.tsx | 63 +++ apps/playground/src/env.ts | 7 + apps/playground/src/hooks/useChat.ts | 16 + apps/playground/src/hooks/useToolUsage.ts | 46 ++ apps/playground/src/lib/tool-loader.ts | 92 +++ apps/playground/src/lib/types.ts | 37 ++ apps/playground/tailwind.config.ts | 12 + apps/playground/tsconfig.json | 12 + packages/tools/hello/README.md | 80 +++ packages/tools/hello/package.json | 27 + packages/tools/hello/src/index.ts | 75 +++ packages/tools/hello/tsconfig.json | 20 + packages/tools/package.json | 23 + packages/tools/src/index.ts | 37 ++ packages/tools/tsconfig.json | 11 + pnpm-lock.yaml | 457 ++++++++++++++- 33 files changed, 2817 insertions(+), 5 deletions(-) create mode 100644 OPENAI_SCHEMA_ERROR.md create mode 100644 STREAMING_EMPTY_RESPONSE.md create mode 100644 USECHAT_INPUT_UNDEFINED.md create mode 100644 apps/playground/.env.local.example create mode 100644 apps/playground/next-env.d.ts create mode 100644 apps/playground/next.config.ts create mode 100644 apps/playground/package.json create mode 100644 apps/playground/postcss.config.mjs create mode 100644 apps/playground/src/app/api/chat/route.ts create mode 100644 apps/playground/src/app/globals.css create mode 100644 apps/playground/src/app/layout.tsx create mode 100644 apps/playground/src/app/page.tsx create mode 100644 apps/playground/src/components/chat/ChatHeader.tsx create mode 100644 apps/playground/src/components/chat/ChatInput.tsx create mode 100644 apps/playground/src/components/chat/ChatInterface.tsx create mode 100644 apps/playground/src/components/chat/ChatMessages.tsx create mode 100644 apps/playground/src/components/chat/MessageBubble.tsx create mode 100644 apps/playground/src/components/sidebar/ToolsSidebar.tsx create mode 100644 apps/playground/src/env.ts create mode 100644 apps/playground/src/hooks/useChat.ts create mode 100644 apps/playground/src/hooks/useToolUsage.ts create mode 100644 apps/playground/src/lib/tool-loader.ts create mode 100644 apps/playground/src/lib/types.ts create mode 100644 apps/playground/tailwind.config.ts create mode 100644 apps/playground/tsconfig.json create mode 100644 packages/tools/hello/README.md create mode 100644 packages/tools/hello/package.json create mode 100644 packages/tools/hello/src/index.ts create mode 100644 packages/tools/hello/tsconfig.json create mode 100644 packages/tools/package.json create mode 100644 packages/tools/src/index.ts create mode 100644 packages/tools/tsconfig.json diff --git a/OPENAI_SCHEMA_ERROR.md b/OPENAI_SCHEMA_ERROR.md new file mode 100644 index 0000000..f7ef9e9 --- /dev/null +++ b/OPENAI_SCHEMA_ERROR.md @@ -0,0 +1,522 @@ +# OpenAI Schema Validation Error - AI SDK v6 + +## ✅ RESOLVED + +**Solution:** Use `tool()` and `jsonSchema()` from AI SDK instead of Zod for tool definitions. + +## Error Message + +``` +Error [AI_APICallError]: Invalid schema for function 'helloWorld': schema must be a JSON Schema of 'type: "object"', got 'type: "None"'. +``` + +## Context + +Building a Next.js playground app to test AI SDK v6 tool execution with OpenAI's GPT-4o-mini model. The error occurs when OpenAI validates the tool schema sent in the API request. + +## Root Cause + +Zod 4.0.0 generates JSON Schema with `allOf` + `$ref` at the root level instead of a direct `type: "object"`. OpenAI's API requires a JSON Schema with `type: "object"` at the root, so it rejects Zod 4 schemas with `type: "None"` error. + +## Environment + +- **AI SDK Version**: `ai@6.0.0-beta.124` +- **OpenAI Provider**: `@ai-sdk/openai@3.0.0-beta.74` +- **OpenAI Library**: `openai@^6.9.1` +- **Zod Version**: `zod@^4.0.0` +- **Next.js Version**: `next@^16.0.4` +- **Node.js**: Latest +- **TypeScript**: Strict mode enabled + +## Tool Definition + +Located at: `packages/tools/hello/src/index.ts` + +```typescript +import { z } from 'zod'; + +/** + * Hello World Tool + * Returns a simple "Hello, World!" greeting + * + * This is a proper AI SDK v6 tool that can be used with streamText() + */ +export const helloWorldTool = { + description: 'Returns a simple "Hello, World!" greeting message', + parameters: z.object({ + // OpenAI requires at least one optional parameter, can't be completely empty + includeTimestamp: z.boolean().optional().describe('Whether to include a timestamp in the response'), + }), + execute: async ({ includeTimestamp = true }: { includeTimestamp?: boolean }) => { + const response: any = { + message: 'Hello, World!', + }; + + if (includeTimestamp) { + response.timestamp = new Date().toISOString(); + } + + return response; + }, +}; + +/** + * Hello Name Tool + * Returns a personalized greeting with the provided name + * + * This is a proper AI SDK v6 tool that can be used with streamText() + */ +export const helloNameTool = { + description: 'Returns a personalized greeting with the provided name', + parameters: z.object({ + name: z.string().describe('The name of the person to greet'), + }), + execute: async ({ name }: { name: string }) => { + return { + message: `Hello, ${name}!`, + timestamp: new Date().toISOString(), + }; + }, +}; +``` + +## Tool Loading + +Located at: `apps/playground/src/lib/tool-loader.ts` + +```typescript +// Static imports for tools (required for Next.js/webpack) +import { helloWorldTool, helloNameTool } from '@tpmjs/hello'; +import { scrapeTool, crawlTool, searchTool } from 'firecrawl-aisdk'; + +/** + * Load a specific TPMJS tool by package name + */ +export async function loadTpmjsTool(packageName: string): Promise { + try { + // Map package names to their tool functions + switch (packageName) { + case '@tpmjs/hello': + // Hello has multiple tools, return all of them + return { + helloWorld: helloWorldTool, + helloName: helloNameTool, + }; + + case 'firecrawl-aisdk': + // Firecrawl has multiple tools, return all of them + return { + scrapeTool, + crawlTool, + searchTool, + }; + + default: + throw new Error(`Unknown tool package: ${packageName}`); + } + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to load tool from package ${packageName}: ${error.message}`); + } + throw new Error(`Failed to load tool from package ${packageName}: Unknown error`); + } +} + +/** + * Load all installed TPMJS tools + */ +export async function loadAllTools(): Promise> { + const installedTools = ['@tpmjs/hello', 'firecrawl-aisdk']; + + const tools: Record = {}; + + for (const packageName of installedTools) { + try { + const tool = await loadTpmjsTool(packageName); + + // If the tool returns an object with multiple tools (like firecrawl), spread them + if (tool && typeof tool === 'object' && !tool.description) { + Object.assign(tools, tool); + } else { + // Single tool - use a cleaned name (remove hyphens, camelCase) + const toolName = packageName.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()).replace(/-/g, ''); + tools[toolName] = tool; + } + } catch (error) { + console.error(`Failed to load tool ${packageName}:`, error); + // Continue loading other tools even if one fails + } + } + + return tools; +} +``` + +## API Route + +Located at: `apps/playground/src/app/api/chat/route.ts` + +```typescript +import { loadAllTools } from '~/lib/tool-loader'; +import { openai } from '@ai-sdk/openai'; +import { streamText } from 'ai'; +import { NextRequest } from 'next/server'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { messages } = body; + + if (!messages || !Array.isArray(messages)) { + return new Response(JSON.stringify({ error: 'Invalid request: messages array required' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Load all available tools + const tools = await loadAllTools(); + + console.log('Loaded tools:', Object.keys(tools)); + + // Create system message + const systemMessage = { + role: 'system' as const, + content: `You are a helpful AI assistant that can use TPMJS tools to help users. + +Available tools: +${Object.entries(tools) + .map(([name, tool]) => `- ${name}: ${tool.description}`) + .join('\n')} + +Call tools as needed to answer user questions. Execute tools directly.`, + }; + + // Stream the AI response with tools + const result = streamText({ + model: openai('gpt-4o-mini'), + messages: [systemMessage, ...messages], + tools, + maxSteps: 5, + }); + + return result.toTextStreamResponse(); + } catch (error) { + console.error('Chat API error:', error); + return new Response( + JSON.stringify({ + error: error instanceof Error ? error.message : 'Unknown error occurred', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ); + } +} +``` + +## Package Configuration + +Located at: `packages/tools/hello/package.json` + +```json +{ + "name": "@tpmjs/hello", + "version": "0.0.1", + "private": true, + "description": "Example TPMJS tools - Hello World and Hello Name", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit" + }, + "keywords": [ + "tpmjs-tool", + "ai-sdk", + "hello", + "example" + ], + "tpmjs": { + "category": "text-analysis", + "description": "Simple greeting tools - Hello World and personalized Hello Name greetings" + }, + "dependencies": { + "ai": "6.0.0-beta.124", + "zod": "^4.0.0" + }, + "devDependencies": { + "@tpmjs/tsconfig": "workspace:*", + "typescript": "^5.9.3" + }, + "files": [ + "dist", + "README.md" + ] +} +``` + +## TypeScript Configuration + +Located at: `packages/tools/hello/tsconfig.json` + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## Compiled Output + +Located at: `packages/tools/hello/dist/index.js` + +```javascript +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helloNameTool = exports.helloWorldTool = void 0; +const zod_1 = require("zod"); +/** + * Hello World Tool + * Returns a simple "Hello, World!" greeting + * + * This is a proper AI SDK v6 tool that can be used with streamText() + */ +exports.helloWorldTool = { + description: 'Returns a simple "Hello, World!" greeting message', + parameters: zod_1.z.object({ + // OpenAI requires at least one optional parameter, can't be completely empty + includeTimestamp: zod_1.z.boolean().optional().describe('Whether to include a timestamp in the response'), + }), + execute: async ({ includeTimestamp = true }) => { + const response = { + message: 'Hello, World!', + }; + if (includeTimestamp) { + response.timestamp = new Date().toISOString(); + } + return response; + }, +}; +/** + * Hello Name Tool + * Returns a personalized greeting with the provided name + * + * This is a proper AI SDK v6 tool that can be used with streamTime() + */ +exports.helloNameTool = { + description: 'Returns a personalized greeting with the provided name', + parameters: zod_1.z.object({ + name: zod_1.z.string().describe('The name of the person to greet'), + }), + execute: async ({ name }) => { + return { + message: `Hello, ${name}!`, + timestamp: new Date().toISOString(), + }; + }, +}; +``` + +## Full Error Response from OpenAI + +```json +{ + "error": { + "message": "Invalid schema for function 'helloWorld': schema must be a JSON Schema of 'type: \"object\"', got 'type: \"None\"'.", + "type": "invalid_request_error", + "param": "tools[0].parameters", + "code": "invalid_function_parameters" + } +} +``` + +API endpoint: `https://api.openai.com/v1/responses` +Status code: 400 + +## Problem Analysis + +1. **OpenAI expects JSON Schema format** - The `tools[0].parameters` field must be a valid JSON Schema object with `type: "object"` + +2. **AI SDK v6 should convert Zod to JSON Schema** - The AI SDK is supposed to automatically convert Zod schemas to JSON Schema when sending to OpenAI, but it's producing `type: "None"` instead + +3. **Potential causes**: + - Zod 4.0.0 compatibility issue with AI SDK v6 beta + - AI SDK not properly converting the Zod schema + - Issue with how the tool object is structured + - Problem with how tools are passed to `streamText()` + +4. **Already tried**: + - Added at least one parameter (even optional) to helloWorldTool + - Used proper Zod schema with `.describe()` for descriptions + - Followed AI SDK v6 tool definition format exactly + - Built the package successfully (dist folder exists) + +## AI SDK v6 Tool Format Reference + +According to AI SDK v6 documentation, a tool should be defined as: + +```typescript +{ + description: string, + parameters: ZodSchema, + execute: async (args) => Promise +} +``` + +This matches our implementation exactly. + +## Questions for ChatGPT + +1. Is there a known compatibility issue between AI SDK v6 Beta (6.0.0-beta.124) and Zod 4.0.0? + +2. Does the AI SDK v6 require a specific tool registration format when passing to `streamText()`? + +3. Should tools be wrapped in a different structure (e.g., using `tool()` helper function)? + +4. Is there a way to manually convert Zod schema to JSON Schema that OpenAI accepts? + +5. Are there any known issues with using workspace packages (`@tpmjs/hello`) in Next.js API routes with dynamic imports? + +6. Should we downgrade to Zod 3.x instead of Zod 4.0.0? + +7. Is there a debug mode to see what JSON Schema is being sent to OpenAI? + +## Additional Context + +- The `firecrawl-aisdk` package works correctly with the same setup +- Build process completes successfully with no TypeScript errors +- The tool is being loaded and passed to `streamText()` correctly +- Error only occurs when OpenAI validates the tool schema +- This is a monorepo using pnpm workspaces and Turborepo + +## Related Files + +- Tool definition: `packages/tools/hello/src/index.ts` +- Tool loader: `apps/playground/src/lib/tool-loader.ts` +- API route: `apps/playground/src/app/api/chat/route.ts` +- Package config: `packages/tools/hello/package.json` +- Compiled output: `packages/tools/hello/dist/index.js` + +## Expected Behavior + +Tools should be automatically converted from Zod schema to JSON Schema by AI SDK v6 and accepted by OpenAI's API. + +## Actual Behavior + +OpenAI rejects the tool schema with error: `got 'type: "None"'` instead of a valid JSON Schema object. + +--- + +## ✅ SOLUTION IMPLEMENTED + +### What We Changed + +Instead of using Zod schemas with `parameters`, we now use AI SDK's `tool()` helper with `jsonSchema()` for the input schema. This bypasses Zod's JSON Schema conversion entirely. + +### Before (Broken with Zod 4) + +```typescript +import { z } from 'zod'; + +export const helloWorldTool = { + description: 'Returns a simple "Hello, World!" greeting message', + parameters: z.object({ + includeTimestamp: z.boolean().optional().describe('Whether to include a timestamp'), + }), + execute: async ({ includeTimestamp = true }) => { + // ... + }, +}; +``` + +### After (Working with jsonSchema) + +```typescript +import { jsonSchema, tool } from 'ai'; + +type HelloWorldInput = { + includeTimestamp?: boolean; +}; + +export const helloWorldTool = tool({ + description: 'Returns a simple "Hello, World!" greeting message', + inputSchema: jsonSchema({ + type: 'object', + properties: { + includeTimestamp: { + type: 'boolean', + description: 'Whether to include a timestamp in the response', + }, + }, + additionalProperties: false, + }), + async execute({ includeTimestamp = true }) { + const response: any = { + message: 'Hello, World!', + }; + if (includeTimestamp) { + response.timestamp = new Date().toISOString(); + } + return response; + }, +}); +``` + +### Key Changes + +1. **Import from `ai`**: Added `jsonSchema` and `tool` imports +2. **Define TypeScript types**: Created `HelloWorldInput` type for type safety +3. **Use `tool()` wrapper**: Wraps the entire tool definition +4. **Use `jsonSchema()` for schema**: Provides explicit JSON Schema with `type: "object"` at root +5. **Removed Zod dependency**: No longer need `zod` in package.json + +### Benefits + +- ✅ Works with OpenAI's strict schema validation +- ✅ Explicit control over JSON Schema structure +- ✅ Full TypeScript type safety with generic types +- ✅ No dependency on Zod (one less package to maintain) +- ✅ Follows AI SDK v6 best practices +- ✅ Guaranteed `type: "object"` at root level + +### Updated Package Dependencies + +```json +{ + "dependencies": { + "ai": "6.0.0-beta.124" + } +} +``` + +Zod is no longer needed in tool packages that use `jsonSchema()`. + +### References + +- [AI SDK Core: tool](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool) +- [AI SDK Core: jsonSchema](https://ai-sdk.dev/docs/reference/ai-sdk-core/json-schema) +- [GitHub Issue: Zod 4 JSON Schema compatibility](https://github.com/vercel/ai/issues/10240) diff --git a/STREAMING_EMPTY_RESPONSE.md b/STREAMING_EMPTY_RESPONSE.md new file mode 100644 index 0000000..98383d7 --- /dev/null +++ b/STREAMING_EMPTY_RESPONSE.md @@ -0,0 +1,363 @@ +# AI SDK v6 Streaming Empty Response Issue + +## Problem + +Using AI SDK v6 Beta with OpenAI and `streamText()`, the API route returns a 200 OK response, but the streamed response body is **completely empty** when tools are involved. + +- **Normal chat** (without tool calls): Works fine, streams text back +- **Tool calls** (when user asks "say hello world"): Returns empty response body, no error messages + +## Environment + +- **AI SDK Version**: `ai@6.0.0-beta.124` +- **OpenAI Provider**: `@ai-sdk/openai@3.0.0-beta.74` +- **OpenAI Library**: `openai@^6.9.1` +- **Next.js Version**: `next@^16.0.4` (App Router) +- **Runtime**: Node.js (`runtime = 'nodejs'`) +- **Model**: `gpt-4o-mini` + +## API Route Implementation + +Located at: `apps/playground/src/app/api/chat/route.ts` + +```typescript +import { createOpenAI } from '@ai-sdk/openai'; +import { streamText, type CoreMessage, tool, jsonSchema } from 'ai'; +import { type NextRequest } from 'next/server'; +import { z } from 'zod'; +import { env } from '~/env'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; +export const maxDuration = 60; + +// Initialize OpenAI provider +const openai = createOpenAI({ + apiKey: env.OPENAI_API_KEY, +}); + +// Request schema +const RequestSchema = z.object({ + messages: z.array( + z.object({ + role: z.enum(['user', 'assistant', 'system']), + content: z.string(), + }) + ), +}); + +// Simple inline test tool to verify streaming works +const testHelloTool = tool({ + description: 'Returns a simple hello world greeting', + inputSchema: jsonSchema<{ includeTimestamp?: boolean }>({ + type: 'object', + properties: { + includeTimestamp: { + type: 'boolean', + description: 'Whether to include a timestamp', + }, + }, + additionalProperties: false, + }), + async execute({ includeTimestamp = true }) { + const response: any = { message: 'Hello, World!' }; + if (includeTimestamp) { + response.timestamp = new Date().toISOString(); + } + return response; + }, +}); + +/** + * POST /api/chat + * Chat with AI agent that can execute TPMJS tools + */ +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { messages } = RequestSchema.parse(body); + + // Use simple inline tool for testing + const tools = { + testHello: testHelloTool, + }; + + // Create system prompt listing available tools + const toolsList = Object.keys(tools) + .map((name) => `- ${name}: ${tools[name]?.description}`) + .join('\n'); + + const systemMessage: CoreMessage = { + role: 'system', + content: `You are a helpful AI assistant that can use TPMJS tools to help users. + +Available tools: +${toolsList} + +Call tools as needed to answer user questions. When a user asks to say hello world or for a greeting, use the testHello tool.`, + }; + + // Stream the response + const result = streamText({ + model: openai('gpt-4o-mini'), + messages: [systemMessage, ...messages], + tools, + }); + + // Return the stream as SSE + return result.toTextStreamResponse(); + } catch (error) { + console.error('Chat API error:', error); + + if (error instanceof z.ZodError) { + return new Response( + JSON.stringify({ + success: false, + error: 'Invalid request format', + details: error.issues, + }), + { + status: 400, + headers: { 'Content-Type': 'application/json' }, + } + ); + } + + return new Response( + JSON.stringify({ + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + } + ); + } +} +``` + +## Client-Side Hook + +Located at: `apps/playground/src/hooks/useChat.ts` + +```typescript +'use client'; + +import { useCallback, useState } from 'react'; + +export interface ChatMessage { + id: string; + role: 'user' | 'assistant' | 'system'; + content: string; + timestamp: Date; +} + +export function useChat() { + const [messages, setMessages] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const sendMessage = useCallback(async (content: string) => { + if (!content.trim()) return; + + // Add user message immediately + const userMessage: ChatMessage = { + id: crypto.randomUUID(), + role: 'user', + content, + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setIsLoading(true); + setError(null); + + try { + // Create assistant message placeholder + const assistantMessageId = crypto.randomUUID(); + const assistantMessage: ChatMessage = { + id: assistantMessageId, + role: 'assistant', + content: '', + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, assistantMessage]); + + // Send request to API + const response = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + messages: [...messages, userMessage].map((m) => ({ + role: m.role, + content: m.content, + })), + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // Read the streaming response + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + + if (!reader) { + throw new Error('Response body is null'); + } + + let accumulatedContent = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + // Decode the chunk + const chunk = decoder.decode(value, { stream: true }); + accumulatedContent += chunk; + + // Update the assistant message with accumulated content + setMessages((prev) => + prev.map((m) => + m.id === assistantMessageId + ? { ...m, content: accumulatedContent } + : m + ) + ); + } + } catch (err) { + console.error('Error sending message:', err); + setError(err instanceof Error ? err.message : 'Failed to send message'); + } finally { + setIsLoading(false); + } + }, [messages]); + + const clearChat = useCallback(() => { + setMessages([]); + setError(null); + }, []); + + return { + messages, + isLoading, + error, + sendMessage, + clearChat, + }; +} +``` + +## Observed Behavior + +### Working Case (Normal Chat) +- User types: "hi" +- API response: 200 OK +- Response body: Streams text chunks successfully +- UI shows: "Hi! How can I help you today?" + +### Broken Case (Tool Call) +- User types: "say hello world" +- API response: 200 OK ✅ +- Response body: **EMPTY** ❌ (no chunks, no data, nothing) +- UI shows: Empty message bubble +- Console: No errors logged + +## HTTP Response Details + +``` +Request Method: POST +Status Code: 200 OK +URL: http://localhost:3001/api/chat +Content-Type: text/plain; charset=utf-8 +Transfer-Encoding: chunked +``` + +The response headers look correct for a streaming response, but the body is completely empty. + +## What We've Tried + +1. ✅ Fixed OpenAI schema validation error (was `type: "None"`, now uses proper JSON Schema) +2. ✅ Using `tool()` and `jsonSchema()` from AI SDK +3. ✅ Simplified to a single inline test tool +4. ✅ Tool executes without errors (no schema validation issues) +5. ✅ Normal chat works fine (proves streaming infrastructure is correct) + +## Questions + +1. **Is `toTextStreamResponse()` the correct method for streaming with tools in AI SDK v6?** + - Should we use a different method like `toDataStreamResponse()` for tool calls? + +2. **Are we constructing the messages array correctly?** + - We're sending `{ role: 'user' | 'assistant' | 'system', content: string }[]` + - Do we need to include tool call messages or tool result messages? + +3. **Does AI SDK v6 require a specific message format for tool calls?** + - Should we be including `toolInvocations` or `tool_calls` in the message history? + - Are we missing required fields in the `CoreMessage` type? + +4. **Is the client-side streaming reader correct?** + - We're reading chunks with `response.body.getReader()` + - Should we be parsing SSE events differently for tool calls? + +5. **Does `streamText()` with tools require `maxSteps` parameter?** + - Do we need to set `maxSteps: 5` to allow multi-step reasoning? + +6. **Are we handling the conversation history correctly?** + - We're sending all previous messages on each request + - Should we be including assistant messages with tool call results? + +## AI SDK v6 Documentation References + +We're following these patterns from the official docs: + +- [streamText() API](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) +- [tool() API](https://ai-sdk.dev/docs/reference/ai-sdk-core/tool) +- [Tool Calling Guide](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) + +But we might be missing something specific about: +- How to handle streaming when tools are executed +- What response format tool calls produce +- How to parse the stream when tools are involved + +## Suspected Issue + +**The message format might be wrong.** We're sending: + +```typescript +const systemMessage: CoreMessage = { + role: 'system', + content: `You are a helpful AI assistant...`, +}; + +const result = streamText({ + model: openai('gpt-4o-mini'), + messages: [systemMessage, ...messages], + tools, +}); +``` + +But `CoreMessage` might need additional fields when tools are involved, or we might need to handle tool call results differently in the conversation history. + +## What We Need + +1. Correct message format for `streamText()` with tools +2. How to properly stream responses that include tool calls +3. Whether we need different client-side parsing for tool call streams +4. Example of a working Next.js API route using AI SDK v6 with `streamText()` and tools + +## Repo Context + +- Monorepo using Turborepo + pnpm workspaces +- Next.js 16 App Router with Turbopack +- TypeScript strict mode +- All UI components from internal `@tpmjs/ui` package +- Tools are imported from workspace package `@tpmjs/hello` diff --git a/USECHAT_INPUT_UNDEFINED.md b/USECHAT_INPUT_UNDEFINED.md new file mode 100644 index 0000000..861026c --- /dev/null +++ b/USECHAT_INPUT_UNDEFINED.md @@ -0,0 +1,400 @@ +# useChat Hook Returns Undefined Input Property + +## Problem + +Using `@ai-sdk/react`'s `useChat` hook, the `input` property is returning `undefined`, causing the application to crash when trying to call `.trim()` on it. + +## Error + +``` +TypeError: Cannot read properties of undefined (reading 'trim') + +at ChatInput (src/components/chat/ChatInput.tsx:41:48) +``` + +**Code that fails:** +```typescript +