@tanstack/openai-base 0.9.14 → 0.10.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 (53) hide show
  1. package/dist/esm/adapters/chat-completions-text.js +2 -1
  2. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  3. package/dist/esm/adapters/responses-text.d.ts +1 -1
  4. package/dist/esm/adapters/responses-text.js +26 -3
  5. package/dist/esm/adapters/responses-text.js.map +1 -1
  6. package/dist/esm/index.js +1 -0
  7. package/dist/esm/tools/apply-patch-tool.js +3 -2
  8. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  9. package/dist/esm/tools/code-interpreter-tool.js +4 -3
  10. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  11. package/dist/esm/tools/computer-use-tool.js +4 -3
  12. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  13. package/dist/esm/tools/custom-tool.js +4 -3
  14. package/dist/esm/tools/custom-tool.js.map +1 -1
  15. package/dist/esm/tools/file-search-tool.js +4 -3
  16. package/dist/esm/tools/file-search-tool.js.map +1 -1
  17. package/dist/esm/tools/image-generation-tool.d.ts +2 -4
  18. package/dist/esm/tools/image-generation-tool.js +6 -7
  19. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  20. package/dist/esm/tools/index.js +14 -0
  21. package/dist/esm/tools/local-shell-tool.js +3 -2
  22. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  23. package/dist/esm/tools/mcp-tool.js +7 -6
  24. package/dist/esm/tools/mcp-tool.js.map +1 -1
  25. package/dist/esm/tools/openai-provider-tool.d.ts +20 -0
  26. package/dist/esm/tools/openai-provider-tool.js +49 -0
  27. package/dist/esm/tools/openai-provider-tool.js.map +1 -0
  28. package/dist/esm/tools/shell-tool.js +4 -3
  29. package/dist/esm/tools/shell-tool.js.map +1 -1
  30. package/dist/esm/tools/tool-converter.js +7 -18
  31. package/dist/esm/tools/tool-converter.js.map +1 -1
  32. package/dist/esm/tools/web-search-preview-tool.d.ts +1 -2
  33. package/dist/esm/tools/web-search-preview-tool.js +5 -5
  34. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  35. package/dist/esm/tools/web-search-tool.d.ts +2 -4
  36. package/dist/esm/tools/web-search-tool.js +6 -7
  37. package/dist/esm/tools/web-search-tool.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/adapters/chat-completions-text.ts +11 -0
  40. package/src/adapters/responses-text.ts +86 -7
  41. package/src/tools/apply-patch-tool.ts +9 -5
  42. package/src/tools/code-interpreter-tool.ts +17 -8
  43. package/src/tools/computer-use-tool.ts +14 -7
  44. package/src/tools/custom-tool.ts +14 -7
  45. package/src/tools/file-search-tool.ts +14 -7
  46. package/src/tools/image-generation-tool.ts +19 -11
  47. package/src/tools/local-shell-tool.ts +9 -5
  48. package/src/tools/mcp-tool.ts +19 -9
  49. package/src/tools/openai-provider-tool.ts +72 -0
  50. package/src/tools/shell-tool.ts +17 -9
  51. package/src/tools/tool-converter.ts +28 -43
  52. package/src/tools/web-search-preview-tool.ts +17 -8
  53. package/src/tools/web-search-tool.ts +18 -10
@@ -31,6 +31,10 @@ import type {
31
31
  TextOptions,
32
32
  } from '@tanstack/ai'
33
33
 
34
+ // Base64 encoding of the '%PDF' file header — every PDF payload starts with
35
+ // these bytes, so inline document data must begin with this prefix.
36
+ const PDF_BASE64_MAGIC = 'JVBERi'
37
+
34
38
  /**
35
39
  * Provider-specific metadata that preserves the Responses API output item ID.
36
40
  *
@@ -1795,7 +1799,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1795
1799
  throw new Error(
1796
1800
  `User message for ${this.name} has no content parts. ` +
1797
1801
  `Empty user messages would produce a paid request with no input; ` +
1798
- `provide at least one text/image/audio part or omit the message.`,
1802
+ `provide at least one text/image/audio/document part or omit the message.`,
1799
1803
  )
1800
1804
  }
1801
1805
 
@@ -1811,7 +1815,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1811
1815
 
1812
1816
  /**
1813
1817
  * Converts a ContentPart to Responses API input content item.
1814
- * Handles text, image, and audio content parts.
1818
+ * Handles text, image, audio, and document (PDF) content parts.
1815
1819
  * Override this in subclasses for additional content types or provider-specific metadata.
1816
1820
  */
1817
1821
  protected convertContentPartToInput(part: ContentPart): ResponseInputContent {
@@ -1855,7 +1859,7 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1855
1859
  }
1856
1860
  }
1857
1861
  // Wrap raw base64 in a data URL — `input_file` rejects bare base64
1858
- // payloads (matches the image branch above which already does this).
1862
+ // payloads (matches the image branch above).
1859
1863
  // Default the MIME if missing so we never interpolate `undefined`.
1860
1864
  const audioValue = part.source.value
1861
1865
  const audioMime = part.source.mimeType || 'application/octet-stream'
@@ -1868,12 +1872,87 @@ export abstract class OpenAIBaseResponsesTextAdapter<
1868
1872
  }
1869
1873
  }
1870
1874
 
1875
+ case 'document': {
1876
+ const documentMetadata = part.metadata as
1877
+ | { filename?: string; detail?: 'auto' | 'low' | 'high' }
1878
+ | undefined
1879
+ // Spread `detail` only when provided so the API applies its own
1880
+ // default ('auto'). The Responses API accepts 'auto' | 'low' | 'high',
1881
+ // but the pinned OpenAI SDK's `ResponseInputFile.detail` type still
1882
+ // lists only 'low' | 'high' — cast so 'auto' (a valid API value) can
1883
+ // pass through without a type error.
1884
+ const documentDetail =
1885
+ documentMetadata?.detail !== undefined
1886
+ ? { detail: documentMetadata.detail as 'low' | 'high' }
1887
+ : {}
1888
+ if (part.source.type === 'url') {
1889
+ // The Responses API fetches the PDF itself; filename and MIME
1890
+ // type are inferred from the response.
1891
+ return {
1892
+ type: 'input_file',
1893
+ file_url: part.source.value,
1894
+ ...documentDetail,
1895
+ }
1896
+ }
1897
+ // This adapter supports only PDF documents; anything else is
1898
+ // rejected. MIME types are case-insensitive and can carry
1899
+ // ;parameters (RFC 2045), so the type is normalized before comparing.
1900
+ const documentValue = part.source.value
1901
+ const documentMime = (
1902
+ (part.source.mimeType || 'application/pdf').split(';')[0] ?? ''
1903
+ )
1904
+ .trim()
1905
+ .toLowerCase()
1906
+ if (documentMime !== 'application/pdf') {
1907
+ throw new Error(
1908
+ `${this.name} document parts only support application/pdf ` +
1909
+ `(received ${documentMime})`,
1910
+ )
1911
+ }
1912
+ // A pre-wrapped data URL carries its own media type — validate it too.
1913
+ if (
1914
+ documentValue.startsWith('data:') &&
1915
+ !/^data:application\/pdf[;,]/i.test(documentValue)
1916
+ ) {
1917
+ throw new Error(
1918
+ `${this.name} document parts only support application/pdf ` +
1919
+ `(received data URL with non-PDF media type)`,
1920
+ )
1921
+ }
1922
+ // Sniff the payload so a non-PDF labeled (or unlabeled) as PDF is
1923
+ // caught locally instead of by an opaque provider 400. Only base64
1924
+ // payloads are checked; a rare `data:` URL without `;base64` is left
1925
+ // to the server.
1926
+ const documentBase64 = documentValue.startsWith('data:')
1927
+ ? /;base64,/i.test(documentValue)
1928
+ ? documentValue.slice(documentValue.indexOf(',') + 1)
1929
+ : ''
1930
+ : documentValue
1931
+ if (documentBase64 && !documentBase64.startsWith(PDF_BASE64_MAGIC)) {
1932
+ throw new Error(
1933
+ `${this.name} document parts only support application/pdf ` +
1934
+ `(inline data does not start with the %PDF header)`,
1935
+ )
1936
+ }
1937
+ // Wrap raw base64 in a data URL — `input_file` rejects bare base64
1938
+ // payloads (matches the image and audio branches above).
1939
+ const documentFileData = documentValue.startsWith('data:')
1940
+ ? documentValue
1941
+ : `data:${documentMime};base64,${documentValue}`
1942
+ return {
1943
+ type: 'input_file',
1944
+ // The Responses API requires a filename alongside PDF `file_data`.
1945
+ filename: documentMetadata?.filename || 'document.pdf',
1946
+ file_data: documentFileData,
1947
+ ...documentDetail,
1948
+ }
1949
+ }
1950
+
1871
1951
  case 'video':
1872
- case 'document':
1873
1952
  default:
1874
- // OpenAI Responses API doesn't accept native video/document parts on
1875
- // this path — surface as explicit unsupported error so callers see
1876
- // the same message regardless of which content type leaked through.
1953
+ // OpenAI Responses API doesn't accept native video parts on this
1954
+ // path — surface as explicit unsupported error so callers see the
1955
+ // same message regardless of which content type leaked through.
1877
1956
  throw new Error(`Unsupported content part type: ${part.type}`)
1878
1957
  }
1879
1958
  }
@@ -1,3 +1,4 @@
1
+ import { openAIProviderTool } from './openai-provider-tool'
1
2
  import type { ApplyPatchTool as ApplyPatchToolConfig } from 'openai/resources/responses/responses'
2
3
  import type { Tool } from '@tanstack/ai'
3
4
 
@@ -24,9 +25,12 @@ export function convertApplyPatchToolToAdapterFormat(
24
25
  * re-wrap this in their own package.
25
26
  */
26
27
  export function applyPatchTool(): Tool {
27
- return {
28
- name: 'apply_patch',
29
- description: 'Apply a patch to modify files',
30
- metadata: {},
31
- }
28
+ return openAIProviderTool(
29
+ {
30
+ name: 'apply_patch',
31
+ description: 'Apply a patch to modify files',
32
+ metadata: {},
33
+ },
34
+ 'apply_patch',
35
+ )
32
36
  }
@@ -1,3 +1,7 @@
1
+ import {
2
+ getOpenAIProviderToolMetadata,
3
+ openAIProviderTool,
4
+ } from './openai-provider-tool'
1
5
  import type { Tool as SDKTool } from 'openai/resources/responses/responses'
2
6
  import type { Tool } from '@tanstack/ai'
3
7
 
@@ -14,7 +18,9 @@ export type CodeInterpreterTool = CodeInterpreterToolConfig
14
18
  export function convertCodeInterpreterToolToAdapterFormat(
15
19
  tool: Tool,
16
20
  ): CodeInterpreterToolConfig {
17
- const metadata = tool.metadata as CodeInterpreterToolConfig
21
+ const metadata = getOpenAIProviderToolMetadata(
22
+ tool,
23
+ ) as CodeInterpreterToolConfig
18
24
  return {
19
25
  type: 'code_interpreter',
20
26
  container: metadata.container,
@@ -30,12 +36,15 @@ export function convertCodeInterpreterToolToAdapterFormat(
30
36
  export function codeInterpreterTool(
31
37
  container: CodeInterpreterToolConfig,
32
38
  ): Tool {
33
- return {
34
- name: 'code_interpreter',
35
- description: 'Execute code in a sandboxed environment',
36
- metadata: {
37
- type: 'code_interpreter',
38
- container,
39
+ return openAIProviderTool(
40
+ {
41
+ name: 'code_interpreter',
42
+ description: 'Execute code in a sandboxed environment',
43
+ metadata: {
44
+ type: 'code_interpreter',
45
+ container,
46
+ },
39
47
  },
40
- }
48
+ 'code_interpreter',
49
+ )
41
50
  }
@@ -1,3 +1,7 @@
1
+ import {
2
+ getOpenAIProviderToolMetadata,
3
+ openAIProviderTool,
4
+ } from './openai-provider-tool'
1
5
  import type { ComputerUsePreviewTool as ComputerUseToolConfig } from 'openai/resources/responses/responses'
2
6
  import type { Tool } from '@tanstack/ai'
3
7
 
@@ -12,7 +16,7 @@ export type ComputerUseTool = ComputerUseToolConfig
12
16
  export function convertComputerUseToolToAdapterFormat(
13
17
  tool: Tool,
14
18
  ): ComputerUseToolConfig {
15
- const metadata = tool.metadata as ComputerUseToolConfig
19
+ const metadata = getOpenAIProviderToolMetadata(tool) as ComputerUseToolConfig
16
20
  return {
17
21
  type: 'computer_use_preview',
18
22
  display_height: metadata.display_height,
@@ -28,11 +32,14 @@ export function convertComputerUseToolToAdapterFormat(
28
32
  * re-wrap this in their own package.
29
33
  */
30
34
  export function computerUseTool(toolData: ComputerUseToolConfig): Tool {
31
- return {
32
- name: 'computer_use_preview',
33
- description: 'Control a virtual computer',
34
- metadata: {
35
- ...toolData,
35
+ return openAIProviderTool(
36
+ {
37
+ name: 'computer_use_preview',
38
+ description: 'Control a virtual computer',
39
+ metadata: {
40
+ ...toolData,
41
+ },
36
42
  },
37
- }
43
+ 'computer_use',
44
+ )
38
45
  }
@@ -1,3 +1,7 @@
1
+ import {
2
+ getOpenAIProviderToolMetadata,
3
+ openAIProviderTool,
4
+ } from './openai-provider-tool'
1
5
  import type { CustomTool as CustomToolConfig } from 'openai/resources/responses/responses'
2
6
  import type { Tool } from '@tanstack/ai'
3
7
 
@@ -10,7 +14,7 @@ export type CustomTool = CustomToolConfig
10
14
  * Converts a standard Tool to OpenAI CustomTool format
11
15
  */
12
16
  export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig {
13
- const metadata = tool.metadata as CustomToolConfig
17
+ const metadata = getOpenAIProviderToolMetadata(tool) as CustomToolConfig
14
18
  // Conditional spread: the SDK's `CustomToolConfig` declares optional
15
19
  // fields as `description?: string` (no `| undefined`) under
16
20
  // exactOptionalPropertyTypes, so we omit absent fields rather than
@@ -29,11 +33,14 @@ export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig {
29
33
  * Creates a standard Tool from CustomTool parameters.
30
34
  */
31
35
  export function customTool(toolData: CustomToolConfig): Tool {
32
- return {
33
- name: 'custom',
34
- description: toolData.description || 'A custom tool',
35
- metadata: {
36
- ...toolData,
36
+ return openAIProviderTool(
37
+ {
38
+ name: 'custom',
39
+ description: toolData.description || 'A custom tool',
40
+ metadata: {
41
+ ...toolData,
42
+ },
37
43
  },
38
- }
44
+ 'custom',
45
+ )
39
46
  }
@@ -1,3 +1,7 @@
1
+ import {
2
+ getOpenAIProviderToolMetadata,
3
+ openAIProviderTool,
4
+ } from './openai-provider-tool'
1
5
  import type { FileSearchTool as FileSearchToolConfig } from 'openai/resources/responses/responses'
2
6
  import type { Tool } from '@tanstack/ai'
3
7
 
@@ -21,7 +25,7 @@ export type FileSearchTool = FileSearchToolConfig
21
25
  export function convertFileSearchToolToAdapterFormat(
22
26
  tool: Tool,
23
27
  ): FileSearchToolConfig {
24
- const metadata = tool.metadata as FileSearchToolConfig
28
+ const metadata = getOpenAIProviderToolMetadata(tool) as FileSearchToolConfig
25
29
  // Conditional spread: SDK's `FileSearchToolConfig` declares the
26
30
  // optional fields without `| undefined`, so we omit absent values
27
31
  // rather than passing them through as explicit `undefined`.
@@ -46,13 +50,16 @@ export function convertFileSearchToolToAdapterFormat(
46
50
  */
47
51
  export function fileSearchTool(toolData: FileSearchToolConfig): Tool {
48
52
  validateMaxNumResults(toolData.max_num_results)
49
- return {
50
- name: 'file_search',
51
- description: 'Search files in vector stores',
52
- metadata: {
53
- ...toolData,
53
+ return openAIProviderTool(
54
+ {
55
+ name: 'file_search',
56
+ description: 'Search files in vector stores',
57
+ metadata: {
58
+ ...toolData,
59
+ },
54
60
  },
55
- }
61
+ 'file_search',
62
+ )
56
63
  }
57
64
 
58
65
  export { validateMaxNumResults }
@@ -1,3 +1,7 @@
1
+ import {
2
+ getOpenAIProviderToolMetadata,
3
+ openAIProviderTool,
4
+ } from './openai-provider-tool'
1
5
  import type { Tool as SDKTool } from 'openai/resources/responses/responses'
2
6
  import type { Tool } from '@tanstack/ai'
3
7
 
@@ -16,15 +20,16 @@ const validatePartialImages = (value: number | undefined) => {
16
20
 
17
21
  /**
18
22
  * Converts a standard Tool to OpenAI ImageGenerationTool format. Spread
19
- * `metadata` first, then force `type: 'image_generation'` last — otherwise a
20
- * `metadata.type` snuck in by a hand-built tool would shadow the literal and
21
- * the dispatcher (which routed by `tool.name`) would emit a tool whose
22
- * runtime `type` doesn't match `image_generation`.
23
+ * `metadata` first, then force `type: 'image_generation'` last so a stray
24
+ * `metadata.type` cannot shadow the wire discriminator.
23
25
  */
24
26
  export function convertImageGenerationToolToAdapterFormat(
25
27
  tool: Tool,
26
28
  ): ImageGenerationToolConfig {
27
- const metadata = tool.metadata as Omit<ImageGenerationToolConfig, 'type'>
29
+ const metadata = getOpenAIProviderToolMetadata(tool) as Omit<
30
+ ImageGenerationToolConfig,
31
+ 'type'
32
+ >
28
33
  return {
29
34
  ...metadata,
30
35
  type: 'image_generation',
@@ -41,13 +46,16 @@ export function imageGenerationTool(
41
46
  toolData: Omit<ImageGenerationToolConfig, 'type'>,
42
47
  ): Tool {
43
48
  validatePartialImages(toolData.partial_images)
44
- return {
45
- name: 'image_generation',
46
- description: 'Generate images based on text descriptions',
47
- metadata: {
48
- ...toolData,
49
+ return openAIProviderTool(
50
+ {
51
+ name: 'image_generation',
52
+ description: 'Generate images based on text descriptions',
53
+ metadata: {
54
+ ...toolData,
55
+ },
49
56
  },
50
- }
57
+ 'image_generation',
58
+ )
51
59
  }
52
60
 
53
61
  export { validatePartialImages }
@@ -1,3 +1,4 @@
1
+ import { openAIProviderTool } from './openai-provider-tool'
1
2
  import type { Tool as SDKTool } from 'openai/resources/responses/responses'
2
3
  import type { Tool } from '@tanstack/ai'
3
4
 
@@ -26,9 +27,12 @@ export function convertLocalShellToolToAdapterFormat(
26
27
  * re-wrap this in their own package.
27
28
  */
28
29
  export function localShellTool(): Tool {
29
- return {
30
- name: 'local_shell',
31
- description: 'Execute local shell commands',
32
- metadata: {},
33
- }
30
+ return openAIProviderTool(
31
+ {
32
+ name: 'local_shell',
33
+ description: 'Execute local shell commands',
34
+ metadata: {},
35
+ },
36
+ 'local_shell',
37
+ )
34
38
  }
@@ -1,3 +1,7 @@
1
+ import {
2
+ getOpenAIProviderToolMetadata,
3
+ openAIProviderTool,
4
+ } from './openai-provider-tool'
1
5
  import type { Tool as SDKTool } from 'openai/resources/responses/responses'
2
6
  import type { Tool } from '@tanstack/ai'
3
7
 
@@ -21,15 +25,18 @@ export function validateMCPtool(tool: MCPToolConfig) {
21
25
  * Converts a standard Tool to OpenAI MCPTool format
22
26
  */
23
27
  export function convertMCPToolToAdapterFormat(tool: Tool): MCPToolConfig {
24
- const metadata = tool.metadata as Omit<MCPToolConfig, 'type'>
28
+ const metadata = getOpenAIProviderToolMetadata(tool) as Omit<
29
+ MCPToolConfig,
30
+ 'type'
31
+ >
25
32
 
26
- const mcpTool: MCPToolConfig = {
33
+ const convertedTool: MCPToolConfig = {
27
34
  ...metadata,
28
35
  type: 'mcp',
29
36
  }
30
37
 
31
- validateMCPtool(mcpTool)
32
- return mcpTool
38
+ validateMCPtool(convertedTool)
39
+ return convertedTool
33
40
  }
34
41
 
35
42
  /**
@@ -41,9 +48,12 @@ export function convertMCPToolToAdapterFormat(tool: Tool): MCPToolConfig {
41
48
  export function mcpTool(toolData: Omit<MCPToolConfig, 'type'>): Tool {
42
49
  validateMCPtool({ ...toolData, type: 'mcp' })
43
50
 
44
- return {
45
- name: 'mcp',
46
- description: toolData.server_description || '',
47
- metadata: toolData,
48
- }
51
+ return openAIProviderTool(
52
+ {
53
+ name: 'mcp',
54
+ description: toolData.server_description || '',
55
+ metadata: toolData,
56
+ },
57
+ 'mcp',
58
+ )
49
59
  }
@@ -0,0 +1,72 @@
1
+ import type { Tool } from '@tanstack/ai'
2
+
3
+ const OPENAI_PROVIDER_TOOL_KINDS = {
4
+ apply_patch: 'openai.apply_patch',
5
+ code_interpreter: 'openai.code_interpreter',
6
+ computer_use: 'openai.computer_use',
7
+ custom: 'openai.custom',
8
+ file_search: 'openai.file_search',
9
+ image_generation: 'openai.image_generation',
10
+ local_shell: 'openai.local_shell',
11
+ mcp: 'openai.mcp',
12
+ shell: 'openai.shell',
13
+ web_search: 'openai.web_search',
14
+ web_search_preview: 'openai.web_search_preview',
15
+ } as const
16
+
17
+ export type OpenAIProviderToolKind = keyof typeof OPENAI_PROVIDER_TOOL_KINDS
18
+
19
+ export function openAIProviderTool(
20
+ tool: Tool,
21
+ toolKind: OpenAIProviderToolKind,
22
+ ): Tool {
23
+ return {
24
+ ...tool,
25
+ metadata: {
26
+ ...tool.metadata,
27
+ __kind: OPENAI_PROVIDER_TOOL_KINDS[toolKind],
28
+ },
29
+ }
30
+ }
31
+
32
+ export function getOpenAIProviderToolKind(
33
+ tool: Tool,
34
+ ): OpenAIProviderToolKind | undefined {
35
+ const toolKind = tool.metadata?.['__kind']
36
+ switch (toolKind) {
37
+ case 'openai.apply_patch':
38
+ return 'apply_patch'
39
+ case 'openai.code_interpreter':
40
+ return 'code_interpreter'
41
+ case 'openai.computer_use':
42
+ return 'computer_use'
43
+ case 'openai.custom':
44
+ return 'custom'
45
+ case 'openai.file_search':
46
+ return 'file_search'
47
+ case 'openai.image_generation':
48
+ return 'image_generation'
49
+ case 'openai.local_shell':
50
+ return 'local_shell'
51
+ case 'openai.mcp':
52
+ return 'mcp'
53
+ case 'openai.shell':
54
+ return 'shell'
55
+ case 'openai.web_search':
56
+ return 'web_search'
57
+ case 'openai.web_search_preview':
58
+ return 'web_search_preview'
59
+ default:
60
+ return undefined
61
+ }
62
+ }
63
+
64
+ /** Returns adapter metadata without the internal runtime discriminator. */
65
+ export function getOpenAIProviderToolMetadata(tool: Tool): Tool['metadata'] {
66
+ if (!tool.metadata) {
67
+ return undefined
68
+ }
69
+ const { __kind: _kind, ...metadata } = tool.metadata
70
+ void _kind
71
+ return metadata
72
+ }
@@ -1,3 +1,7 @@
1
+ import {
2
+ getOpenAIProviderToolMetadata,
3
+ openAIProviderTool,
4
+ } from './openai-provider-tool'
1
5
  import type { FunctionShellTool as ShellToolConfig } from 'openai/resources/responses/responses'
2
6
  import type { Tool } from '@tanstack/ai'
3
7
 
@@ -21,7 +25,8 @@ export interface ShellToolFactoryConfig {
21
25
  * `environment` (container config + skills) stored in metadata.
22
26
  */
23
27
  export function convertShellToolToAdapterFormat(tool: Tool): ShellToolConfig {
24
- const metadata = (tool.metadata ?? {}) as ShellToolFactoryConfig
28
+ const metadata = (getOpenAIProviderToolMetadata(tool) ??
29
+ {}) as ShellToolFactoryConfig
25
30
  return {
26
31
  type: 'shell',
27
32
  ...(metadata.environment !== undefined && {
@@ -37,13 +42,16 @@ export function convertShellToolToAdapterFormat(tool: Tool): ShellToolConfig {
37
42
  * re-wrap this in their own package.
38
43
  */
39
44
  export function shellTool(config: ShellToolFactoryConfig = {}): Tool {
40
- return {
41
- name: 'shell',
42
- description: 'Execute shell commands',
43
- metadata: {
44
- ...(config.environment !== undefined && {
45
- environment: config.environment,
46
- }),
45
+ return openAIProviderTool(
46
+ {
47
+ name: 'shell',
48
+ description: 'Execute shell commands',
49
+ metadata: {
50
+ ...(config.environment !== undefined && {
51
+ environment: config.environment,
52
+ }),
53
+ },
47
54
  },
48
- }
55
+ 'shell',
56
+ )
49
57
  }
@@ -7,62 +7,47 @@ import { convertFunctionToolToAdapterFormat } from './function-tool'
7
7
  import { convertImageGenerationToolToAdapterFormat } from './image-generation-tool'
8
8
  import { convertLocalShellToolToAdapterFormat } from './local-shell-tool'
9
9
  import { convertMCPToolToAdapterFormat } from './mcp-tool'
10
+ import { getOpenAIProviderToolKind } from './openai-provider-tool'
10
11
  import { convertShellToolToAdapterFormat } from './shell-tool'
11
12
  import { convertWebSearchPreviewToolToAdapterFormat } from './web-search-preview-tool'
12
13
  import { convertWebSearchToolToAdapterFormat } from './web-search-tool'
13
14
  import type { OpenAITool } from './index'
15
+ import { assertUniqueToolNames } from '@tanstack/ai/adapter-internals'
14
16
  import type { Tool } from '@tanstack/ai'
15
17
 
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
18
  /**
31
19
  * Converts an array of standard Tools to OpenAI-specific format
32
20
  */
33
21
  export function convertToolsToProviderFormat(
34
22
  tools: Array<Tool>,
35
23
  ): Array<OpenAITool> {
24
+ assertUniqueToolNames(tools)
36
25
  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
- }
26
+ switch (getOpenAIProviderToolKind(tool)) {
27
+ case 'apply_patch':
28
+ return convertApplyPatchToolToAdapterFormat(tool)
29
+ case 'code_interpreter':
30
+ return convertCodeInterpreterToolToAdapterFormat(tool)
31
+ case 'computer_use':
32
+ return convertComputerUseToolToAdapterFormat(tool)
33
+ case 'custom':
34
+ return convertCustomToolToAdapterFormat(tool)
35
+ case 'file_search':
36
+ return convertFileSearchToolToAdapterFormat(tool)
37
+ case 'image_generation':
38
+ return convertImageGenerationToolToAdapterFormat(tool)
39
+ case 'local_shell':
40
+ return convertLocalShellToolToAdapterFormat(tool)
41
+ case 'mcp':
42
+ return convertMCPToolToAdapterFormat(tool)
43
+ case 'shell':
44
+ return convertShellToolToAdapterFormat(tool)
45
+ case 'web_search_preview':
46
+ return convertWebSearchPreviewToolToAdapterFormat(tool)
47
+ case 'web_search':
48
+ return convertWebSearchToolToAdapterFormat(tool)
49
+ case undefined:
50
+ return convertFunctionToolToAdapterFormat(tool)
64
51
  }
65
-
66
- return convertFunctionToolToAdapterFormat(tool)
67
52
  })
68
53
  }