@tanstack/openai-base 0.1.0

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.
Files changed (110) hide show
  1. package/dist/esm/adapters/chat-completions-text.d.ts +76 -0
  2. package/dist/esm/adapters/chat-completions-text.js +411 -0
  3. package/dist/esm/adapters/chat-completions-text.js.map +1 -0
  4. package/dist/esm/adapters/chat-completions-tool-converter.d.ts +24 -0
  5. package/dist/esm/adapters/chat-completions-tool-converter.js +29 -0
  6. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -0
  7. package/dist/esm/adapters/image.d.ts +32 -0
  8. package/dist/esm/adapters/image.js +69 -0
  9. package/dist/esm/adapters/image.js.map +1 -0
  10. package/dist/esm/adapters/responses-text.d.ts +115 -0
  11. package/dist/esm/adapters/responses-text.js +635 -0
  12. package/dist/esm/adapters/responses-text.js.map +1 -0
  13. package/dist/esm/adapters/responses-tool-converter.d.ts +35 -0
  14. package/dist/esm/adapters/responses-tool-converter.js +27 -0
  15. package/dist/esm/adapters/responses-tool-converter.js.map +1 -0
  16. package/dist/esm/adapters/summarize.d.ts +28 -0
  17. package/dist/esm/adapters/summarize.js +74 -0
  18. package/dist/esm/adapters/summarize.js.map +1 -0
  19. package/dist/esm/adapters/transcription.d.ts +39 -0
  20. package/dist/esm/adapters/transcription.js +139 -0
  21. package/dist/esm/adapters/transcription.js.map +1 -0
  22. package/dist/esm/adapters/tts.d.ts +26 -0
  23. package/dist/esm/adapters/tts.js +65 -0
  24. package/dist/esm/adapters/tts.js.map +1 -0
  25. package/dist/esm/adapters/video.d.ts +48 -0
  26. package/dist/esm/adapters/video.js +192 -0
  27. package/dist/esm/adapters/video.js.map +1 -0
  28. package/dist/esm/index.d.ts +15 -0
  29. package/dist/esm/index.js +65 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/tools/apply-patch-tool.d.ts +11 -0
  32. package/dist/esm/tools/apply-patch-tool.js +17 -0
  33. package/dist/esm/tools/apply-patch-tool.js.map +1 -0
  34. package/dist/esm/tools/code-interpreter-tool.d.ts +11 -0
  35. package/dist/esm/tools/code-interpreter-tool.js +22 -0
  36. package/dist/esm/tools/code-interpreter-tool.js.map +1 -0
  37. package/dist/esm/tools/computer-use-tool.d.ts +11 -0
  38. package/dist/esm/tools/computer-use-tool.js +23 -0
  39. package/dist/esm/tools/computer-use-tool.js.map +1 -0
  40. package/dist/esm/tools/custom-tool.d.ts +11 -0
  41. package/dist/esm/tools/custom-tool.js +23 -0
  42. package/dist/esm/tools/custom-tool.js.map +1 -0
  43. package/dist/esm/tools/file-search-tool.d.ts +11 -0
  44. package/dist/esm/tools/file-search-tool.js +30 -0
  45. package/dist/esm/tools/file-search-tool.js.map +1 -0
  46. package/dist/esm/tools/function-tool.d.ts +15 -0
  47. package/dist/esm/tools/function-tool.js +24 -0
  48. package/dist/esm/tools/function-tool.js.map +1 -0
  49. package/dist/esm/tools/image-generation-tool.d.ts +11 -0
  50. package/dist/esm/tools/image-generation-tool.js +27 -0
  51. package/dist/esm/tools/image-generation-tool.js.map +1 -0
  52. package/dist/esm/tools/index.d.ts +27 -0
  53. package/dist/esm/tools/local-shell-tool.d.ts +11 -0
  54. package/dist/esm/tools/local-shell-tool.js +17 -0
  55. package/dist/esm/tools/local-shell-tool.js.map +1 -0
  56. package/dist/esm/tools/mcp-tool.d.ts +12 -0
  57. package/dist/esm/tools/mcp-tool.js +31 -0
  58. package/dist/esm/tools/mcp-tool.js.map +1 -0
  59. package/dist/esm/tools/shell-tool.d.ts +11 -0
  60. package/dist/esm/tools/shell-tool.js +17 -0
  61. package/dist/esm/tools/shell-tool.js.map +1 -0
  62. package/dist/esm/tools/tool-choice.d.ts +17 -0
  63. package/dist/esm/tools/tool-converter.d.ts +6 -0
  64. package/dist/esm/tools/tool-converter.js +61 -0
  65. package/dist/esm/tools/tool-converter.js.map +1 -0
  66. package/dist/esm/tools/web-search-preview-tool.d.ts +11 -0
  67. package/dist/esm/tools/web-search-preview-tool.js +20 -0
  68. package/dist/esm/tools/web-search-preview-tool.js.map +1 -0
  69. package/dist/esm/tools/web-search-tool.d.ts +11 -0
  70. package/dist/esm/tools/web-search-tool.js +16 -0
  71. package/dist/esm/tools/web-search-tool.js.map +1 -0
  72. package/dist/esm/types/config.d.ts +4 -0
  73. package/dist/esm/types/message-metadata.d.ts +19 -0
  74. package/dist/esm/types/provider-options.d.ts +40 -0
  75. package/dist/esm/utils/client.d.ts +3 -0
  76. package/dist/esm/utils/client.js +8 -0
  77. package/dist/esm/utils/client.js.map +1 -0
  78. package/dist/esm/utils/schema-converter.d.ts +12 -0
  79. package/dist/esm/utils/schema-converter.js +65 -0
  80. package/dist/esm/utils/schema-converter.js.map +1 -0
  81. package/package.json +57 -0
  82. package/src/adapters/chat-completions-text.ts +817 -0
  83. package/src/adapters/chat-completions-tool-converter.ts +70 -0
  84. package/src/adapters/image.ts +158 -0
  85. package/src/adapters/responses-text.ts +1147 -0
  86. package/src/adapters/responses-tool-converter.ts +77 -0
  87. package/src/adapters/summarize.ts +174 -0
  88. package/src/adapters/transcription.ts +194 -0
  89. package/src/adapters/tts.ts +124 -0
  90. package/src/adapters/video.ts +385 -0
  91. package/src/index.ts +24 -0
  92. package/src/tools/apply-patch-tool.ts +32 -0
  93. package/src/tools/code-interpreter-tool.ts +39 -0
  94. package/src/tools/computer-use-tool.ts +38 -0
  95. package/src/tools/custom-tool.ts +33 -0
  96. package/src/tools/file-search-tool.ts +51 -0
  97. package/src/tools/function-tool.ts +44 -0
  98. package/src/tools/image-generation-tool.ts +51 -0
  99. package/src/tools/index.ts +41 -0
  100. package/src/tools/local-shell-tool.ts +32 -0
  101. package/src/tools/mcp-tool.ts +47 -0
  102. package/src/tools/shell-tool.ts +30 -0
  103. package/src/tools/tool-choice.ts +31 -0
  104. package/src/tools/tool-converter.ts +68 -0
  105. package/src/tools/web-search-preview-tool.ts +39 -0
  106. package/src/tools/web-search-tool.ts +38 -0
  107. package/src/types/config.ts +5 -0
  108. package/src/utils/client.ts +8 -0
  109. package/src/utils/request-options.ts +16 -0
  110. package/src/utils/schema-converter.ts +89 -0
@@ -0,0 +1,51 @@
1
+ import type OpenAI from 'openai'
2
+ import type { Tool } from '@tanstack/ai'
3
+
4
+ export type ImageGenerationToolConfig = OpenAI.Responses.Tool.ImageGeneration
5
+
6
+ /** @deprecated Renamed to `ImageGenerationToolConfig`. Will be removed in a future release. */
7
+ export type ImageGenerationTool = ImageGenerationToolConfig
8
+
9
+ const validatePartialImages = (value: number | undefined) => {
10
+ if (value !== undefined && (value < 0 || value > 3)) {
11
+ throw new Error('partial_images must be between 0 and 3')
12
+ }
13
+ }
14
+
15
+ /**
16
+ * Converts a standard Tool to OpenAI ImageGenerationTool format. Spread
17
+ * `metadata` first, then force `type: 'image_generation'` last — otherwise a
18
+ * `metadata.type` snuck in by a hand-built tool would shadow the literal and
19
+ * the dispatcher (which routed by `tool.name`) would emit a tool whose
20
+ * runtime `type` doesn't match `image_generation`.
21
+ */
22
+ export function convertImageGenerationToolToAdapterFormat(
23
+ tool: Tool,
24
+ ): ImageGenerationToolConfig {
25
+ const metadata = tool.metadata as Omit<ImageGenerationToolConfig, 'type'>
26
+ return {
27
+ ...metadata,
28
+ type: 'image_generation',
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Creates a standard Tool from ImageGenerationTool parameters.
34
+ *
35
+ * Base (non-branded) factory. Providers that need branded return types should
36
+ * re-wrap this in their own package.
37
+ */
38
+ export function imageGenerationTool(
39
+ toolData: Omit<ImageGenerationToolConfig, 'type'>,
40
+ ): Tool {
41
+ validatePartialImages(toolData.partial_images)
42
+ return {
43
+ name: 'image_generation',
44
+ description: 'Generate images based on text descriptions',
45
+ metadata: {
46
+ ...toolData,
47
+ },
48
+ }
49
+ }
50
+
51
+ export { validatePartialImages }
@@ -0,0 +1,41 @@
1
+ import type { ApplyPatchToolConfig } from './apply-patch-tool'
2
+ import type { CodeInterpreterToolConfig } from './code-interpreter-tool'
3
+ import type { ComputerUseToolConfig } from './computer-use-tool'
4
+ import type { CustomToolConfig } from './custom-tool'
5
+ import type { FileSearchToolConfig } from './file-search-tool'
6
+ import type { FunctionToolConfig } from './function-tool'
7
+ import type { ImageGenerationToolConfig } from './image-generation-tool'
8
+ import type { LocalShellToolConfig } from './local-shell-tool'
9
+ import type { MCPToolConfig } from './mcp-tool'
10
+ import type { ShellToolConfig } from './shell-tool'
11
+ import type { WebSearchPreviewToolConfig } from './web-search-preview-tool'
12
+ import type { WebSearchToolConfig } from './web-search-tool'
13
+
14
+ export type OpenAITool =
15
+ | ApplyPatchToolConfig
16
+ | CodeInterpreterToolConfig
17
+ | ComputerUseToolConfig
18
+ | CustomToolConfig
19
+ | FileSearchToolConfig
20
+ | FunctionToolConfig
21
+ | ImageGenerationToolConfig
22
+ | LocalShellToolConfig
23
+ | MCPToolConfig
24
+ | ShellToolConfig
25
+ | WebSearchPreviewToolConfig
26
+ | WebSearchToolConfig
27
+
28
+ export * from './apply-patch-tool'
29
+ export * from './code-interpreter-tool'
30
+ export * from './computer-use-tool'
31
+ export * from './custom-tool'
32
+ export * from './file-search-tool'
33
+ export * from './function-tool'
34
+ export * from './image-generation-tool'
35
+ export * from './local-shell-tool'
36
+ export * from './mcp-tool'
37
+ export * from './shell-tool'
38
+ export * from './tool-choice'
39
+ export * from './tool-converter'
40
+ export * from './web-search-preview-tool'
41
+ export * from './web-search-tool'
@@ -0,0 +1,32 @@
1
+ import type OpenAI from 'openai'
2
+ import type { Tool } from '@tanstack/ai'
3
+
4
+ export type LocalShellToolConfig = OpenAI.Responses.Tool.LocalShell
5
+
6
+ /** @deprecated Renamed to `LocalShellToolConfig`. Will be removed in a future release. */
7
+ export type LocalShellTool = LocalShellToolConfig
8
+
9
+ /**
10
+ * Converts a standard Tool to OpenAI LocalShellTool format
11
+ */
12
+ export function convertLocalShellToolToAdapterFormat(
13
+ _tool: Tool,
14
+ ): LocalShellToolConfig {
15
+ return {
16
+ type: 'local_shell',
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Creates a standard Tool from LocalShellTool parameters.
22
+ *
23
+ * Base (non-branded) factory. Providers that need branded return types should
24
+ * re-wrap this in their own package.
25
+ */
26
+ export function localShellTool(): Tool {
27
+ return {
28
+ name: 'local_shell',
29
+ description: 'Execute local shell commands',
30
+ metadata: {},
31
+ }
32
+ }
@@ -0,0 +1,47 @@
1
+ import type OpenAI from 'openai'
2
+ import type { Tool } from '@tanstack/ai'
3
+
4
+ export type MCPToolConfig = OpenAI.Responses.Tool.Mcp
5
+
6
+ /** @deprecated Renamed to `MCPToolConfig`. Will be removed in a future release. */
7
+ export type MCPTool = MCPToolConfig
8
+
9
+ export function validateMCPtool(tool: MCPToolConfig) {
10
+ if (!tool.server_url && !tool.connector_id) {
11
+ throw new Error('Either server_url or connector_id must be provided.')
12
+ }
13
+ if (tool.connector_id && tool.server_url) {
14
+ throw new Error('Only one of server_url or connector_id can be provided.')
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Converts a standard Tool to OpenAI MCPTool format
20
+ */
21
+ export function convertMCPToolToAdapterFormat(tool: Tool): MCPToolConfig {
22
+ const metadata = tool.metadata as Omit<MCPToolConfig, 'type'>
23
+
24
+ const mcpTool: MCPToolConfig = {
25
+ ...metadata,
26
+ type: 'mcp',
27
+ }
28
+
29
+ validateMCPtool(mcpTool)
30
+ return mcpTool
31
+ }
32
+
33
+ /**
34
+ * Creates a standard Tool from MCPTool parameters.
35
+ *
36
+ * Base (non-branded) factory. Providers that need branded return types should
37
+ * re-wrap this in their own package.
38
+ */
39
+ export function mcpTool(toolData: Omit<MCPToolConfig, 'type'>): Tool {
40
+ validateMCPtool({ ...toolData, type: 'mcp' })
41
+
42
+ return {
43
+ name: 'mcp',
44
+ description: toolData.server_description || '',
45
+ metadata: toolData,
46
+ }
47
+ }
@@ -0,0 +1,30 @@
1
+ import type OpenAI from 'openai'
2
+ import type { Tool } from '@tanstack/ai'
3
+
4
+ export type ShellToolConfig = OpenAI.Responses.FunctionShellTool
5
+
6
+ /** @deprecated Renamed to `ShellToolConfig`. Will be removed in a future release. */
7
+ export type ShellTool = ShellToolConfig
8
+
9
+ /**
10
+ * Converts a standard Tool to OpenAI ShellTool format
11
+ */
12
+ export function convertShellToolToAdapterFormat(_tool: Tool): ShellToolConfig {
13
+ return {
14
+ type: 'shell',
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Creates a standard Tool from ShellTool parameters.
20
+ *
21
+ * Base (non-branded) factory. Providers that need branded return types should
22
+ * re-wrap this in their own package.
23
+ */
24
+ export function shellTool(): Tool {
25
+ return {
26
+ name: 'shell',
27
+ description: 'Execute shell commands',
28
+ metadata: {},
29
+ }
30
+ }
@@ -0,0 +1,31 @@
1
+ interface MCPToolChoice {
2
+ type: 'mcp'
3
+ server_label: string
4
+ }
5
+
6
+ interface FunctionToolChoice {
7
+ type: 'function'
8
+ name: string
9
+ }
10
+
11
+ interface CustomToolChoice {
12
+ type: 'custom'
13
+ name: string
14
+ }
15
+
16
+ interface HostedToolChoice {
17
+ type:
18
+ | 'file_search'
19
+ | 'web_search_preview'
20
+ | 'computer_use_preview'
21
+ | 'code_interpreter'
22
+ | 'image_generation'
23
+ | 'shell'
24
+ | 'apply_patch'
25
+ }
26
+
27
+ export type ToolChoice =
28
+ | MCPToolChoice
29
+ | FunctionToolChoice
30
+ | CustomToolChoice
31
+ | HostedToolChoice
@@ -0,0 +1,68 @@
1
+ import { convertApplyPatchToolToAdapterFormat } from './apply-patch-tool'
2
+ import { convertCodeInterpreterToolToAdapterFormat } from './code-interpreter-tool'
3
+ import { convertComputerUseToolToAdapterFormat } from './computer-use-tool'
4
+ import { convertCustomToolToAdapterFormat } from './custom-tool'
5
+ import { convertFileSearchToolToAdapterFormat } from './file-search-tool'
6
+ import { convertFunctionToolToAdapterFormat } from './function-tool'
7
+ import { convertImageGenerationToolToAdapterFormat } from './image-generation-tool'
8
+ import { convertLocalShellToolToAdapterFormat } from './local-shell-tool'
9
+ import { convertMCPToolToAdapterFormat } from './mcp-tool'
10
+ import { convertShellToolToAdapterFormat } from './shell-tool'
11
+ import { convertWebSearchPreviewToolToAdapterFormat } from './web-search-preview-tool'
12
+ import { convertWebSearchToolToAdapterFormat } from './web-search-tool'
13
+ import type { OpenAITool } from './index'
14
+ import type { Tool } from '@tanstack/ai'
15
+
16
+ const SPECIAL_TOOL_NAMES = new Set([
17
+ 'apply_patch',
18
+ 'code_interpreter',
19
+ 'computer_use_preview',
20
+ 'file_search',
21
+ 'image_generation',
22
+ 'local_shell',
23
+ 'mcp',
24
+ 'shell',
25
+ 'web_search_preview',
26
+ 'web_search',
27
+ 'custom',
28
+ ])
29
+
30
+ /**
31
+ * Converts an array of standard Tools to OpenAI-specific format
32
+ */
33
+ export function convertToolsToProviderFormat(
34
+ tools: Array<Tool>,
35
+ ): Array<OpenAITool> {
36
+ return tools.map((tool) => {
37
+ const toolName = tool.name
38
+
39
+ if (SPECIAL_TOOL_NAMES.has(toolName)) {
40
+ switch (toolName) {
41
+ case 'apply_patch':
42
+ return convertApplyPatchToolToAdapterFormat(tool)
43
+ case 'code_interpreter':
44
+ return convertCodeInterpreterToolToAdapterFormat(tool)
45
+ case 'computer_use_preview':
46
+ return convertComputerUseToolToAdapterFormat(tool)
47
+ case 'file_search':
48
+ return convertFileSearchToolToAdapterFormat(tool)
49
+ case 'image_generation':
50
+ return convertImageGenerationToolToAdapterFormat(tool)
51
+ case 'local_shell':
52
+ return convertLocalShellToolToAdapterFormat(tool)
53
+ case 'mcp':
54
+ return convertMCPToolToAdapterFormat(tool)
55
+ case 'shell':
56
+ return convertShellToolToAdapterFormat(tool)
57
+ case 'web_search_preview':
58
+ return convertWebSearchPreviewToolToAdapterFormat(tool)
59
+ case 'web_search':
60
+ return convertWebSearchToolToAdapterFormat(tool)
61
+ case 'custom':
62
+ return convertCustomToolToAdapterFormat(tool)
63
+ }
64
+ }
65
+
66
+ return convertFunctionToolToAdapterFormat(tool)
67
+ })
68
+ }
@@ -0,0 +1,39 @@
1
+ import type OpenAI from 'openai'
2
+ import type { Tool } from '@tanstack/ai'
3
+
4
+ export type WebSearchPreviewToolConfig = OpenAI.Responses.WebSearchPreviewTool
5
+
6
+ /** @deprecated Renamed to `WebSearchPreviewToolConfig`. Will be removed in a future release. */
7
+ export type WebSearchPreviewTool = WebSearchPreviewToolConfig
8
+
9
+ /**
10
+ * Converts a standard Tool to OpenAI WebSearchPreviewTool format. Force the
11
+ * literal `type: 'web_search_preview'` instead of trusting `metadata.type`,
12
+ * since a hand-authored tool with a missing or wrong `type` would emit a
13
+ * malformed payload while the dispatcher already routed by `tool.name`.
14
+ */
15
+ export function convertWebSearchPreviewToolToAdapterFormat(
16
+ tool: Tool,
17
+ ): WebSearchPreviewToolConfig {
18
+ const metadata = tool.metadata as Omit<WebSearchPreviewToolConfig, 'type'>
19
+ return {
20
+ ...metadata,
21
+ type: 'web_search_preview',
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Creates a standard Tool from WebSearchPreviewTool parameters.
27
+ *
28
+ * Base (non-branded) factory. Providers that need branded return types should
29
+ * re-wrap this in their own package.
30
+ */
31
+ export function webSearchPreviewTool(
32
+ toolData: WebSearchPreviewToolConfig,
33
+ ): Tool {
34
+ return {
35
+ name: 'web_search_preview',
36
+ description: 'Search the web (preview version)',
37
+ metadata: toolData,
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import type OpenAI from 'openai'
2
+ import type { Tool } from '@tanstack/ai'
3
+
4
+ export type WebSearchToolConfig = OpenAI.Responses.WebSearchTool
5
+
6
+ /** @deprecated Renamed to `WebSearchToolConfig`. Will be removed in a future release. */
7
+ export type WebSearchTool = WebSearchToolConfig
8
+
9
+ /**
10
+ * Converts a standard Tool to OpenAI WebSearchTool format. Spread `metadata`
11
+ * first, then force `type: 'web_search'` last to keep the runtime `type`
12
+ * matching the discriminator the dispatcher routed by — otherwise a tool
13
+ * authored by hand with a different `metadata.type` would emit a malformed
14
+ * payload.
15
+ */
16
+ export function convertWebSearchToolToAdapterFormat(
17
+ tool: Tool,
18
+ ): WebSearchToolConfig {
19
+ const metadata = tool.metadata as Omit<WebSearchToolConfig, 'type'>
20
+ return {
21
+ ...metadata,
22
+ type: 'web_search',
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Creates a standard Tool from WebSearchTool parameters.
28
+ *
29
+ * Base (non-branded) factory. Providers that need branded return types should
30
+ * re-wrap this in their own package.
31
+ */
32
+ export function webSearchTool(toolData: WebSearchToolConfig): Tool {
33
+ return {
34
+ name: 'web_search',
35
+ description: 'Search the web',
36
+ metadata: toolData,
37
+ }
38
+ }
@@ -0,0 +1,5 @@
1
+ import type { ClientOptions } from 'openai'
2
+
3
+ export interface OpenAICompatibleClientConfig extends ClientOptions {
4
+ apiKey: string
5
+ }
@@ -0,0 +1,8 @@
1
+ import OpenAI from 'openai'
2
+ import type { OpenAICompatibleClientConfig } from '../types/config'
3
+
4
+ export function createOpenAICompatibleClient(
5
+ config: OpenAICompatibleClientConfig,
6
+ ): OpenAI {
7
+ return new OpenAI(config)
8
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Extract `headers` and `signal` from a `Request | RequestInit` for the OpenAI
3
+ * SDK's per-call `RequestOptions`. `Request` exposes `headers` as a `Headers`
4
+ * instance (HeadersInit-compatible) while `RequestInit` exposes `HeadersInit`
5
+ * directly — this helper accepts either shape so callers don't need to cast.
6
+ *
7
+ * Always returns an object (possibly empty) rather than `undefined` so test
8
+ * assertions that match the second argument shape via `expect.anything()` /
9
+ * `expect.objectContaining()` keep working when no request override was set.
10
+ */
11
+ export function extractRequestOptions(
12
+ request: Request | RequestInit | undefined,
13
+ ): { headers?: HeadersInit; signal?: AbortSignal | null } {
14
+ if (!request) return {}
15
+ return { headers: request.headers, signal: request.signal ?? undefined }
16
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Transform a JSON schema to be compatible with OpenAI's structured output requirements.
3
+ * OpenAI requires:
4
+ * - All properties must be in the `required` array
5
+ * - Optional fields should have null added to their type union
6
+ * - additionalProperties must be false for objects
7
+ *
8
+ * @param schema - JSON schema to transform
9
+ * @param originalRequired - Original required array (to know which fields were optional)
10
+ * @returns Transformed schema compatible with OpenAI structured output
11
+ */
12
+ export function makeStructuredOutputCompatible(
13
+ schema: Record<string, any>,
14
+ originalRequired?: Array<string>,
15
+ ): Record<string, any> {
16
+ const result = { ...schema }
17
+ const required =
18
+ originalRequired ?? (Array.isArray(result.required) ? result.required : [])
19
+
20
+ if (result.type === 'object' && result.properties) {
21
+ const properties = { ...result.properties }
22
+ const allPropertyNames = Object.keys(properties)
23
+
24
+ for (const propName of allPropertyNames) {
25
+ let prop = properties[propName]
26
+ const wasOptional = !required.includes(propName)
27
+
28
+ // Step 1: Recurse into nested structures
29
+ if (prop.type === 'object' && prop.properties) {
30
+ prop = makeStructuredOutputCompatible(prop, prop.required || [])
31
+ } else if (prop.type === 'array' && prop.items) {
32
+ prop = {
33
+ ...prop,
34
+ items: makeStructuredOutputCompatible(
35
+ prop.items,
36
+ prop.items.required || [],
37
+ ),
38
+ }
39
+ } else if (prop.anyOf) {
40
+ prop = makeStructuredOutputCompatible(prop, prop.required || [])
41
+ } else if (prop.oneOf) {
42
+ throw new Error(
43
+ 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',
44
+ )
45
+ }
46
+
47
+ // Step 2: Apply null-widening for optional properties (after recursion)
48
+ if (wasOptional) {
49
+ if (prop.anyOf) {
50
+ // For anyOf, add a null variant if not already present
51
+ if (!prop.anyOf.some((v: any) => v.type === 'null')) {
52
+ prop = { ...prop, anyOf: [...prop.anyOf, { type: 'null' }] }
53
+ }
54
+ } else if (prop.type && !Array.isArray(prop.type)) {
55
+ prop = { ...prop, type: [prop.type, 'null'] }
56
+ } else if (Array.isArray(prop.type) && !prop.type.includes('null')) {
57
+ prop = { ...prop, type: [...prop.type, 'null'] }
58
+ }
59
+ }
60
+
61
+ properties[propName] = prop
62
+ }
63
+
64
+ result.properties = properties
65
+ result.required = allPropertyNames
66
+ result.additionalProperties = false
67
+ }
68
+
69
+ if (result.type === 'array' && result.items) {
70
+ result.items = makeStructuredOutputCompatible(
71
+ result.items,
72
+ result.items.required || [],
73
+ )
74
+ }
75
+
76
+ if (result.anyOf && Array.isArray(result.anyOf)) {
77
+ result.anyOf = result.anyOf.map((variant) =>
78
+ makeStructuredOutputCompatible(variant, variant.required || []),
79
+ )
80
+ }
81
+
82
+ if (result.oneOf) {
83
+ throw new Error(
84
+ 'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',
85
+ )
86
+ }
87
+
88
+ return result
89
+ }