Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions .changeset/full-stars-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-openai': patch
'@tanstack/ai': patch
'@tanstack/ai-solid': patch
---

Fix up model names for OpenAI and release the new response API's
Comment thread
AlemTuzlak marked this conversation as resolved.
Outdated
8 changes: 4 additions & 4 deletions packages/typescript/ai-openai/src/model-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ const GPT5_2_PRO = {
>

const GPT5_2_CHAT = {
name: 'gpt-5.2-chat',
name: 'gpt-5.2-chat-latest',
context_window: 128_000,
max_output_tokens: 16_384,
knowledge_cutoff: '2025-08-31',
Expand Down Expand Up @@ -1271,7 +1271,7 @@ const GPT_4_TURBO = {
>

const CHATGPT_40 = {
name: 'chatgpt-4.0',
name: 'chatgpt-4o-latest',
context_window: 128_000,
max_output_tokens: 4_096,
knowledge_cutoff: '2023-10-01',
Expand Down Expand Up @@ -1539,7 +1539,7 @@ const GPT_4O_TRANSCRIBE_DIARIZE = {
> */

const GPT_5_1_CHAT = {
name: 'gpt-5.1-chat',
name: 'gpt-5.1-chat-latest',
context_window: 128_000,
max_output_tokens: 16_384,
knowledge_cutoff: '2024-09-30',
Expand Down Expand Up @@ -1568,7 +1568,7 @@ const GPT_5_1_CHAT = {
>

const GPT_5_CHAT = {
name: 'gpt-5-chat',
name: 'gpt-5-chat-latest',
context_window: 128_000,
max_output_tokens: 16_384,
knowledge_cutoff: '2024-09-30',
Expand Down
22 changes: 21 additions & 1 deletion packages/typescript/ai-openai/src/utils/schema-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function makeOpenAIStructuredOutputCompatible(
const prop = properties[propName]
const wasOptional = !originalRequired.includes(propName)

// Recursively transform nested objects/arrays
// Recursively transform nested objects/arrays/unions
if (prop.type === 'object' && prop.properties) {
properties[propName] = makeOpenAIStructuredOutputCompatible(
prop,
Expand All @@ -75,6 +75,12 @@ export function makeOpenAIStructuredOutputCompatible(
prop.items.required || [],
),
}
} else if (prop.anyOf || prop.oneOf) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to notice, oneOf is not part of the open ai supported types

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah you're right, thank you! Wanted to cover both while I was at it πŸ˜“

// Handle anyOf/oneOf at property level (union types)
properties[propName] = makeOpenAIStructuredOutputCompatible(
prop,
prop.required || [],
)
} else if (wasOptional) {
// Make optional fields nullable by adding null to the type
if (prop.type && !Array.isArray(prop.type)) {
Expand Down Expand Up @@ -106,5 +112,19 @@ export function makeOpenAIStructuredOutputCompatible(
)
}

// Handle anyOf (union types) - each variant needs to be transformed
if (result.anyOf && Array.isArray(result.anyOf)) {
result.anyOf = result.anyOf.map((variant) =>
makeOpenAIStructuredOutputCompatible(variant, variant.required || []),
)
}

// Handle oneOf (discriminated unions) - each variant needs to be transformed
if (result.oneOf && Array.isArray(result.oneOf)) {
result.oneOf = result.oneOf.map((variant) =>
makeOpenAIStructuredOutputCompatible(variant, variant.required || []),
)
}

return result
}
183 changes: 183 additions & 0 deletions packages/typescript/ai-openai/tests/schema-converter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { describe, expect, it } from 'vitest'
import {
makeOpenAIStructuredOutputCompatible,
transformNullsToUndefined,
} from '../src/utils/schema-converter'

Comment thread
AlemTuzlak marked this conversation as resolved.
describe('transformNullsToUndefined', () => {
it('should convert null to undefined', () => {
expect(transformNullsToUndefined(null)).toBe(undefined)
})

it('should handle nested objects with null values', () => {
const input = { a: 'hello', b: null, c: { d: null, e: 'world' } }
const result = transformNullsToUndefined(input)
expect(result).toEqual({ a: 'hello', c: { e: 'world' } })
})

it('should handle arrays with null values', () => {
const input = [1, null, 3]
const result = transformNullsToUndefined(input)
expect(result).toEqual([1, undefined, 3])
})

it('should preserve non-null values', () => {
const input = { a: 'string', b: 123, c: true, d: [1, 2, 3] }
const result = transformNullsToUndefined(input)
expect(result).toEqual(input)
})
})

describe('makeOpenAIStructuredOutputCompatible', () => {
it('should add additionalProperties: false to object schemas', () => {
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
},
required: ['name'],
}

const result = makeOpenAIStructuredOutputCompatible(schema, ['name'])
expect(result.additionalProperties).toBe(false)
})

it('should make all properties required', () => {
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name'],
}

const result = makeOpenAIStructuredOutputCompatible(schema, ['name'])
expect(result.required).toEqual(['name', 'age'])
})

it('should make optional fields nullable', () => {
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
nickname: { type: 'string' },
},
required: ['name'],
}

const result = makeOpenAIStructuredOutputCompatible(schema, ['name'])
expect(result.properties.name.type).toBe('string')
expect(result.properties.nickname.type).toEqual(['string', 'null'])
})

it('should handle anyOf (union types) by transforming each variant', () => {
const schema = {
type: 'object',
properties: {
u: {
anyOf: [
{
type: 'object',
properties: { a: { type: 'string' } },
required: ['a'],
},
{
type: 'object',
properties: { b: { type: 'number' } },
required: ['b'],
},
],
},
},
required: ['u'],
}

const result = makeOpenAIStructuredOutputCompatible(schema, ['u'])

// Each variant in anyOf should have additionalProperties: false
expect(result.properties.u.anyOf[0].additionalProperties).toBe(false)
expect(result.properties.u.anyOf[1].additionalProperties).toBe(false)
})
Comment thread
AlemTuzlak marked this conversation as resolved.

it('should handle oneOf (discriminated unions) by transforming each variant', () => {
const schema = {
type: 'object',
properties: {
u: {
oneOf: [
{
type: 'object',
properties: { type: { const: 'a' }, value: { type: 'string' } },
required: ['type', 'value'],
},
{
type: 'object',
properties: { type: { const: 'b' }, count: { type: 'number' } },
required: ['type', 'count'],
},
],
},
},
required: ['u'],
}

const result = makeOpenAIStructuredOutputCompatible(schema, ['u'])

// Each variant in oneOf should have additionalProperties: false
expect(result.properties.u.oneOf[0].additionalProperties).toBe(false)
expect(result.properties.u.oneOf[1].additionalProperties).toBe(false)
})

it('should handle nested objects inside anyOf', () => {
const schema = {
type: 'object',
properties: {
data: {
anyOf: [
{
type: 'object',
properties: {
nested: {
type: 'object',
properties: { x: { type: 'string' } },
required: ['x'],
},
},
required: ['nested'],
},
],
},
},
required: ['data'],
}

const result = makeOpenAIStructuredOutputCompatible(schema, ['data'])

// The nested object inside anyOf variant should also have additionalProperties: false
expect(result.properties.data.anyOf[0].additionalProperties).toBe(false)
expect(
result.properties.data.anyOf[0].properties.nested.additionalProperties,
).toBe(false)
})

it('should handle arrays with items', () => {
const schema = {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object',
properties: { id: { type: 'string' } },
required: ['id'],
},
},
},
required: ['items'],
}

const result = makeOpenAIStructuredOutputCompatible(schema, ['items'])
expect(result.properties.items.items.additionalProperties).toBe(false)
})
})
2 changes: 1 addition & 1 deletion packages/typescript/ai/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export interface JSONSchema {
maxProperties?: number
title?: string
examples?: Array<unknown>
[key: string]: unknown // Allow additional properties for extensibility
[key: string]: any // Allow additional properties for extensibility
}

/**
Expand Down
Loading