@tanstack/openai-base 0.8.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.
|
@@ -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 =
|
|
37
|
+
prop = coerceStrictSchema(prop, prop.required || []);
|
|
12
38
|
} else if (prop.type === "array" && prop.items) {
|
|
13
39
|
prop = {
|
|
14
40
|
...prop,
|
|
15
|
-
items:
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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.8.
|
|
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",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"@tanstack/ai-utils": "0.2.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@tanstack/ai": "^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.
|
|
48
|
+
"@tanstack/ai": "0.28.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build": "vite build",
|
|
@@ -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 =
|
|
91
|
+
prop = coerceStrictSchema(prop, prop.required || [])
|
|
32
92
|
} else if (prop.type === 'array' && prop.items) {
|
|
33
93
|
prop = {
|
|
34
94
|
...prop,
|
|
35
|
-
items:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
133
|
+
coerceStrictSchema(variant, variant.required || []),
|
|
80
134
|
)
|
|
81
135
|
}
|
|
82
136
|
|