@tanstack/openai-base 0.8.4 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,7 +18,14 @@ export type ChatCompletionFunctionTool = Extract<ChatCompletionTool, {
18
18
  * - Optional fields made nullable
19
19
  * - additionalProperties: false
20
20
  *
21
- * This enables strict mode for all tools automatically.
21
+ * This enables strict mode for tools whose schemas fit OpenAI's strict subset.
22
+ *
23
+ * Schemas using keywords outside that subset (`oneOf`/`allOf`/`not`/`$ref`/
24
+ * `$defs` — common with MCP servers like Notion) can't be coerced to a
25
+ * strict-valid shape, and `strict: true` would make the API reject the ENTIRE
26
+ * request with a 400. Such tools are emitted with `strict: false` (their schema
27
+ * passed through, only unsupported `format` keywords stripped) so they stay
28
+ * callable.
22
29
  */
23
30
  export declare function convertFunctionToolToChatCompletionsFormat(tool: Tool, schemaConverter?: (schema: Record<string, any>, required: Array<string>) => Record<string, any>): ChatCompletionFunctionTool;
24
31
  /**
@@ -1,10 +1,21 @@
1
- import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
1
+ import { makeStructuredOutputCompatible, isStrictModeCompatible, stripUnsupportedFormats } from "../utils/schema-converter.js";
2
2
  function convertFunctionToolToChatCompletionsFormat(tool, schemaConverter = makeStructuredOutputCompatible) {
3
3
  const inputSchema = tool.inputSchema ?? {
4
4
  type: "object",
5
5
  properties: {},
6
6
  required: []
7
7
  };
8
+ if (!isStrictModeCompatible(inputSchema)) {
9
+ return {
10
+ type: "function",
11
+ function: {
12
+ name: tool.name,
13
+ description: tool.description,
14
+ parameters: stripUnsupportedFormats(inputSchema),
15
+ strict: false
16
+ }
17
+ };
18
+ }
8
19
  const jsonSchema = {
9
20
  ...schemaConverter(inputSchema, inputSchema.required || [])
10
21
  };
@@ -1 +1 @@
1
- {"version":3,"file":"chat-completions-tool-converter.js","sources":["../../../src/adapters/chat-completions-tool-converter.ts"],"sourcesContent":["import { makeStructuredOutputCompatible } from '../utils/schema-converter'\nimport type { ChatCompletionTool } from 'openai/resources/chat/completions/completions'\nimport type { JSONSchema, Tool } from '@tanstack/ai'\n\n/**\n * Chat Completions API tool format. The SDK's `ChatCompletionTool` is the\n * union `ChatCompletionFunctionTool | ChatCompletionCustomTool`; we only\n * emit the function variant here. Re-exported as our own alias so consumers\n * importing the converter's output don't have to reach into the SDK.\n */\nexport type ChatCompletionFunctionTool = Extract<\n ChatCompletionTool,\n { type: 'function' }\n>\n\n/**\n * Converts a standard Tool to OpenAI Chat Completions ChatCompletionTool format.\n *\n * Tool schemas are already converted to JSON Schema in the ai layer.\n * We apply OpenAI-compatible transformations for strict mode:\n * - All properties in required array\n * - Optional fields made nullable\n * - additionalProperties: false\n *\n * This enables strict mode for all tools automatically.\n */\nexport function convertFunctionToolToChatCompletionsFormat(\n tool: Tool,\n schemaConverter: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any> = makeStructuredOutputCompatible,\n): ChatCompletionFunctionTool {\n const inputSchema = (tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n }) as JSONSchema\n\n // Shallow-copy the converter's result before mutating: a subclass-supplied\n // schemaConverter has no contract requirement to return a fresh object,\n // and a passthrough `(s) => s` would otherwise have its caller's schema\n // mutated by the `additionalProperties = false` assignment below.\n const jsonSchema = {\n ...schemaConverter(inputSchema, inputSchema.required || []),\n }\n jsonSchema.additionalProperties = false\n\n return {\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: jsonSchema,\n strict: true,\n },\n } satisfies ChatCompletionFunctionTool\n}\n\n/**\n * Converts an array of standard Tools to Chat Completions format.\n * Chat Completions API primarily supports function tools.\n */\nexport function convertToolsToChatCompletionsFormat(\n tools: Array<Tool>,\n schemaConverter?: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any>,\n): Array<ChatCompletionFunctionTool> {\n return tools.map((tool) =>\n convertFunctionToolToChatCompletionsFormat(tool, schemaConverter),\n )\n}\n"],"names":[],"mappings":";AA0BO,SAAS,2CACd,MACA,kBAG2B,gCACC;AAC5B,QAAM,cAAe,KAAK,eAAe;AAAA,IACvC,MAAM;AAAA,IACN,YAAY,CAAA;AAAA,IACZ,UAAU,CAAA;AAAA,EAAC;AAOb,QAAM,aAAa;AAAA,IACjB,GAAG,gBAAgB,aAAa,YAAY,YAAY,CAAA,CAAE;AAAA,EAAA;AAE5D,aAAW,uBAAuB;AAElC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,MACR,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,YAAY;AAAA,MACZ,QAAQ;AAAA,IAAA;AAAA,EACV;AAEJ;AAMO,SAAS,oCACd,OACA,iBAImC;AACnC,SAAO,MAAM;AAAA,IAAI,CAAC,SAChB,2CAA2C,MAAM,eAAe;AAAA,EAAA;AAEpE;"}
1
+ {"version":3,"file":"chat-completions-tool-converter.js","sources":["../../../src/adapters/chat-completions-tool-converter.ts"],"sourcesContent":["import {\n isStrictModeCompatible,\n makeStructuredOutputCompatible,\n stripUnsupportedFormats,\n} from '../utils/schema-converter'\nimport type { ChatCompletionTool } from 'openai/resources/chat/completions/completions'\nimport type { JSONSchema, Tool } from '@tanstack/ai'\n\n/**\n * Chat Completions API tool format. The SDK's `ChatCompletionTool` is the\n * union `ChatCompletionFunctionTool | ChatCompletionCustomTool`; we only\n * emit the function variant here. Re-exported as our own alias so consumers\n * importing the converter's output don't have to reach into the SDK.\n */\nexport type ChatCompletionFunctionTool = Extract<\n ChatCompletionTool,\n { type: 'function' }\n>\n\n/**\n * Converts a standard Tool to OpenAI Chat Completions ChatCompletionTool format.\n *\n * Tool schemas are already converted to JSON Schema in the ai layer.\n * We apply OpenAI-compatible transformations for strict mode:\n * - All properties in required array\n * - Optional fields made nullable\n * - additionalProperties: false\n *\n * This enables strict mode for tools whose schemas fit OpenAI's strict subset.\n *\n * Schemas using keywords outside that subset (`oneOf`/`allOf`/`not`/`$ref`/\n * `$defs` — common with MCP servers like Notion) can't be coerced to a\n * strict-valid shape, and `strict: true` would make the API reject the ENTIRE\n * request with a 400. Such tools are emitted with `strict: false` (their schema\n * passed through, only unsupported `format` keywords stripped) so they stay\n * callable.\n */\nexport function convertFunctionToolToChatCompletionsFormat(\n tool: Tool,\n schemaConverter: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any> = makeStructuredOutputCompatible,\n): ChatCompletionFunctionTool {\n const inputSchema = (tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n }) as JSONSchema\n\n // Schema outside OpenAI's strict subset: send non-strict so the tool still\n // works instead of 400-ing the whole request.\n if (!isStrictModeCompatible(inputSchema)) {\n return {\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: stripUnsupportedFormats(inputSchema),\n strict: false,\n },\n } satisfies ChatCompletionFunctionTool\n }\n\n // Shallow-copy the converter's result before mutating: a subclass-supplied\n // schemaConverter has no contract requirement to return a fresh object,\n // and a passthrough `(s) => s` would otherwise have its caller's schema\n // mutated by the `additionalProperties = false` assignment below.\n const jsonSchema = {\n ...schemaConverter(inputSchema, inputSchema.required || []),\n }\n jsonSchema.additionalProperties = false\n\n return {\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: jsonSchema,\n strict: true,\n },\n } satisfies ChatCompletionFunctionTool\n}\n\n/**\n * Converts an array of standard Tools to Chat Completions format.\n * Chat Completions API primarily supports function tools.\n */\nexport function convertToolsToChatCompletionsFormat(\n tools: Array<Tool>,\n schemaConverter?: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any>,\n): Array<ChatCompletionFunctionTool> {\n return tools.map((tool) =>\n convertFunctionToolToChatCompletionsFormat(tool, schemaConverter),\n )\n}\n"],"names":[],"mappings":";AAqCO,SAAS,2CACd,MACA,kBAG2B,gCACC;AAC5B,QAAM,cAAe,KAAK,eAAe;AAAA,IACvC,MAAM;AAAA,IACN,YAAY,CAAA;AAAA,IACZ,UAAU,CAAA;AAAA,EAAC;AAKb,MAAI,CAAC,uBAAuB,WAAW,GAAG;AACxC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM,KAAK;AAAA,QACX,aAAa,KAAK;AAAA,QAClB,YAAY,wBAAwB,WAAW;AAAA,QAC/C,QAAQ;AAAA,MAAA;AAAA,IACV;AAAA,EAEJ;AAMA,QAAM,aAAa;AAAA,IACjB,GAAG,gBAAgB,aAAa,YAAY,YAAY,CAAA,CAAE;AAAA,EAAA;AAE5D,aAAW,uBAAuB;AAElC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,MACR,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,YAAY;AAAA,MACZ,QAAQ;AAAA,IAAA;AAAA,EACV;AAEJ;AAMO,SAAS,oCACd,OACA,iBAImC;AACnC,SAAO,MAAM;AAAA,IAAI,CAAC,SAChB,2CAA2C,MAAM,eAAe;AAAA,EAAA;AAEpE;"}
@@ -25,7 +25,14 @@ export interface ResponsesFunctionTool {
25
25
  * - Optional fields made nullable
26
26
  * - additionalProperties: false
27
27
  *
28
- * This enables strict mode for all tools automatically.
28
+ * This enables strict mode for tools whose schemas fit OpenAI's strict subset.
29
+ *
30
+ * Schemas using keywords outside that subset (`oneOf`/`allOf`/`not`/`$ref`/
31
+ * `$defs` — common with MCP servers like Notion) can't be coerced to a
32
+ * strict-valid shape, and `strict: true` would make the Responses API reject
33
+ * the ENTIRE request with a 400. Such tools are emitted with `strict: false`
34
+ * (their schema passed through, only unsupported `format` keywords stripped) so
35
+ * they stay callable.
29
36
  */
30
37
  export declare function convertFunctionToolToResponsesFormat(tool: Tool, schemaConverter?: (schema: Record<string, any>, required: Array<string>) => Record<string, any>): ResponsesFunctionTool;
31
38
  /**
@@ -1,10 +1,19 @@
1
- import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
1
+ import { makeStructuredOutputCompatible, isStrictModeCompatible, stripUnsupportedFormats } from "../utils/schema-converter.js";
2
2
  function convertFunctionToolToResponsesFormat(tool, schemaConverter = makeStructuredOutputCompatible) {
3
3
  const inputSchema = tool.inputSchema ?? {
4
4
  type: "object",
5
5
  properties: {},
6
6
  required: []
7
7
  };
8
+ if (!isStrictModeCompatible(inputSchema)) {
9
+ return {
10
+ type: "function",
11
+ name: tool.name,
12
+ description: tool.description,
13
+ parameters: stripUnsupportedFormats(inputSchema),
14
+ strict: false
15
+ };
16
+ }
8
17
  const jsonSchema = {
9
18
  ...schemaConverter(inputSchema, inputSchema.required || [])
10
19
  };
@@ -1 +1 @@
1
- {"version":3,"file":"responses-tool-converter.js","sources":["../../../src/adapters/responses-tool-converter.ts"],"sourcesContent":["import { makeStructuredOutputCompatible } from '../utils/schema-converter'\nimport type { JSONSchema, Tool } from '@tanstack/ai'\n\n/**\n * Responses API function tool format.\n * This is distinct from the Chat Completions API tool format.\n *\n * The Responses API uses a flatter structure:\n * { type: 'function', name: string, description?: string, parameters: object, strict?: boolean }\n *\n * vs. Chat Completions:\n * { type: 'function', function: { name, description, parameters }, strict?: boolean }\n */\nexport interface ResponsesFunctionTool {\n type: 'function'\n name: string\n description?: string | null\n parameters: Record<string, any> | null\n strict: boolean | null\n}\n\n/**\n * Converts a standard Tool to the Responses API FunctionTool format.\n *\n * Tool schemas are already converted to JSON Schema in the ai layer.\n * We apply OpenAI-compatible transformations for strict mode:\n * - All properties in required array\n * - Optional fields made nullable\n * - additionalProperties: false\n *\n * This enables strict mode for all tools automatically.\n */\nexport function convertFunctionToolToResponsesFormat(\n tool: Tool,\n schemaConverter: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any> = makeStructuredOutputCompatible,\n): ResponsesFunctionTool {\n const inputSchema = (tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n }) as JSONSchema\n\n // Shallow-copy the converter's result before mutating — a subclass-supplied\n // schemaConverter has no contract requirement to return a fresh object;\n // mutating in place could corrupt the caller's tool definition.\n const jsonSchema = {\n ...schemaConverter(inputSchema, inputSchema.required || []),\n }\n jsonSchema.additionalProperties = false\n\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: jsonSchema,\n strict: true,\n }\n}\n\n/**\n * Converts an array of standard Tools to Responses API format.\n * The Responses API primarily supports function tools at the base level.\n */\nexport function convertToolsToResponsesFormat(\n tools: Array<Tool>,\n schemaConverter?: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any>,\n): Array<ResponsesFunctionTool> {\n return tools.map((tool) =>\n convertFunctionToolToResponsesFormat(tool, schemaConverter),\n )\n}\n"],"names":[],"mappings":";AAgCO,SAAS,qCACd,MACA,kBAG2B,gCACJ;AACvB,QAAM,cAAe,KAAK,eAAe;AAAA,IACvC,MAAM;AAAA,IACN,YAAY,CAAA;AAAA,IACZ,UAAU,CAAA;AAAA,EAAC;AAMb,QAAM,aAAa;AAAA,IACjB,GAAG,gBAAgB,aAAa,YAAY,YAAY,CAAA,CAAE;AAAA,EAAA;AAE5D,aAAW,uBAAuB;AAElC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,YAAY;AAAA,IACZ,QAAQ;AAAA,EAAA;AAEZ;AAMO,SAAS,8BACd,OACA,iBAI8B;AAC9B,SAAO,MAAM;AAAA,IAAI,CAAC,SAChB,qCAAqC,MAAM,eAAe;AAAA,EAAA;AAE9D;"}
1
+ {"version":3,"file":"responses-tool-converter.js","sources":["../../../src/adapters/responses-tool-converter.ts"],"sourcesContent":["import {\n isStrictModeCompatible,\n makeStructuredOutputCompatible,\n stripUnsupportedFormats,\n} from '../utils/schema-converter'\nimport type { JSONSchema, Tool } from '@tanstack/ai'\n\n/**\n * Responses API function tool format.\n * This is distinct from the Chat Completions API tool format.\n *\n * The Responses API uses a flatter structure:\n * { type: 'function', name: string, description?: string, parameters: object, strict?: boolean }\n *\n * vs. Chat Completions:\n * { type: 'function', function: { name, description, parameters }, strict?: boolean }\n */\nexport interface ResponsesFunctionTool {\n type: 'function'\n name: string\n description?: string | null\n parameters: Record<string, any> | null\n strict: boolean | null\n}\n\n/**\n * Converts a standard Tool to the Responses API FunctionTool format.\n *\n * Tool schemas are already converted to JSON Schema in the ai layer.\n * We apply OpenAI-compatible transformations for strict mode:\n * - All properties in required array\n * - Optional fields made nullable\n * - additionalProperties: false\n *\n * This enables strict mode for tools whose schemas fit OpenAI's strict subset.\n *\n * Schemas using keywords outside that subset (`oneOf`/`allOf`/`not`/`$ref`/\n * `$defs` — common with MCP servers like Notion) can't be coerced to a\n * strict-valid shape, and `strict: true` would make the Responses API reject\n * the ENTIRE request with a 400. Such tools are emitted with `strict: false`\n * (their schema passed through, only unsupported `format` keywords stripped) so\n * they stay callable.\n */\nexport function convertFunctionToolToResponsesFormat(\n tool: Tool,\n schemaConverter: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any> = makeStructuredOutputCompatible,\n): ResponsesFunctionTool {\n const inputSchema = (tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n }) as JSONSchema\n\n // Schema outside OpenAI's strict subset: send non-strict so the tool still\n // works instead of 400-ing the whole request.\n if (!isStrictModeCompatible(inputSchema)) {\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: stripUnsupportedFormats(inputSchema),\n strict: false,\n }\n }\n\n // Shallow-copy the converter's result before mutating — a subclass-supplied\n // schemaConverter has no contract requirement to return a fresh object;\n // mutating in place could corrupt the caller's tool definition.\n const jsonSchema = {\n ...schemaConverter(inputSchema, inputSchema.required || []),\n }\n jsonSchema.additionalProperties = false\n\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: jsonSchema,\n strict: true,\n }\n}\n\n/**\n * Converts an array of standard Tools to Responses API format.\n * The Responses API primarily supports function tools at the base level.\n */\nexport function convertToolsToResponsesFormat(\n tools: Array<Tool>,\n schemaConverter?: (\n schema: Record<string, any>,\n required: Array<string>,\n ) => Record<string, any>,\n): Array<ResponsesFunctionTool> {\n return tools.map((tool) =>\n convertFunctionToolToResponsesFormat(tool, schemaConverter),\n )\n}\n"],"names":[],"mappings":";AA2CO,SAAS,qCACd,MACA,kBAG2B,gCACJ;AACvB,QAAM,cAAe,KAAK,eAAe;AAAA,IACvC,MAAM;AAAA,IACN,YAAY,CAAA;AAAA,IACZ,UAAU,CAAA;AAAA,EAAC;AAKb,MAAI,CAAC,uBAAuB,WAAW,GAAG;AACxC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,YAAY,wBAAwB,WAAW;AAAA,MAC/C,QAAQ;AAAA,IAAA;AAAA,EAEZ;AAKA,QAAM,aAAa;AAAA,IACjB,GAAG,gBAAgB,aAAa,YAAY,YAAY,CAAA,CAAE;AAAA,EAAA;AAE5D,aAAW,uBAAuB;AAElC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,YAAY;AAAA,IACZ,QAAQ;AAAA,EAAA;AAEZ;AAMO,SAAS,8BACd,OACA,iBAI8B;AAC9B,SAAO,MAAM;AAAA,IAAI,CAAC,SAChB,qCAAqC,MAAM,eAAe;AAAA,EAAA;AAE9D;"}
@@ -13,5 +13,11 @@ export type FunctionTool = FunctionToolConfig;
13
13
  * - additionalProperties: false
14
14
  *
15
15
  * This enables strict mode for all tools automatically.
16
+ *
17
+ * Some tool schemas (e.g. MCP server tools that use `oneOf`, `$ref`, or
18
+ * `$defs`) cannot be expressed under OpenAI's strict Structured Outputs
19
+ * subset. For those we fall back to a non-strict tool definition — stripping
20
+ * only the formats OpenAI rejects — so the tool is still usable instead of
21
+ * failing the request with a 400 "Invalid schema" error.
16
22
  */
17
23
  export declare function convertFunctionToolToAdapterFormat(tool: Tool): FunctionToolConfig;
@@ -1,10 +1,19 @@
1
- import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
1
+ import { isStrictModeCompatible, stripUnsupportedFormats, makeStructuredOutputCompatible } from "../utils/schema-converter.js";
2
2
  function convertFunctionToolToAdapterFormat(tool) {
3
3
  const inputSchema = tool.inputSchema ?? {
4
4
  type: "object",
5
5
  properties: {},
6
6
  required: []
7
7
  };
8
+ if (!isStrictModeCompatible(inputSchema)) {
9
+ return {
10
+ type: "function",
11
+ name: tool.name,
12
+ description: tool.description,
13
+ parameters: stripUnsupportedFormats(inputSchema),
14
+ strict: false
15
+ };
16
+ }
8
17
  const jsonSchema = makeStructuredOutputCompatible(
9
18
  inputSchema,
10
19
  inputSchema.required || []
@@ -1 +1 @@
1
- {"version":3,"file":"function-tool.js","sources":["../../../src/tools/function-tool.ts"],"sourcesContent":["import { makeStructuredOutputCompatible } from '../utils/schema-converter'\nimport type { FunctionTool as FunctionToolConfig } from 'openai/resources/responses/responses'\nimport type { JSONSchema, Tool } from '@tanstack/ai'\n\nexport type { FunctionToolConfig }\n\n/** @deprecated Renamed to `FunctionToolConfig`. Will be removed in a future release. */\nexport type FunctionTool = FunctionToolConfig\n\n/**\n * Converts a standard Tool to OpenAI FunctionTool format.\n *\n * Tool schemas are already converted to JSON Schema in the ai layer.\n * We apply OpenAI-specific transformations for strict mode:\n * - All properties in required array\n * - Optional fields made nullable\n * - additionalProperties: false\n *\n * This enables strict mode for all tools automatically.\n */\nexport function convertFunctionToolToAdapterFormat(\n tool: Tool,\n): FunctionToolConfig {\n const inputSchema = (tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n }) as JSONSchema\n\n const jsonSchema = makeStructuredOutputCompatible(\n inputSchema,\n inputSchema.required || [],\n )\n\n jsonSchema.additionalProperties = false\n\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: jsonSchema,\n strict: true,\n } satisfies FunctionToolConfig\n}\n"],"names":[],"mappings":";AAoBO,SAAS,mCACd,MACoB;AACpB,QAAM,cAAe,KAAK,eAAe;AAAA,IACvC,MAAM;AAAA,IACN,YAAY,CAAA;AAAA,IACZ,UAAU,CAAA;AAAA,EAAC;AAGb,QAAM,aAAa;AAAA,IACjB;AAAA,IACA,YAAY,YAAY,CAAA;AAAA,EAAC;AAG3B,aAAW,uBAAuB;AAElC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,YAAY;AAAA,IACZ,QAAQ;AAAA,EAAA;AAEZ;"}
1
+ {"version":3,"file":"function-tool.js","sources":["../../../src/tools/function-tool.ts"],"sourcesContent":["import {\n isStrictModeCompatible,\n makeStructuredOutputCompatible,\n stripUnsupportedFormats,\n} from '../utils/schema-converter'\nimport type { FunctionTool as FunctionToolConfig } from 'openai/resources/responses/responses'\nimport type { JSONSchema, Tool } from '@tanstack/ai'\n\nexport type { FunctionToolConfig }\n\n/** @deprecated Renamed to `FunctionToolConfig`. Will be removed in a future release. */\nexport type FunctionTool = FunctionToolConfig\n\n/**\n * Converts a standard Tool to OpenAI FunctionTool format.\n *\n * Tool schemas are already converted to JSON Schema in the ai layer.\n * We apply OpenAI-specific transformations for strict mode:\n * - All properties in required array\n * - Optional fields made nullable\n * - additionalProperties: false\n *\n * This enables strict mode for all tools automatically.\n *\n * Some tool schemas (e.g. MCP server tools that use `oneOf`, `$ref`, or\n * `$defs`) cannot be expressed under OpenAI's strict Structured Outputs\n * subset. For those we fall back to a non-strict tool definition — stripping\n * only the formats OpenAI rejects — so the tool is still usable instead of\n * failing the request with a 400 \"Invalid schema\" error.\n */\nexport function convertFunctionToolToAdapterFormat(\n tool: Tool,\n): FunctionToolConfig {\n const inputSchema = (tool.inputSchema ?? {\n type: 'object',\n properties: {},\n required: [],\n }) as JSONSchema\n\n if (!isStrictModeCompatible(inputSchema)) {\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: stripUnsupportedFormats(inputSchema),\n strict: false,\n } satisfies FunctionToolConfig\n }\n\n const jsonSchema = makeStructuredOutputCompatible(\n inputSchema,\n inputSchema.required || [],\n )\n\n jsonSchema.additionalProperties = false\n\n return {\n type: 'function',\n name: tool.name,\n description: tool.description,\n parameters: jsonSchema,\n strict: true,\n } satisfies FunctionToolConfig\n}\n"],"names":[],"mappings":";AA8BO,SAAS,mCACd,MACoB;AACpB,QAAM,cAAe,KAAK,eAAe;AAAA,IACvC,MAAM;AAAA,IACN,YAAY,CAAA;AAAA,IACZ,UAAU,CAAA;AAAA,EAAC;AAGb,MAAI,CAAC,uBAAuB,WAAW,GAAG;AACxC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM,KAAK;AAAA,MACX,aAAa,KAAK;AAAA,MAClB,YAAY,wBAAwB,WAAW;AAAA,MAC/C,QAAQ;AAAA,IAAA;AAAA,EAEZ;AAEA,QAAM,aAAa;AAAA,IACjB;AAAA,IACA,YAAY,YAAY,CAAA;AAAA,EAAC;AAG3B,aAAW,uBAAuB;AAElC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,KAAK;AAAA,IACX,aAAa,KAAK;AAAA,IAClB,YAAY;AAAA,IACZ,QAAQ;AAAA,EAAA;AAEZ;"}
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Recursively drop JSON-Schema `format` keywords whose value isn't in OpenAI's
3
+ * strict-mode allowlist. Pure — returns a fresh tree and never mutates `node`,
4
+ * so the caller's original tool definition is left intact.
5
+ *
6
+ * A property *named* `format` always has a schema (object/boolean) value, never
7
+ * a bare string, so it is preserved and recursed into; only the `format`
8
+ * *keyword* (whose value is a string) is subject to removal.
9
+ */
10
+ export declare function stripUnsupportedFormats(node: any): any;
1
11
  /**
2
12
  * Transform a JSON schema to be compatible with OpenAI's structured output requirements.
3
13
  * OpenAI requires:
@@ -11,3 +21,14 @@
11
21
  * @returns Transformed schema compatible with OpenAI structured output
12
22
  */
13
23
  export declare function makeStructuredOutputCompatible(schema: Record<string, any>, originalRequired?: Array<string>): Record<string, any>;
24
+ /**
25
+ * Returns `false` when `schema` (anywhere in the tree) uses a JSON-Schema
26
+ * keyword outside OpenAI's strict Structured Outputs subset — i.e. it cannot be
27
+ * made strict-compatible and must be sent with `strict: false`.
28
+ *
29
+ * Conservative by design: keywords are matched as object keys, so a property
30
+ * literally named e.g. `oneOf` also trips it. That only costs that one tool its
31
+ * strict mode, which is strictly safer than a false "compatible" verdict that
32
+ * 400s the whole request.
33
+ */
34
+ export declare function isStrictModeCompatible(schema: unknown): boolean;
@@ -24,6 +24,28 @@ function stripUnsupportedFormats(node) {
24
24
  function makeStructuredOutputCompatible(schema, originalRequired) {
25
25
  return stripUnsupportedFormats(coerceStrictSchema(schema, originalRequired));
26
26
  }
27
+ const STRICT_UNSUPPORTED_KEYWORDS = [
28
+ "oneOf",
29
+ "allOf",
30
+ "not",
31
+ "$ref",
32
+ "$defs",
33
+ "definitions"
34
+ ];
35
+ function isStrictModeCompatible(schema) {
36
+ return !containsStrictUnsupportedKeyword(schema);
37
+ }
38
+ function containsStrictUnsupportedKeyword(node) {
39
+ if (Array.isArray(node)) {
40
+ return node.some(containsStrictUnsupportedKeyword);
41
+ }
42
+ if (node === null || typeof node !== "object") return false;
43
+ for (const [key, value] of Object.entries(node)) {
44
+ if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return true;
45
+ if (containsStrictUnsupportedKeyword(value)) return true;
46
+ }
47
+ return false;
48
+ }
27
49
  function coerceStrictSchema(schema, originalRequired) {
28
50
  const result = { ...schema };
29
51
  const required = originalRequired ?? (Array.isArray(result["required"]) ? result["required"] : []);
@@ -80,6 +102,8 @@ function coerceStrictSchema(schema, originalRequired) {
80
102
  return result;
81
103
  }
82
104
  export {
83
- makeStructuredOutputCompatible
105
+ isStrictModeCompatible,
106
+ makeStructuredOutputCompatible,
107
+ stripUnsupportedFormats
84
108
  };
85
109
  //# sourceMappingURL=schema-converter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-converter.js","sources":["../../../src/utils/schema-converter.ts"],"sourcesContent":["/**\n * String `format` values accepted by OpenAI's strict Structured Outputs subset.\n * Any other format (e.g. \"uri\", \"uri-reference\", \"regex\") causes the API to\n * reject the whole request with `400 ... '<format>' is not a valid format`.\n * MCP servers and hand-written tools routinely declare such formats, so we strip\n * the unsupported ones before sending. See:\n * https://platform.openai.com/docs/guides/structured-outputs#supported-properties\n */\nconst SUPPORTED_STRING_FORMATS = new Set([\n 'date-time',\n 'time',\n 'date',\n 'duration',\n 'email',\n 'hostname',\n 'ipv4',\n 'ipv6',\n 'uuid',\n])\n\n/**\n * Recursively drop JSON-Schema `format` keywords whose value isn't in OpenAI's\n * strict-mode allowlist. Pure — returns a fresh tree and never mutates `node`,\n * so the caller's original tool definition is left intact.\n *\n * A property *named* `format` always has a schema (object/boolean) value, never\n * a bare string, so it is preserved and recursed into; only the `format`\n * *keyword* (whose value is a string) is subject to removal.\n */\nfunction stripUnsupportedFormats(node: any): any {\n if (Array.isArray(node)) return node.map(stripUnsupportedFormats)\n if (node === null || typeof node !== 'object') return node\n\n const out: Record<string, any> = {}\n for (const [key, value] of Object.entries(node)) {\n if (\n key === 'format' &&\n typeof value === 'string' &&\n !SUPPORTED_STRING_FORMATS.has(value)\n ) {\n continue\n }\n out[key] = stripUnsupportedFormats(value)\n }\n return out\n}\n\n/**\n * Transform a JSON schema to be compatible with OpenAI's structured output requirements.\n * OpenAI requires:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for objects\n * - String `format` keywords must be from a fixed allowlist (others are stripped)\n *\n * @param schema - JSON schema to transform\n * @param originalRequired - Original required array (to know which fields were optional)\n * @returns Transformed schema compatible with OpenAI structured output\n */\nexport function makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): Record<string, any> {\n return stripUnsupportedFormats(coerceStrictSchema(schema, originalRequired))\n}\n\n/**\n * Strict-mode structural rewrite (required widening, nullability,\n * additionalProperties). Kept private so the public entry point can apply the\n * format-stripping pass exactly once over the fully-rewritten tree.\n */\nfunction coerceStrictSchema(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): Record<string, any> {\n const result = { ...schema }\n const required =\n originalRequired ??\n (Array.isArray(result['required']) ? result['required'] : [])\n\n if (result.type === 'object' && result.properties) {\n const properties = { ...result.properties }\n const allPropertyNames = Object.keys(properties)\n\n for (const propName of allPropertyNames) {\n let prop = properties[propName]\n const wasOptional = !required.includes(propName)\n\n // Step 1: Recurse into nested structures\n if (prop.type === 'object' && prop.properties) {\n prop = coerceStrictSchema(prop, prop.required || [])\n } else if (prop.type === 'array' && prop.items) {\n prop = {\n ...prop,\n items: coerceStrictSchema(prop.items, prop.items.required || []),\n }\n } else if (prop.anyOf) {\n prop = coerceStrictSchema(prop, prop.required || [])\n } else if (prop.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n // Step 2: Apply null-widening for optional properties (after recursion)\n if (wasOptional) {\n if (prop.anyOf) {\n // For anyOf, add a null variant if not already present\n if (!prop.anyOf.some((v: any) => v.type === 'null')) {\n prop = { ...prop, anyOf: [...prop.anyOf, { type: 'null' }] }\n }\n } else if (prop.type && !Array.isArray(prop.type)) {\n prop = { ...prop, type: [prop.type, 'null'] }\n } else if (Array.isArray(prop.type) && !prop.type.includes('null')) {\n prop = { ...prop, type: [...prop.type, 'null'] }\n }\n }\n\n properties[propName] = prop\n }\n\n result.properties = properties\n result.required = allPropertyNames\n result.additionalProperties = false\n }\n\n if (result.type === 'array' && result.items) {\n result.items = coerceStrictSchema(result.items, result.items.required || [])\n }\n\n if (result.anyOf && Array.isArray(result.anyOf)) {\n result.anyOf = result.anyOf.map((variant) =>\n coerceStrictSchema(variant, variant.required || []),\n )\n }\n\n if (result.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n return result\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema-converter.js","sources":["../../../src/utils/schema-converter.ts"],"sourcesContent":["/**\n * String `format` values accepted by OpenAI's strict Structured Outputs subset.\n * Any other format (e.g. \"uri\", \"uri-reference\", \"regex\") causes the API to\n * reject the whole request with `400 ... '<format>' is not a valid format`.\n * MCP servers and hand-written tools routinely declare such formats, so we strip\n * the unsupported ones before sending. See:\n * https://platform.openai.com/docs/guides/structured-outputs#supported-properties\n */\nconst SUPPORTED_STRING_FORMATS = new Set([\n 'date-time',\n 'time',\n 'date',\n 'duration',\n 'email',\n 'hostname',\n 'ipv4',\n 'ipv6',\n 'uuid',\n])\n\n/**\n * Recursively drop JSON-Schema `format` keywords whose value isn't in OpenAI's\n * strict-mode allowlist. Pure — returns a fresh tree and never mutates `node`,\n * so the caller's original tool definition is left intact.\n *\n * A property *named* `format` always has a schema (object/boolean) value, never\n * a bare string, so it is preserved and recursed into; only the `format`\n * *keyword* (whose value is a string) is subject to removal.\n */\nexport function stripUnsupportedFormats(node: any): any {\n if (Array.isArray(node)) return node.map(stripUnsupportedFormats)\n if (node === null || typeof node !== 'object') return node\n\n const out: Record<string, any> = {}\n for (const [key, value] of Object.entries(node)) {\n if (\n key === 'format' &&\n typeof value === 'string' &&\n !SUPPORTED_STRING_FORMATS.has(value)\n ) {\n continue\n }\n out[key] = stripUnsupportedFormats(value)\n }\n return out\n}\n\n/**\n * Transform a JSON schema to be compatible with OpenAI's structured output requirements.\n * OpenAI requires:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for objects\n * - String `format` keywords must be from a fixed allowlist (others are stripped)\n *\n * @param schema - JSON schema to transform\n * @param originalRequired - Original required array (to know which fields were optional)\n * @returns Transformed schema compatible with OpenAI structured output\n */\nexport function makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): Record<string, any> {\n return stripUnsupportedFormats(coerceStrictSchema(schema, originalRequired))\n}\n\n/**\n * JSON-Schema keywords outside OpenAI's strict Structured Outputs subset. A\n * schema using any of these can't be coerced into a strict-valid shape, and\n * sending it with `strict: true` makes the API reject the ENTIRE request\n * (e.g. `400 Invalid schema ... 'additionalProperties' is required to be ...`).\n * Tools with such schemas are emitted with `strict: false` instead (see the\n * tool converters) so they remain callable. MCP servers (e.g. Notion) routinely\n * emit these.\n *\n * - `oneOf` / `allOf` / `not` — combinator keywords strict mode rejects\n * - `$ref` / `$defs` / `definitions` — references and definition pools whose\n * object subschemas escape the `additionalProperties: false` normalization\n * strict mode requires\n */\nconst STRICT_UNSUPPORTED_KEYWORDS: ReadonlyArray<string> = [\n 'oneOf',\n 'allOf',\n 'not',\n '$ref',\n '$defs',\n 'definitions',\n]\n\n/**\n * Returns `false` when `schema` (anywhere in the tree) uses a JSON-Schema\n * keyword outside OpenAI's strict Structured Outputs subset — i.e. it cannot be\n * made strict-compatible and must be sent with `strict: false`.\n *\n * Conservative by design: keywords are matched as object keys, so a property\n * literally named e.g. `oneOf` also trips it. That only costs that one tool its\n * strict mode, which is strictly safer than a false \"compatible\" verdict that\n * 400s the whole request.\n */\nexport function isStrictModeCompatible(schema: unknown): boolean {\n return !containsStrictUnsupportedKeyword(schema)\n}\n\nfunction containsStrictUnsupportedKeyword(node: unknown): boolean {\n if (Array.isArray(node)) {\n return node.some(containsStrictUnsupportedKeyword)\n }\n if (node === null || typeof node !== 'object') return false\n for (const [key, value] of Object.entries(node)) {\n if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return true\n if (containsStrictUnsupportedKeyword(value)) return true\n }\n return false\n}\n\n/**\n * Strict-mode structural rewrite (required widening, nullability,\n * additionalProperties). Kept private so the public entry point can apply the\n * format-stripping pass exactly once over the fully-rewritten tree.\n */\nfunction coerceStrictSchema(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n): Record<string, any> {\n const result = { ...schema }\n const required =\n originalRequired ??\n (Array.isArray(result['required']) ? result['required'] : [])\n\n if (result.type === 'object' && result.properties) {\n const properties = { ...result.properties }\n const allPropertyNames = Object.keys(properties)\n\n for (const propName of allPropertyNames) {\n let prop = properties[propName]\n const wasOptional = !required.includes(propName)\n\n // Step 1: Recurse into nested structures\n if (prop.type === 'object' && prop.properties) {\n prop = coerceStrictSchema(prop, prop.required || [])\n } else if (prop.type === 'array' && prop.items) {\n prop = {\n ...prop,\n items: coerceStrictSchema(prop.items, prop.items.required || []),\n }\n } else if (prop.anyOf) {\n prop = coerceStrictSchema(prop, prop.required || [])\n } else if (prop.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n // Step 2: Apply null-widening for optional properties (after recursion)\n if (wasOptional) {\n if (prop.anyOf) {\n // For anyOf, add a null variant if not already present\n if (!prop.anyOf.some((v: any) => v.type === 'null')) {\n prop = { ...prop, anyOf: [...prop.anyOf, { type: 'null' }] }\n }\n } else if (prop.type && !Array.isArray(prop.type)) {\n prop = { ...prop, type: [prop.type, 'null'] }\n } else if (Array.isArray(prop.type) && !prop.type.includes('null')) {\n prop = { ...prop, type: [...prop.type, 'null'] }\n }\n }\n\n properties[propName] = prop\n }\n\n result.properties = properties\n result.required = allPropertyNames\n result.additionalProperties = false\n }\n\n if (result.type === 'array' && result.items) {\n result.items = coerceStrictSchema(result.items, result.items.required || [])\n }\n\n if (result.anyOf && Array.isArray(result.anyOf)) {\n result.anyOf = result.anyOf.map((variant) =>\n coerceStrictSchema(variant, variant.required || []),\n )\n }\n\n if (result.oneOf) {\n throw new Error(\n 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',\n )\n }\n\n return result\n}\n"],"names":[],"mappings":"AAQA,MAAM,+CAA+B,IAAI;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAWM,SAAS,wBAAwB,MAAgB;AACtD,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,IAAI,uBAAuB;AAChE,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AAEtD,QAAM,MAA2B,CAAA;AACjC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,QACE,QAAQ,YACR,OAAO,UAAU,YACjB,CAAC,yBAAyB,IAAI,KAAK,GACnC;AACA;AAAA,IACF;AACA,QAAI,GAAG,IAAI,wBAAwB,KAAK;AAAA,EAC1C;AACA,SAAO;AACT;AAcO,SAAS,+BACd,QACA,kBACqB;AACrB,SAAO,wBAAwB,mBAAmB,QAAQ,gBAAgB,CAAC;AAC7E;AAgBA,MAAM,8BAAqD;AAAA,EACzD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,uBAAuB,QAA0B;AAC/D,SAAO,CAAC,iCAAiC,MAAM;AACjD;AAEA,SAAS,iCAAiC,MAAwB;AAChE,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,WAAO,KAAK,KAAK,gCAAgC;AAAA,EACnD;AACA,MAAI,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AACtD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC/C,QAAI,4BAA4B,SAAS,GAAG,EAAG,QAAO;AACtD,QAAI,iCAAiC,KAAK,EAAG,QAAO;AAAA,EACtD;AACA,SAAO;AACT;AAOA,SAAS,mBACP,QACA,kBACqB;AACrB,QAAM,SAAS,EAAE,GAAG,OAAA;AACpB,QAAM,WACJ,qBACC,MAAM,QAAQ,OAAO,UAAU,CAAC,IAAI,OAAO,UAAU,IAAI,CAAA;AAE5D,MAAI,OAAO,SAAS,YAAY,OAAO,YAAY;AACjD,UAAM,aAAa,EAAE,GAAG,OAAO,WAAA;AAC/B,UAAM,mBAAmB,OAAO,KAAK,UAAU;AAE/C,eAAW,YAAY,kBAAkB;AACvC,UAAI,OAAO,WAAW,QAAQ;AAC9B,YAAM,cAAc,CAAC,SAAS,SAAS,QAAQ;AAG/C,UAAI,KAAK,SAAS,YAAY,KAAK,YAAY;AAC7C,eAAO,mBAAmB,MAAM,KAAK,YAAY,CAAA,CAAE;AAAA,MACrD,WAAW,KAAK,SAAS,WAAW,KAAK,OAAO;AAC9C,eAAO;AAAA,UACL,GAAG;AAAA,UACH,OAAO,mBAAmB,KAAK,OAAO,KAAK,MAAM,YAAY,CAAA,CAAE;AAAA,QAAA;AAAA,MAEnE,WAAW,KAAK,OAAO;AACrB,eAAO,mBAAmB,MAAM,KAAK,YAAY,CAAA,CAAE;AAAA,MACrD,WAAW,KAAK,OAAO;AACrB,cAAM,IAAI;AAAA,UACR;AAAA,QAAA;AAAA,MAEJ;AAGA,UAAI,aAAa;AACf,YAAI,KAAK,OAAO;AAEd,cAAI,CAAC,KAAK,MAAM,KAAK,CAAC,MAAW,EAAE,SAAS,MAAM,GAAG;AACnD,mBAAO,EAAE,GAAG,MAAM,OAAO,CAAC,GAAG,KAAK,OAAO,EAAE,MAAM,OAAA,CAAQ,EAAA;AAAA,UAC3D;AAAA,QACF,WAAW,KAAK,QAAQ,CAAC,MAAM,QAAQ,KAAK,IAAI,GAAG;AACjD,iBAAO,EAAE,GAAG,MAAM,MAAM,CAAC,KAAK,MAAM,MAAM,EAAA;AAAA,QAC5C,WAAW,MAAM,QAAQ,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,MAAM,GAAG;AAClE,iBAAO,EAAE,GAAG,MAAM,MAAM,CAAC,GAAG,KAAK,MAAM,MAAM,EAAA;AAAA,QAC/C;AAAA,MACF;AAEA,iBAAW,QAAQ,IAAI;AAAA,IACzB;AAEA,WAAO,aAAa;AACpB,WAAO,WAAW;AAClB,WAAO,uBAAuB;AAAA,EAChC;AAEA,MAAI,OAAO,SAAS,WAAW,OAAO,OAAO;AAC3C,WAAO,QAAQ,mBAAmB,OAAO,OAAO,OAAO,MAAM,YAAY,EAAE;AAAA,EAC7E;AAEA,MAAI,OAAO,SAAS,MAAM,QAAQ,OAAO,KAAK,GAAG;AAC/C,WAAO,QAAQ,OAAO,MAAM;AAAA,MAAI,CAAC,YAC/B,mBAAmB,SAAS,QAAQ,YAAY,CAAA,CAAE;AAAA,IAAA;AAAA,EAEtD;AAEA,MAAI,OAAO,OAAO;AAChB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/openai-base",
3
- "version": "0.8.4",
3
+ "version": "0.8.7",
4
4
  "description": "Shared OpenAI SDK base adapters for TanStack AI providers using Chat Completions and Responses APIs.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -47,13 +47,13 @@
47
47
  "@tanstack/ai-utils": "0.2.2"
48
48
  },
49
49
  "peerDependencies": {
50
- "@tanstack/ai": "^0.31.0"
50
+ "@tanstack/ai": "^0.32.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@vitest/coverage-v8": "4.0.14",
54
54
  "vite": "^7.3.3",
55
55
  "zod": "^4.2.0",
56
- "@tanstack/ai": "0.31.0"
56
+ "@tanstack/ai": "0.32.0"
57
57
  },
58
58
  "scripts": {
59
59
  "build": "vite build",
@@ -1,4 +1,8 @@
1
- import { makeStructuredOutputCompatible } from '../utils/schema-converter'
1
+ import {
2
+ isStrictModeCompatible,
3
+ makeStructuredOutputCompatible,
4
+ stripUnsupportedFormats,
5
+ } from '../utils/schema-converter'
2
6
  import type { ChatCompletionTool } from 'openai/resources/chat/completions/completions'
3
7
  import type { JSONSchema, Tool } from '@tanstack/ai'
4
8
 
@@ -22,7 +26,14 @@ export type ChatCompletionFunctionTool = Extract<
22
26
  * - Optional fields made nullable
23
27
  * - additionalProperties: false
24
28
  *
25
- * This enables strict mode for all tools automatically.
29
+ * This enables strict mode for tools whose schemas fit OpenAI's strict subset.
30
+ *
31
+ * Schemas using keywords outside that subset (`oneOf`/`allOf`/`not`/`$ref`/
32
+ * `$defs` — common with MCP servers like Notion) can't be coerced to a
33
+ * strict-valid shape, and `strict: true` would make the API reject the ENTIRE
34
+ * request with a 400. Such tools are emitted with `strict: false` (their schema
35
+ * passed through, only unsupported `format` keywords stripped) so they stay
36
+ * callable.
26
37
  */
27
38
  export function convertFunctionToolToChatCompletionsFormat(
28
39
  tool: Tool,
@@ -37,6 +48,20 @@ export function convertFunctionToolToChatCompletionsFormat(
37
48
  required: [],
38
49
  }) as JSONSchema
39
50
 
51
+ // Schema outside OpenAI's strict subset: send non-strict so the tool still
52
+ // works instead of 400-ing the whole request.
53
+ if (!isStrictModeCompatible(inputSchema)) {
54
+ return {
55
+ type: 'function',
56
+ function: {
57
+ name: tool.name,
58
+ description: tool.description,
59
+ parameters: stripUnsupportedFormats(inputSchema),
60
+ strict: false,
61
+ },
62
+ } satisfies ChatCompletionFunctionTool
63
+ }
64
+
40
65
  // Shallow-copy the converter's result before mutating: a subclass-supplied
41
66
  // schemaConverter has no contract requirement to return a fresh object,
42
67
  // and a passthrough `(s) => s` would otherwise have its caller's schema
@@ -1,4 +1,8 @@
1
- import { makeStructuredOutputCompatible } from '../utils/schema-converter'
1
+ import {
2
+ isStrictModeCompatible,
3
+ makeStructuredOutputCompatible,
4
+ stripUnsupportedFormats,
5
+ } from '../utils/schema-converter'
2
6
  import type { JSONSchema, Tool } from '@tanstack/ai'
3
7
 
4
8
  /**
@@ -28,7 +32,14 @@ export interface ResponsesFunctionTool {
28
32
  * - Optional fields made nullable
29
33
  * - additionalProperties: false
30
34
  *
31
- * This enables strict mode for all tools automatically.
35
+ * This enables strict mode for tools whose schemas fit OpenAI's strict subset.
36
+ *
37
+ * Schemas using keywords outside that subset (`oneOf`/`allOf`/`not`/`$ref`/
38
+ * `$defs` — common with MCP servers like Notion) can't be coerced to a
39
+ * strict-valid shape, and `strict: true` would make the Responses API reject
40
+ * the ENTIRE request with a 400. Such tools are emitted with `strict: false`
41
+ * (their schema passed through, only unsupported `format` keywords stripped) so
42
+ * they stay callable.
32
43
  */
33
44
  export function convertFunctionToolToResponsesFormat(
34
45
  tool: Tool,
@@ -43,6 +54,18 @@ export function convertFunctionToolToResponsesFormat(
43
54
  required: [],
44
55
  }) as JSONSchema
45
56
 
57
+ // Schema outside OpenAI's strict subset: send non-strict so the tool still
58
+ // works instead of 400-ing the whole request.
59
+ if (!isStrictModeCompatible(inputSchema)) {
60
+ return {
61
+ type: 'function',
62
+ name: tool.name,
63
+ description: tool.description,
64
+ parameters: stripUnsupportedFormats(inputSchema),
65
+ strict: false,
66
+ }
67
+ }
68
+
46
69
  // Shallow-copy the converter's result before mutating — a subclass-supplied
47
70
  // schemaConverter has no contract requirement to return a fresh object;
48
71
  // mutating in place could corrupt the caller's tool definition.
@@ -1,4 +1,8 @@
1
- import { makeStructuredOutputCompatible } from '../utils/schema-converter'
1
+ import {
2
+ isStrictModeCompatible,
3
+ makeStructuredOutputCompatible,
4
+ stripUnsupportedFormats,
5
+ } from '../utils/schema-converter'
2
6
  import type { FunctionTool as FunctionToolConfig } from 'openai/resources/responses/responses'
3
7
  import type { JSONSchema, Tool } from '@tanstack/ai'
4
8
 
@@ -17,6 +21,12 @@ export type FunctionTool = FunctionToolConfig
17
21
  * - additionalProperties: false
18
22
  *
19
23
  * 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.
20
30
  */
21
31
  export function convertFunctionToolToAdapterFormat(
22
32
  tool: Tool,
@@ -27,6 +37,16 @@ export function convertFunctionToolToAdapterFormat(
27
37
  required: [],
28
38
  }) as JSONSchema
29
39
 
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
+
30
50
  const jsonSchema = makeStructuredOutputCompatible(
31
51
  inputSchema,
32
52
  inputSchema.required || [],
@@ -27,7 +27,7 @@ const SUPPORTED_STRING_FORMATS = new Set([
27
27
  * a bare string, so it is preserved and recursed into; only the `format`
28
28
  * *keyword* (whose value is a string) is subject to removal.
29
29
  */
30
- function stripUnsupportedFormats(node: any): any {
30
+ export function stripUnsupportedFormats(node: any): any {
31
31
  if (Array.isArray(node)) return node.map(stripUnsupportedFormats)
32
32
  if (node === null || typeof node !== 'object') return node
33
33
 
@@ -64,6 +64,55 @@ export function makeStructuredOutputCompatible(
64
64
  return stripUnsupportedFormats(coerceStrictSchema(schema, originalRequired))
65
65
  }
66
66
 
67
+ /**
68
+ * JSON-Schema keywords outside OpenAI's strict Structured Outputs subset. A
69
+ * schema using any of these can't be coerced into a strict-valid shape, and
70
+ * sending it with `strict: true` makes the API reject the ENTIRE request
71
+ * (e.g. `400 Invalid schema ... 'additionalProperties' is required to be ...`).
72
+ * Tools with such schemas are emitted with `strict: false` instead (see the
73
+ * tool converters) so they remain callable. MCP servers (e.g. Notion) routinely
74
+ * emit these.
75
+ *
76
+ * - `oneOf` / `allOf` / `not` — combinator keywords strict mode rejects
77
+ * - `$ref` / `$defs` / `definitions` — references and definition pools whose
78
+ * object subschemas escape the `additionalProperties: false` normalization
79
+ * strict mode requires
80
+ */
81
+ const STRICT_UNSUPPORTED_KEYWORDS: ReadonlyArray<string> = [
82
+ 'oneOf',
83
+ 'allOf',
84
+ 'not',
85
+ '$ref',
86
+ '$defs',
87
+ 'definitions',
88
+ ]
89
+
90
+ /**
91
+ * Returns `false` when `schema` (anywhere in the tree) uses a JSON-Schema
92
+ * keyword outside OpenAI's strict Structured Outputs subset — i.e. it cannot be
93
+ * made strict-compatible and must be sent with `strict: false`.
94
+ *
95
+ * Conservative by design: keywords are matched as object keys, so a property
96
+ * literally named e.g. `oneOf` also trips it. That only costs that one tool its
97
+ * strict mode, which is strictly safer than a false "compatible" verdict that
98
+ * 400s the whole request.
99
+ */
100
+ export function isStrictModeCompatible(schema: unknown): boolean {
101
+ return !containsStrictUnsupportedKeyword(schema)
102
+ }
103
+
104
+ function containsStrictUnsupportedKeyword(node: unknown): boolean {
105
+ if (Array.isArray(node)) {
106
+ return node.some(containsStrictUnsupportedKeyword)
107
+ }
108
+ if (node === null || typeof node !== 'object') return false
109
+ for (const [key, value] of Object.entries(node)) {
110
+ if (STRICT_UNSUPPORTED_KEYWORDS.includes(key)) return true
111
+ if (containsStrictUnsupportedKeyword(value)) return true
112
+ }
113
+ return false
114
+ }
115
+
67
116
  /**
68
117
  * Strict-mode structural rewrite (required widening, nullability,
69
118
  * additionalProperties). Kept private so the public entry point can apply the