tpmjs/OPENAI_SCHEMA_ERROR.md
Ajax Davis 0658aea425 feat: implement playground app with AI SDK v6 tool execution
- 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 <noreply@anthropic.com>
2025-12-04 02:51:02 +10:00

522 lines
14 KiB
Markdown

# 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<any> {
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<Record<string, any>> {
const installedTools = ['@tpmjs/hello', 'firecrawl-aisdk'];
const tools: Record<string, any> = {};
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<any>
}
```
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<HelloWorldInput>({
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)