@tanstack/openai-base 0.7.0 → 0.8.1

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.
@@ -1,4 +1,4 @@
1
- import { ComputerTool as ComputerUseToolConfig } from 'openai/resources/responses/responses';
1
+ import { ComputerUsePreviewTool as ComputerUseToolConfig } from 'openai/resources/responses/responses';
2
2
  import { Tool } from '@tanstack/ai';
3
3
  export type { ComputerUseToolConfig };
4
4
  /** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */
@@ -1 +1 @@
1
- {"version":3,"file":"computer-use-tool.js","sources":["../../../src/tools/computer-use-tool.ts"],"sourcesContent":["import type { ComputerTool as ComputerUseToolConfig } from 'openai/resources/responses/responses'\nimport type { Tool } from '@tanstack/ai'\n\nexport type { ComputerUseToolConfig }\n\n/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */\nexport type ComputerUseTool = ComputerUseToolConfig\n\n/**\n * Converts a standard Tool to OpenAI ComputerUseTool format\n */\nexport function convertComputerUseToolToAdapterFormat(\n tool: Tool,\n): ComputerUseToolConfig {\n const metadata = tool.metadata as ComputerUseToolConfig\n return {\n type: 'computer_use_preview',\n display_height: metadata.display_height,\n display_width: metadata.display_width,\n environment: metadata.environment,\n }\n}\n\n/**\n * Creates a standard Tool from ComputerUseTool parameters.\n *\n * Base (non-branded) factory. Providers that need branded return types should\n * re-wrap this in their own package.\n */\nexport function computerUseTool(toolData: ComputerUseToolConfig): Tool {\n return {\n name: 'computer_use_preview',\n description: 'Control a virtual computer',\n metadata: {\n ...toolData,\n },\n }\n}\n"],"names":[],"mappings":"AAWO,SAAS,sCACd,MACuB;AACvB,QAAM,WAAW,KAAK;AACtB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB,SAAS;AAAA,IACzB,eAAe,SAAS;AAAA,IACxB,aAAa,SAAS;AAAA,EAAA;AAE1B;AAQO,SAAS,gBAAgB,UAAuC;AACrE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,aAAa;AAAA,IACb,UAAU;AAAA,MACR,GAAG;AAAA,IAAA;AAAA,EACL;AAEJ;"}
1
+ {"version":3,"file":"computer-use-tool.js","sources":["../../../src/tools/computer-use-tool.ts"],"sourcesContent":["import type { ComputerUsePreviewTool as ComputerUseToolConfig } from 'openai/resources/responses/responses'\nimport type { Tool } from '@tanstack/ai'\n\nexport type { ComputerUseToolConfig }\n\n/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */\nexport type ComputerUseTool = ComputerUseToolConfig\n\n/**\n * Converts a standard Tool to OpenAI ComputerUseTool format\n */\nexport function convertComputerUseToolToAdapterFormat(\n tool: Tool,\n): ComputerUseToolConfig {\n const metadata = tool.metadata as ComputerUseToolConfig\n return {\n type: 'computer_use_preview',\n display_height: metadata.display_height,\n display_width: metadata.display_width,\n environment: metadata.environment,\n }\n}\n\n/**\n * Creates a standard Tool from ComputerUseTool parameters.\n *\n * Base (non-branded) factory. Providers that need branded return types should\n * re-wrap this in their own package.\n */\nexport function computerUseTool(toolData: ComputerUseToolConfig): Tool {\n return {\n name: 'computer_use_preview',\n description: 'Control a virtual computer',\n metadata: {\n ...toolData,\n },\n }\n}\n"],"names":[],"mappings":"AAWO,SAAS,sCACd,MACuB;AACvB,QAAM,WAAW,KAAK;AACtB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,gBAAgB,SAAS;AAAA,IACzB,eAAe,SAAS;AAAA,IACxB,aAAa,SAAS;AAAA,EAAA;AAE1B;AAQO,SAAS,gBAAgB,UAAuC;AACrE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,aAAa;AAAA,IACb,UAAU;AAAA,MACR,GAAG;AAAA,IAAA;AAAA,EACL;AAEJ;"}
@@ -4,13 +4,23 @@ export type { ShellToolConfig };
4
4
  /** @deprecated Renamed to `ShellToolConfig`. Will be removed in a future release. */
5
5
  export type ShellTool = ShellToolConfig;
6
6
  /**
7
- * Converts a standard Tool to OpenAI ShellTool format
7
+ * Config accepted by {@link shellTool}. `environment` mirrors the OpenAI
8
+ * Responses API shell tool environment (e.g. `container_auto` + `skills`).
9
+ * Typed via indexed access so it tracks the installed SDK without naming the
10
+ * union members directly.
8
11
  */
9
- export declare function convertShellToolToAdapterFormat(_tool: Tool): ShellToolConfig;
12
+ export interface ShellToolFactoryConfig {
13
+ environment?: NonNullable<ShellToolConfig['environment']>;
14
+ }
15
+ /**
16
+ * Converts a standard Tool to OpenAI ShellTool format, preserving any
17
+ * `environment` (container config + skills) stored in metadata.
18
+ */
19
+ export declare function convertShellToolToAdapterFormat(tool: Tool): ShellToolConfig;
10
20
  /**
11
21
  * Creates a standard Tool from ShellTool parameters.
12
22
  *
13
23
  * Base (non-branded) factory. Providers that need branded return types should
14
24
  * re-wrap this in their own package.
15
25
  */
16
- export declare function shellTool(): Tool;
26
+ export declare function shellTool(config?: ShellToolFactoryConfig): Tool;
@@ -1,13 +1,21 @@
1
- function convertShellToolToAdapterFormat(_tool) {
1
+ function convertShellToolToAdapterFormat(tool) {
2
+ const metadata = tool.metadata ?? {};
2
3
  return {
3
- type: "shell"
4
+ type: "shell",
5
+ ...metadata.environment !== void 0 && {
6
+ environment: metadata.environment
7
+ }
4
8
  };
5
9
  }
6
- function shellTool() {
10
+ function shellTool(config = {}) {
7
11
  return {
8
12
  name: "shell",
9
13
  description: "Execute shell commands",
10
- metadata: {}
14
+ metadata: {
15
+ ...config.environment !== void 0 && {
16
+ environment: config.environment
17
+ }
18
+ }
11
19
  };
12
20
  }
13
21
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"shell-tool.js","sources":["../../../src/tools/shell-tool.ts"],"sourcesContent":["import type { FunctionShellTool as ShellToolConfig } from 'openai/resources/responses/responses'\nimport type { Tool } from '@tanstack/ai'\n\nexport type { ShellToolConfig }\n\n/** @deprecated Renamed to `ShellToolConfig`. Will be removed in a future release. */\nexport type ShellTool = ShellToolConfig\n\n/**\n * Converts a standard Tool to OpenAI ShellTool format\n */\nexport function convertShellToolToAdapterFormat(_tool: Tool): ShellToolConfig {\n return {\n type: 'shell',\n }\n}\n\n/**\n * Creates a standard Tool from ShellTool parameters.\n *\n * Base (non-branded) factory. Providers that need branded return types should\n * re-wrap this in their own package.\n */\nexport function shellTool(): Tool {\n return {\n name: 'shell',\n description: 'Execute shell commands',\n metadata: {},\n }\n}\n"],"names":[],"mappings":"AAWO,SAAS,gCAAgC,OAA8B;AAC5E,SAAO;AAAA,IACL,MAAM;AAAA,EAAA;AAEV;AAQO,SAAS,YAAkB;AAChC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,aAAa;AAAA,IACb,UAAU,CAAA;AAAA,EAAC;AAEf;"}
1
+ {"version":3,"file":"shell-tool.js","sources":["../../../src/tools/shell-tool.ts"],"sourcesContent":["import type { FunctionShellTool as ShellToolConfig } from 'openai/resources/responses/responses'\nimport type { Tool } from '@tanstack/ai'\n\nexport type { ShellToolConfig }\n\n/** @deprecated Renamed to `ShellToolConfig`. Will be removed in a future release. */\nexport type ShellTool = ShellToolConfig\n\n/**\n * Config accepted by {@link shellTool}. `environment` mirrors the OpenAI\n * Responses API shell tool environment (e.g. `container_auto` + `skills`).\n * Typed via indexed access so it tracks the installed SDK without naming the\n * union members directly.\n */\nexport interface ShellToolFactoryConfig {\n environment?: NonNullable<ShellToolConfig['environment']>\n}\n\n/**\n * Converts a standard Tool to OpenAI ShellTool format, preserving any\n * `environment` (container config + skills) stored in metadata.\n */\nexport function convertShellToolToAdapterFormat(tool: Tool): ShellToolConfig {\n const metadata = (tool.metadata ?? {}) as ShellToolFactoryConfig\n return {\n type: 'shell',\n ...(metadata.environment !== undefined && {\n environment: metadata.environment,\n }),\n }\n}\n\n/**\n * Creates a standard Tool from ShellTool parameters.\n *\n * Base (non-branded) factory. Providers that need branded return types should\n * re-wrap this in their own package.\n */\nexport function shellTool(config: ShellToolFactoryConfig = {}): Tool {\n return {\n name: 'shell',\n description: 'Execute shell commands',\n metadata: {\n ...(config.environment !== undefined && {\n environment: config.environment,\n }),\n },\n }\n}\n"],"names":[],"mappings":"AAsBO,SAAS,gCAAgC,MAA6B;AAC3E,QAAM,WAAY,KAAK,YAAY,CAAA;AACnC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,GAAI,SAAS,gBAAgB,UAAa;AAAA,MACxC,aAAa,SAAS;AAAA,IAAA;AAAA,EACxB;AAEJ;AAQO,SAAS,UAAU,SAAiC,IAAU;AACnE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,aAAa;AAAA,IACb,UAAU;AAAA,MACR,GAAI,OAAO,gBAAgB,UAAa;AAAA,QACtC,aAAa,OAAO;AAAA,MAAA;AAAA,IACtB;AAAA,EACF;AAEJ;"}
@@ -43,7 +43,7 @@ function convertToolsToProviderFormat(tools) {
43
43
  case "mcp":
44
44
  return convertMCPToolToAdapterFormat(tool);
45
45
  case "shell":
46
- return convertShellToolToAdapterFormat();
46
+ return convertShellToolToAdapterFormat(tool);
47
47
  case "web_search_preview":
48
48
  return convertWebSearchPreviewToolToAdapterFormat(tool);
49
49
  case "web_search":
@@ -1 +1 @@
1
- {"version":3,"file":"tool-converter.js","sources":["../../../src/tools/tool-converter.ts"],"sourcesContent":["import { convertApplyPatchToolToAdapterFormat } from './apply-patch-tool'\nimport { convertCodeInterpreterToolToAdapterFormat } from './code-interpreter-tool'\nimport { convertComputerUseToolToAdapterFormat } from './computer-use-tool'\nimport { convertCustomToolToAdapterFormat } from './custom-tool'\nimport { convertFileSearchToolToAdapterFormat } from './file-search-tool'\nimport { convertFunctionToolToAdapterFormat } from './function-tool'\nimport { convertImageGenerationToolToAdapterFormat } from './image-generation-tool'\nimport { convertLocalShellToolToAdapterFormat } from './local-shell-tool'\nimport { convertMCPToolToAdapterFormat } from './mcp-tool'\nimport { convertShellToolToAdapterFormat } from './shell-tool'\nimport { convertWebSearchPreviewToolToAdapterFormat } from './web-search-preview-tool'\nimport { convertWebSearchToolToAdapterFormat } from './web-search-tool'\nimport type { OpenAITool } from './index'\nimport type { Tool } from '@tanstack/ai'\n\nconst SPECIAL_TOOL_NAMES = new Set([\n 'apply_patch',\n 'code_interpreter',\n 'computer_use_preview',\n 'file_search',\n 'image_generation',\n 'local_shell',\n 'mcp',\n 'shell',\n 'web_search_preview',\n 'web_search',\n 'custom',\n])\n\n/**\n * Converts an array of standard Tools to OpenAI-specific format\n */\nexport function convertToolsToProviderFormat(\n tools: Array<Tool>,\n): Array<OpenAITool> {\n return tools.map((tool) => {\n const toolName = tool.name\n\n if (SPECIAL_TOOL_NAMES.has(toolName)) {\n switch (toolName) {\n case 'apply_patch':\n return convertApplyPatchToolToAdapterFormat(tool)\n case 'code_interpreter':\n return convertCodeInterpreterToolToAdapterFormat(tool)\n case 'computer_use_preview':\n return convertComputerUseToolToAdapterFormat(tool)\n case 'file_search':\n return convertFileSearchToolToAdapterFormat(tool)\n case 'image_generation':\n return convertImageGenerationToolToAdapterFormat(tool)\n case 'local_shell':\n return convertLocalShellToolToAdapterFormat(tool)\n case 'mcp':\n return convertMCPToolToAdapterFormat(tool)\n case 'shell':\n return convertShellToolToAdapterFormat(tool)\n case 'web_search_preview':\n return convertWebSearchPreviewToolToAdapterFormat(tool)\n case 'web_search':\n return convertWebSearchToolToAdapterFormat(tool)\n case 'custom':\n return convertCustomToolToAdapterFormat(tool)\n }\n }\n\n return convertFunctionToolToAdapterFormat(tool)\n })\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAeA,MAAM,yCAAyB,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAKM,SAAS,6BACd,OACmB;AACnB,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,UAAM,WAAW,KAAK;AAEtB,QAAI,mBAAmB,IAAI,QAAQ,GAAG;AACpC,cAAQ,UAAA;AAAA,QACN,KAAK;AACH,iBAAO,qCAAyC;AAAA,QAClD,KAAK;AACH,iBAAO,0CAA0C,IAAI;AAAA,QACvD,KAAK;AACH,iBAAO,sCAAsC,IAAI;AAAA,QACnD,KAAK;AACH,iBAAO,qCAAqC,IAAI;AAAA,QAClD,KAAK;AACH,iBAAO,0CAA0C,IAAI;AAAA,QACvD,KAAK;AACH,iBAAO,qCAAyC;AAAA,QAClD,KAAK;AACH,iBAAO,8BAA8B,IAAI;AAAA,QAC3C,KAAK;AACH,iBAAO,gCAAoC;AAAA,QAC7C,KAAK;AACH,iBAAO,2CAA2C,IAAI;AAAA,QACxD,KAAK;AACH,iBAAO,oCAAoC,IAAI;AAAA,QACjD,KAAK;AACH,iBAAO,iCAAiC,IAAI;AAAA,MAAA;AAAA,IAElD;AAEA,WAAO,mCAAmC,IAAI;AAAA,EAChD,CAAC;AACH;"}
1
+ {"version":3,"file":"tool-converter.js","sources":["../../../src/tools/tool-converter.ts"],"sourcesContent":["import { convertApplyPatchToolToAdapterFormat } from './apply-patch-tool'\nimport { convertCodeInterpreterToolToAdapterFormat } from './code-interpreter-tool'\nimport { convertComputerUseToolToAdapterFormat } from './computer-use-tool'\nimport { convertCustomToolToAdapterFormat } from './custom-tool'\nimport { convertFileSearchToolToAdapterFormat } from './file-search-tool'\nimport { convertFunctionToolToAdapterFormat } from './function-tool'\nimport { convertImageGenerationToolToAdapterFormat } from './image-generation-tool'\nimport { convertLocalShellToolToAdapterFormat } from './local-shell-tool'\nimport { convertMCPToolToAdapterFormat } from './mcp-tool'\nimport { convertShellToolToAdapterFormat } from './shell-tool'\nimport { convertWebSearchPreviewToolToAdapterFormat } from './web-search-preview-tool'\nimport { convertWebSearchToolToAdapterFormat } from './web-search-tool'\nimport type { OpenAITool } from './index'\nimport type { Tool } from '@tanstack/ai'\n\nconst SPECIAL_TOOL_NAMES = new Set([\n 'apply_patch',\n 'code_interpreter',\n 'computer_use_preview',\n 'file_search',\n 'image_generation',\n 'local_shell',\n 'mcp',\n 'shell',\n 'web_search_preview',\n 'web_search',\n 'custom',\n])\n\n/**\n * Converts an array of standard Tools to OpenAI-specific format\n */\nexport function convertToolsToProviderFormat(\n tools: Array<Tool>,\n): Array<OpenAITool> {\n return tools.map((tool) => {\n const toolName = tool.name\n\n if (SPECIAL_TOOL_NAMES.has(toolName)) {\n switch (toolName) {\n case 'apply_patch':\n return convertApplyPatchToolToAdapterFormat(tool)\n case 'code_interpreter':\n return convertCodeInterpreterToolToAdapterFormat(tool)\n case 'computer_use_preview':\n return convertComputerUseToolToAdapterFormat(tool)\n case 'file_search':\n return convertFileSearchToolToAdapterFormat(tool)\n case 'image_generation':\n return convertImageGenerationToolToAdapterFormat(tool)\n case 'local_shell':\n return convertLocalShellToolToAdapterFormat(tool)\n case 'mcp':\n return convertMCPToolToAdapterFormat(tool)\n case 'shell':\n return convertShellToolToAdapterFormat(tool)\n case 'web_search_preview':\n return convertWebSearchPreviewToolToAdapterFormat(tool)\n case 'web_search':\n return convertWebSearchToolToAdapterFormat(tool)\n case 'custom':\n return convertCustomToolToAdapterFormat(tool)\n }\n }\n\n return convertFunctionToolToAdapterFormat(tool)\n })\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAeA,MAAM,yCAAyB,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAKM,SAAS,6BACd,OACmB;AACnB,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,UAAM,WAAW,KAAK;AAEtB,QAAI,mBAAmB,IAAI,QAAQ,GAAG;AACpC,cAAQ,UAAA;AAAA,QACN,KAAK;AACH,iBAAO,qCAAyC;AAAA,QAClD,KAAK;AACH,iBAAO,0CAA0C,IAAI;AAAA,QACvD,KAAK;AACH,iBAAO,sCAAsC,IAAI;AAAA,QACnD,KAAK;AACH,iBAAO,qCAAqC,IAAI;AAAA,QAClD,KAAK;AACH,iBAAO,0CAA0C,IAAI;AAAA,QACvD,KAAK;AACH,iBAAO,qCAAyC;AAAA,QAClD,KAAK;AACH,iBAAO,8BAA8B,IAAI;AAAA,QAC3C,KAAK;AACH,iBAAO,gCAAgC,IAAI;AAAA,QAC7C,KAAK;AACH,iBAAO,2CAA2C,IAAI;AAAA,QACxD,KAAK;AACH,iBAAO,oCAAoC,IAAI;AAAA,QACjD,KAAK;AACH,iBAAO,iCAAiC,IAAI;AAAA,MAAA;AAAA,IAElD;AAEA,WAAO,mCAAmC,IAAI;AAAA,EAChD,CAAC;AACH;"}
@@ -4,6 +4,7 @@
4
4
  * - All properties must be in the `required` array
5
5
  * - Optional fields should have null added to their type union
6
6
  * - additionalProperties must be false for objects
7
+ * - String `format` keywords must be from a fixed allowlist (others are stripped)
7
8
  *
8
9
  * @param schema - JSON schema to transform
9
10
  * @param originalRequired - Original required array (to know which fields were optional)
@@ -1,4 +1,30 @@
1
+ const SUPPORTED_STRING_FORMATS = /* @__PURE__ */ new Set([
2
+ "date-time",
3
+ "time",
4
+ "date",
5
+ "duration",
6
+ "email",
7
+ "hostname",
8
+ "ipv4",
9
+ "ipv6",
10
+ "uuid"
11
+ ]);
12
+ function stripUnsupportedFormats(node) {
13
+ if (Array.isArray(node)) return node.map(stripUnsupportedFormats);
14
+ if (node === null || typeof node !== "object") return node;
15
+ const out = {};
16
+ for (const [key, value] of Object.entries(node)) {
17
+ if (key === "format" && typeof value === "string" && !SUPPORTED_STRING_FORMATS.has(value)) {
18
+ continue;
19
+ }
20
+ out[key] = stripUnsupportedFormats(value);
21
+ }
22
+ return out;
23
+ }
1
24
  function makeStructuredOutputCompatible(schema, originalRequired) {
25
+ return stripUnsupportedFormats(coerceStrictSchema(schema, originalRequired));
26
+ }
27
+ function coerceStrictSchema(schema, originalRequired) {
2
28
  const result = { ...schema };
3
29
  const required = originalRequired ?? (Array.isArray(result["required"]) ? result["required"] : []);
4
30
  if (result.type === "object" && result.properties) {
@@ -8,17 +34,14 @@ function makeStructuredOutputCompatible(schema, originalRequired) {
8
34
  let prop = properties[propName];
9
35
  const wasOptional = !required.includes(propName);
10
36
  if (prop.type === "object" && prop.properties) {
11
- prop = makeStructuredOutputCompatible(prop, prop.required || []);
37
+ prop = coerceStrictSchema(prop, prop.required || []);
12
38
  } else if (prop.type === "array" && prop.items) {
13
39
  prop = {
14
40
  ...prop,
15
- items: makeStructuredOutputCompatible(
16
- prop.items,
17
- prop.items.required || []
18
- )
41
+ items: coerceStrictSchema(prop.items, prop.items.required || [])
19
42
  };
20
43
  } else if (prop.anyOf) {
21
- prop = makeStructuredOutputCompatible(prop, prop.required || []);
44
+ prop = coerceStrictSchema(prop, prop.required || []);
22
45
  } else if (prop.oneOf) {
23
46
  throw new Error(
24
47
  "oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types"
@@ -42,14 +65,11 @@ function makeStructuredOutputCompatible(schema, originalRequired) {
42
65
  result.additionalProperties = false;
43
66
  }
44
67
  if (result.type === "array" && result.items) {
45
- result.items = makeStructuredOutputCompatible(
46
- result.items,
47
- result.items.required || []
48
- );
68
+ result.items = coerceStrictSchema(result.items, result.items.required || []);
49
69
  }
50
70
  if (result.anyOf && Array.isArray(result.anyOf)) {
51
71
  result.anyOf = result.anyOf.map(
52
- (variant) => makeStructuredOutputCompatible(variant, variant.required || [])
72
+ (variant) => coerceStrictSchema(variant, variant.required || [])
53
73
  );
54
74
  }
55
75
  if (result.oneOf) {
@@ -1 +1 @@
1
- {"version":3,"file":"schema-converter.js","sources":["../../../src/utils/schema-converter.ts"],"sourcesContent":["/**\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 *\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 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 = makeStructuredOutputCompatible(prop, prop.required || [])\n } else if (prop.type === 'array' && prop.items) {\n prop = {\n ...prop,\n items: makeStructuredOutputCompatible(\n prop.items,\n prop.items.required || [],\n ),\n }\n } else if (prop.anyOf) {\n prop = makeStructuredOutputCompatible(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 = makeStructuredOutputCompatible(\n result.items,\n result.items.required || [],\n )\n }\n\n if (result.anyOf && Array.isArray(result.anyOf)) {\n result.anyOf = result.anyOf.map((variant) =>\n makeStructuredOutputCompatible(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":"AAWO,SAAS,+BACd,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,+BAA+B,MAAM,KAAK,YAAY,CAAA,CAAE;AAAA,MACjE,WAAW,KAAK,SAAS,WAAW,KAAK,OAAO;AAC9C,eAAO;AAAA,UACL,GAAG;AAAA,UACH,OAAO;AAAA,YACL,KAAK;AAAA,YACL,KAAK,MAAM,YAAY,CAAA;AAAA,UAAC;AAAA,QAC1B;AAAA,MAEJ,WAAW,KAAK,OAAO;AACrB,eAAO,+BAA+B,MAAM,KAAK,YAAY,CAAA,CAAE;AAAA,MACjE,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;AAAA,MACb,OAAO;AAAA,MACP,OAAO,MAAM,YAAY,CAAA;AAAA,IAAC;AAAA,EAE9B;AAEA,MAAI,OAAO,SAAS,MAAM,QAAQ,OAAO,KAAK,GAAG;AAC/C,WAAO,QAAQ,OAAO,MAAM;AAAA,MAAI,CAAC,YAC/B,+BAA+B,SAAS,QAAQ,YAAY,CAAA,CAAE;AAAA,IAAA;AAAA,EAElE;AAEA,MAAI,OAAO,OAAO;AAChB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO;AACT;"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/openai-base",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Shared OpenAI SDK base adapters for TanStack AI providers using Chat Completions and Responses APIs.",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -35,17 +35,17 @@
35
35
  "structured-outputs"
36
36
  ],
37
37
  "dependencies": {
38
- "openai": "^6.9.1",
38
+ "openai": "^6.41.0",
39
39
  "@tanstack/ai-utils": "0.2.1"
40
40
  },
41
41
  "peerDependencies": {
42
- "@tanstack/ai": "^0.27.0"
42
+ "@tanstack/ai": "^0.28.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@vitest/coverage-v8": "4.0.14",
46
46
  "vite": "^7.3.3",
47
47
  "zod": "^4.2.0",
48
- "@tanstack/ai": "0.27.0"
48
+ "@tanstack/ai": "0.28.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "vite build",
@@ -1,4 +1,4 @@
1
- import type { ComputerTool as ComputerUseToolConfig } from 'openai/resources/responses/responses'
1
+ import type { ComputerUsePreviewTool as ComputerUseToolConfig } from 'openai/resources/responses/responses'
2
2
  import type { Tool } from '@tanstack/ai'
3
3
 
4
4
  export type { ComputerUseToolConfig }
@@ -7,11 +7,26 @@ export type { ShellToolConfig }
7
7
  export type ShellTool = ShellToolConfig
8
8
 
9
9
  /**
10
- * Converts a standard Tool to OpenAI ShellTool format
10
+ * Config accepted by {@link shellTool}. `environment` mirrors the OpenAI
11
+ * Responses API shell tool environment (e.g. `container_auto` + `skills`).
12
+ * Typed via indexed access so it tracks the installed SDK without naming the
13
+ * union members directly.
11
14
  */
12
- export function convertShellToolToAdapterFormat(_tool: Tool): ShellToolConfig {
15
+ export interface ShellToolFactoryConfig {
16
+ environment?: NonNullable<ShellToolConfig['environment']>
17
+ }
18
+
19
+ /**
20
+ * Converts a standard Tool to OpenAI ShellTool format, preserving any
21
+ * `environment` (container config + skills) stored in metadata.
22
+ */
23
+ export function convertShellToolToAdapterFormat(tool: Tool): ShellToolConfig {
24
+ const metadata = (tool.metadata ?? {}) as ShellToolFactoryConfig
13
25
  return {
14
26
  type: 'shell',
27
+ ...(metadata.environment !== undefined && {
28
+ environment: metadata.environment,
29
+ }),
15
30
  }
16
31
  }
17
32
 
@@ -21,10 +36,14 @@ export function convertShellToolToAdapterFormat(_tool: Tool): ShellToolConfig {
21
36
  * Base (non-branded) factory. Providers that need branded return types should
22
37
  * re-wrap this in their own package.
23
38
  */
24
- export function shellTool(): Tool {
39
+ export function shellTool(config: ShellToolFactoryConfig = {}): Tool {
25
40
  return {
26
41
  name: 'shell',
27
42
  description: 'Execute shell commands',
28
- metadata: {},
43
+ metadata: {
44
+ ...(config.environment !== undefined && {
45
+ environment: config.environment,
46
+ }),
47
+ },
29
48
  }
30
49
  }
@@ -1,9 +1,57 @@
1
+ /**
2
+ * String `format` values accepted by OpenAI's strict Structured Outputs subset.
3
+ * Any other format (e.g. "uri", "uri-reference", "regex") causes the API to
4
+ * reject the whole request with `400 ... '<format>' is not a valid format`.
5
+ * MCP servers and hand-written tools routinely declare such formats, so we strip
6
+ * the unsupported ones before sending. See:
7
+ * https://platform.openai.com/docs/guides/structured-outputs#supported-properties
8
+ */
9
+ const SUPPORTED_STRING_FORMATS = new Set([
10
+ 'date-time',
11
+ 'time',
12
+ 'date',
13
+ 'duration',
14
+ 'email',
15
+ 'hostname',
16
+ 'ipv4',
17
+ 'ipv6',
18
+ 'uuid',
19
+ ])
20
+
21
+ /**
22
+ * Recursively drop JSON-Schema `format` keywords whose value isn't in OpenAI's
23
+ * strict-mode allowlist. Pure — returns a fresh tree and never mutates `node`,
24
+ * so the caller's original tool definition is left intact.
25
+ *
26
+ * A property *named* `format` always has a schema (object/boolean) value, never
27
+ * a bare string, so it is preserved and recursed into; only the `format`
28
+ * *keyword* (whose value is a string) is subject to removal.
29
+ */
30
+ function stripUnsupportedFormats(node: any): any {
31
+ if (Array.isArray(node)) return node.map(stripUnsupportedFormats)
32
+ if (node === null || typeof node !== 'object') return node
33
+
34
+ const out: Record<string, any> = {}
35
+ for (const [key, value] of Object.entries(node)) {
36
+ if (
37
+ key === 'format' &&
38
+ typeof value === 'string' &&
39
+ !SUPPORTED_STRING_FORMATS.has(value)
40
+ ) {
41
+ continue
42
+ }
43
+ out[key] = stripUnsupportedFormats(value)
44
+ }
45
+ return out
46
+ }
47
+
1
48
  /**
2
49
  * Transform a JSON schema to be compatible with OpenAI's structured output requirements.
3
50
  * OpenAI requires:
4
51
  * - All properties must be in the `required` array
5
52
  * - Optional fields should have null added to their type union
6
53
  * - additionalProperties must be false for objects
54
+ * - String `format` keywords must be from a fixed allowlist (others are stripped)
7
55
  *
8
56
  * @param schema - JSON schema to transform
9
57
  * @param originalRequired - Original required array (to know which fields were optional)
@@ -12,6 +60,18 @@
12
60
  export function makeStructuredOutputCompatible(
13
61
  schema: Record<string, any>,
14
62
  originalRequired?: Array<string>,
63
+ ): Record<string, any> {
64
+ return stripUnsupportedFormats(coerceStrictSchema(schema, originalRequired))
65
+ }
66
+
67
+ /**
68
+ * Strict-mode structural rewrite (required widening, nullability,
69
+ * additionalProperties). Kept private so the public entry point can apply the
70
+ * format-stripping pass exactly once over the fully-rewritten tree.
71
+ */
72
+ function coerceStrictSchema(
73
+ schema: Record<string, any>,
74
+ originalRequired?: Array<string>,
15
75
  ): Record<string, any> {
16
76
  const result = { ...schema }
17
77
  const required =
@@ -28,17 +88,14 @@ export function makeStructuredOutputCompatible(
28
88
 
29
89
  // Step 1: Recurse into nested structures
30
90
  if (prop.type === 'object' && prop.properties) {
31
- prop = makeStructuredOutputCompatible(prop, prop.required || [])
91
+ prop = coerceStrictSchema(prop, prop.required || [])
32
92
  } else if (prop.type === 'array' && prop.items) {
33
93
  prop = {
34
94
  ...prop,
35
- items: makeStructuredOutputCompatible(
36
- prop.items,
37
- prop.items.required || [],
38
- ),
95
+ items: coerceStrictSchema(prop.items, prop.items.required || []),
39
96
  }
40
97
  } else if (prop.anyOf) {
41
- prop = makeStructuredOutputCompatible(prop, prop.required || [])
98
+ prop = coerceStrictSchema(prop, prop.required || [])
42
99
  } else if (prop.oneOf) {
43
100
  throw new Error(
44
101
  'oneOf is not supported in OpenAI structured output schemas. Check the supported outputs here: https://platform.openai.com/docs/guides/structured-outputs#supported-types',
@@ -68,15 +125,12 @@ export function makeStructuredOutputCompatible(
68
125
  }
69
126
 
70
127
  if (result.type === 'array' && result.items) {
71
- result.items = makeStructuredOutputCompatible(
72
- result.items,
73
- result.items.required || [],
74
- )
128
+ result.items = coerceStrictSchema(result.items, result.items.required || [])
75
129
  }
76
130
 
77
131
  if (result.anyOf && Array.isArray(result.anyOf)) {
78
132
  result.anyOf = result.anyOf.map((variant) =>
79
- makeStructuredOutputCompatible(variant, variant.required || []),
133
+ coerceStrictSchema(variant, variant.required || []),
80
134
  )
81
135
  }
82
136