Skip to content

Commit 22ccaaa

Browse files
authored
fix(openai): apply strict fallback in the provider-path function-tool converter (#790)
* fix(openai): apply strict fallback in the provider-path function-tool converter The provider tool dispatcher (convertToolsToProviderFormat) routes MCP and function tools through convertFunctionToolToAdapterFormat, which still forced strict:true and 400'd on schemas outside OpenAI's strict subset (e.g. Notion MCP tools using $defs/oneOf). Apply the same isStrictModeCompatible check used by the responses/chat-completions converters: emit strict:false and strip unsupported formats when the schema can't be strict, keeping the tool callable. * chore: add changeset for openai strict-fallback function-tool fix
1 parent 2f2d8a9 commit 22ccaaa

3 files changed

Lines changed: 51 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/openai-base': patch
3+
---
4+
5+
Apply the strict-mode fallback in the provider-path `function-tool` converter. A tool whose input JSON Schema can't satisfy OpenAI's strict function-calling constraints now falls back to a non-strict tool definition (matching the converter's other path) instead of emitting an invalid strict tool, so such tools work across the OpenAI-based adapters (`@tanstack/ai-openai`, `@tanstack/ai-grok`, `@tanstack/ai-groq`).

packages/openai-base/src/tools/function-tool.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { makeStructuredOutputCompatible } from '../utils/schema-converter'
1+
import {
2+
isStrictModeCompatible,
3+
makeStructuredOutputCompatible,
4+
stripUnsupportedFormats,
5+
} from '../utils/schema-converter'
26
import type { FunctionTool as FunctionToolConfig } from 'openai/resources/responses/responses'
37
import type { JSONSchema, Tool } from '@tanstack/ai'
48

@@ -17,6 +21,12 @@ export type FunctionTool = FunctionToolConfig
1721
* - additionalProperties: false
1822
*
1923
* This enables strict mode for all tools automatically.
24+
*
25+
* Some tool schemas (e.g. MCP server tools that use `oneOf`, `$ref`, or
26+
* `$defs`) cannot be expressed under OpenAI's strict Structured Outputs
27+
* subset. For those we fall back to a non-strict tool definition — stripping
28+
* only the formats OpenAI rejects — so the tool is still usable instead of
29+
* failing the request with a 400 "Invalid schema" error.
2030
*/
2131
export function convertFunctionToolToAdapterFormat(
2232
tool: Tool,
@@ -27,6 +37,16 @@ export function convertFunctionToolToAdapterFormat(
2737
required: [],
2838
}) as JSONSchema
2939

40+
if (!isStrictModeCompatible(inputSchema)) {
41+
return {
42+
type: 'function',
43+
name: tool.name,
44+
description: tool.description,
45+
parameters: stripUnsupportedFormats(inputSchema),
46+
strict: false,
47+
} satisfies FunctionToolConfig
48+
}
49+
3050
const jsonSchema = makeStructuredOutputCompatible(
3151
inputSchema,
3252
inputSchema.required || [],

packages/openai-base/tests/tool-converter-strict-fallback.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import { convertFunctionToolToResponsesFormat } from '../src/adapters/responses-tool-converter'
33
import { convertFunctionToolToChatCompletionsFormat } from '../src/adapters/chat-completions-tool-converter'
4+
import { convertFunctionToolToAdapterFormat } from '../src/tools/function-tool'
45
import type { Tool } from '@tanstack/ai'
56

67
/** A schema fully inside OpenAI's strict Structured Outputs subset. */
@@ -74,3 +75,27 @@ describe('chat-completions tool converter — strict fallback', () => {
7475
expect(params.properties.site.format).toBeUndefined()
7576
})
7677
})
78+
79+
// This is the converter the provider tool-dispatcher (`convertToolsToProviderFormat`)
80+
// actually uses for MCP / function tools on the Responses + Chat Completions
81+
// adapters, so the same strict fallback must apply here.
82+
describe('function-tool adapter converter — strict fallback', () => {
83+
it('uses strict:true for strict-subset schemas', () => {
84+
const out = convertFunctionToolToAdapterFormat(strictSafeTool)
85+
expect(out.strict).toBe(true)
86+
expect(
87+
(out.parameters as Record<string, unknown>).additionalProperties,
88+
).toBe(false)
89+
})
90+
91+
it('falls back to strict:false for schemas with unsupported keywords', () => {
92+
const out = convertFunctionToolToAdapterFormat(gnarlyTool)
93+
expect(out.strict).toBe(false)
94+
// Schema is preserved (not corrupted) so the tool stays callable...
95+
const params = out.parameters as any
96+
expect(params.$defs.parent.oneOf).toBeDefined()
97+
// ...but unsupported `format` keywords are still stripped.
98+
expect(params.properties.site.format).toBeUndefined()
99+
expect(params.properties.user_id.format).toBe('uuid')
100+
})
101+
})

0 commit comments

Comments
 (0)