Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignoreDependencies": ["@faker-js/faker"],
"ignoreDependencies": ["@faker-js/faker", "@playwright/test"],
"ignoreWorkspaces": ["examples/**"],
"ignore": [
"packages/typescript/ai-openai/live-tests/**",
"packages/typescript/ai-openai/src/**/*.test.ts",
"packages/typescript/ai-openai/src/audio/audio-provider-options.ts",
"packages/typescript/ai-openai/src/audio/transcribe-provider-options.ts",
"packages/typescript/ai-openai/src/image/image-provider-options.ts",
"packages/typescript/smoke-tests/adapters/src/**",
"packages/typescript/smoke-tests/e2e/playwright.config.ts",
"packages/typescript/smoke-tests/e2e/src/**",
"packages/typescript/smoke-tests/e2e/vite.config.ts"
],
"ignoreExportsUsedInFile": true,
"workspaces": {
"packages/react-ai": {
"ignore": []
},
"packages/typescript/ai-anthropic": {
"ignore": ["src/tools/**"]
},
"packages/typescript/ai-gemini": {
"ignore": ["src/tools/**"]
},
"packages/typescript/ai-openai": {
"ignore": ["src/tools/**"]
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"clean": "find . -name 'dist' -type d -prune -exec rm -rf {} +",
"clean:node_modules": "find . -name 'node_modules' -type d -prune -exec rm -rf {} +",
"clean:all": "pnpm run clean && pnpm run clean:node_modules",
"copy:readme": "cp README.md packages/typescript/ai/README.md && cp README.md packages/typescript/ai-devtools/README.md && cp README.md packages/typescript/ai-client/README.md && cp README.md packages/typescript/ai-gemini/README.md && cp README.md packages/typescript/ai-ollama/README.md && cp README.md packages/typescript/ai-openai/README.md && cp README.md packages/typescript/ai-react/README.md && cp README.md packages/typescript/ai-react-ui/README.md && cp README.md packages/typescript/react-ai-devtools/README.md && cp README.md packages/typescript/solid-ai-devtools/README.md && cp README.md packages/typescript/tests-adapters/README.md",
"copy:readme": "cp README.md packages/typescript/ai/README.md && cp README.md packages/typescript/ai-devtools/README.md && cp README.md packages/typescript/ai-client/README.md && cp README.md packages/typescript/ai-gemini/README.md && cp README.md packages/typescript/ai-ollama/README.md && cp README.md packages/typescript/ai-openai/README.md && cp README.md packages/typescript/ai-react/README.md && cp README.md packages/typescript/ai-react-ui/README.md && cp README.md packages/typescript/react-ai-devtools/README.md && cp README.md packages/typescript/solid-ai-devtools/README.md",
"dev": "pnpm run watch",
"docs:generate": "node scripts/generateDocs.js && pnpm run copy:readme",
"format": "pnpm run prettier:write",
Expand Down
59 changes: 59 additions & 0 deletions packages/typescript/ai-openai/live-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# OpenAI Adapter Live Tests

This directory contains live integration tests for the OpenAI adapter using the Responses API.

## Setup

1. Create a `.env.local` file in this directory with your OpenAI API key:

```
OPENAI_API_KEY=sk-...
```

2. Install dependencies from the workspace root:

```bash
pnpm install
```

## Running Tests

Run individual tests:

```bash
pnpm test # Test with required parameters
pnpm test:optional # Test with optional parameters
```

Run all tests:

```bash
pnpm test:all
```

## Test Scripts

### `tool-test.ts`

Tests tool calling with all required parameters. Verifies that:

- Tool calls are properly detected in the stream
- Function names are correctly captured
- Arguments are passed as JSON strings
- Tools can be executed with the parsed arguments

### `tool-test-optional.ts`

Tests tool calling with optional parameters. Verifies that:

- Tools with optional parameters work correctly
- The strict mode is disabled when not all parameters are required
- Default values can be applied for missing optional parameters

## Key Findings

The OpenAI Responses API has different behavior compared to the Chat Completions API:

1. **Strict Mode**: When `strict: true`, ALL properties must be in the `required` array
2. **Tool Metadata**: Function names come from `response.output_item.added` events, not from `response.function_call_arguments.done`
3. **Finish Reason**: The Responses API doesn't have a `finish_reason` field; it must be inferred from the output content
18 changes: 18 additions & 0 deletions packages/typescript/ai-openai/live-tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "ai-openai-live-tests",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "tsx tool-test.ts",
"test:optional": "tsx tool-test-optional.ts",
"test:all": "tsx tool-test.ts && tsx tool-test-optional.ts"
},
"dependencies": {
"@tanstack/ai": "workspace:*",
"@tanstack/ai-openai": "workspace:*"
},
"devDependencies": {
"tsx": "^4.19.2"
}
}
182 changes: 182 additions & 0 deletions packages/typescript/ai-openai/live-tests/tool-test-optional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { createOpenAI } from '../src/index'
import { readFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'

// Load environment variables from .env.local manually
const __dirname = dirname(fileURLToPath(import.meta.url))
try {
const envContent = readFileSync(join(__dirname, '.env.local'), 'utf-8')
envContent.split('\n').forEach((line) => {
const match = line.match(/^([^=]+)=(.*)$/)
if (match) {
process.env[match[1].trim()] = match[2].trim()
}
})
} catch (e) {
// .env.local not found, will use process.env
}

const apiKey = process.env.OPENAI_API_KEY

if (!apiKey) {
console.error('❌ OPENAI_API_KEY not found in .env.local')
process.exit(1)
}

async function testToolWithOptionalParameters() {
console.log('🚀 Testing OpenAI tool calling with OPTIONAL parameters\n')

const adapter = createOpenAI(apiKey)

// Create a tool with optional parameters (unit is optional)
const getTemperatureTool = {
type: 'function' as const,
function: {
name: 'get_temperature',
description: 'Get the current temperature for a specific location',
parameters: {
type: 'object',
properties: {
location: {
type: 'string',
description: 'The city or location to get the temperature for',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description:
'The temperature unit (optional, defaults to fahrenheit)',
},
},
required: ['location'], // unit is optional
},
},
execute: async (args: any) => {
console.log(
'✅ Tool executed with arguments:',
JSON.stringify(args, null, 2),
)

if (!args || !args.location) {
console.error('❌ ERROR: Location argument is missing!')
return 'Error: Location is required'
}

const unit = args.unit || 'fahrenheit'
console.log(` - location: "${args.location}"`)
console.log(
` - unit: "${unit}" (${args.unit ? 'provided' : 'defaulted'})`,
)

return `The temperature in ${args.location} is 72°${unit === 'celsius' ? 'C' : 'F'}`
},
}

const messages = [
{
role: 'user' as const,
content:
'What is the temperature in Paris? Use the get_temperature tool.',
},
]

console.log('📤 Sending request with tool:')
console.log(' Tool name:', getTemperatureTool.function.name)
console.log(
' Required params:',
getTemperatureTool.function.parameters.required,
)
console.log(' Optional params:', ['unit'])
console.log(' User message:', messages[0].content)
console.log()

try {
console.log('📥 Streaming response...\n')

let toolCallFound = false
let toolCallArguments: any = null
let toolExecuted = false
let finalResponse = ''

// @ts-ignore - using internal chat method
const stream = adapter.chatStream({
model: 'gpt-4o-mini',
messages,
tools: [getTemperatureTool],
})

for await (const chunk of stream) {
if (chunk.type === 'tool_call') {
toolCallFound = true
toolCallArguments = chunk.toolCall.function.arguments
console.log('🔧 Tool call detected!')
console.log(' Name:', chunk.toolCall.function.name)
console.log(' Arguments (raw):', toolCallArguments)

// Parse if it's a string
if (typeof toolCallArguments === 'string') {
try {
const parsed = JSON.parse(toolCallArguments)
toolCallArguments = parsed
} catch (e) {
console.error(' ❌ Failed to parse arguments:', e)
}
}

// Execute the tool
if (getTemperatureTool.execute) {
console.log('\n🔨 Executing tool...')
try {
const result = await getTemperatureTool.execute(toolCallArguments)
toolExecuted = true
console.log(' Result:', result)
} catch (error) {
console.error(' ❌ Tool execution error:', error)
}
}
}

if (chunk.type === 'content') {
finalResponse += chunk.delta
}
}

console.log('\n' + '='.repeat(60))
console.log('📊 Test Summary:')
console.log(' Tool call found:', toolCallFound ? '✅' : '❌')
console.log(' Arguments received:', toolCallArguments ? '✅' : '❌')
console.log(' Tool executed:', toolExecuted ? '✅' : '❌')
console.log(
' Location provided:',
toolCallArguments?.location ? '✅' : '❌',
)
console.log('='.repeat(60))

if (!toolCallFound) {
console.error('\n❌ FAIL: No tool call was detected')
process.exit(1)
}

if (!toolCallArguments || !toolCallArguments.location) {
console.error('\n❌ FAIL: Tool arguments missing or invalid')
process.exit(1)
}

if (!toolExecuted) {
console.error('\n❌ FAIL: Tool was not executed successfully')
process.exit(1)
}

console.log(
'\n✅ SUCCESS: Tool calling with optional parameters works correctly!',
)
process.exit(0)
} catch (error: any) {
console.error('\n❌ ERROR:', error.message)
console.error('Stack:', error.stack)
process.exit(1)
}
}

testToolWithOptionalParameters()
Loading
Loading