@tanstack/ai 0.33.0 → 0.34.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.
- package/dist/esm/activities/chat/index.js +23 -17
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.d.ts +18 -0
- package/dist/esm/activities/chat/messages.js +63 -0
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +2 -2
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.d.ts +13 -0
- package/dist/esm/activities/chat/tools/schema-converter.js +61 -33
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/package.json +3 -2
- package/skills/ai-core/media-generation/SKILL.md +11 -4
- package/src/activities/chat/index.ts +93 -31
- package/src/activities/chat/messages.ts +103 -0
- package/src/activities/chat/stream/processor.ts +11 -3
- package/src/activities/chat/tools/schema-converter.ts +146 -93
|
@@ -20,74 +20,84 @@ function isStandardJSONSchema(schema) {
|
|
|
20
20
|
function isStandardSchema(schema) {
|
|
21
21
|
return isPropertyCarrier(schema) && "~standard" in schema && typeof schema["~standard"] === "object" && schema["~standard"] !== null && "version" in schema["~standard"] && schema["~standard"].version === 1 && "validate" in schema["~standard"] && typeof schema["~standard"].validate === "function";
|
|
22
22
|
}
|
|
23
|
+
function pruneMap(map) {
|
|
24
|
+
return Object.keys(map).length > 0 ? map : void 0;
|
|
25
|
+
}
|
|
23
26
|
function makeStructuredOutputCompatible(schema, originalRequired = []) {
|
|
24
27
|
const result = { ...schema };
|
|
28
|
+
const map = {};
|
|
25
29
|
if (result.type === "object" && result.properties) {
|
|
26
30
|
const properties = { ...result.properties };
|
|
27
31
|
const allPropertyNames = Object.keys(properties);
|
|
32
|
+
const propertyMaps = {};
|
|
28
33
|
for (const propName of allPropertyNames) {
|
|
29
34
|
const prop = properties[propName];
|
|
30
35
|
if (!prop) continue;
|
|
31
36
|
const wasOptional = !originalRequired.includes(propName);
|
|
37
|
+
let widenedHere = false;
|
|
38
|
+
let childMap;
|
|
32
39
|
if (prop.type === "object" && prop.properties) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
properties[propName] = wasOptional ? { ...transformed, type: ["object", "null"] } : transformed;
|
|
40
|
+
const nested = makeStructuredOutputCompatible(prop, prop.required || []);
|
|
41
|
+
properties[propName] = wasOptional ? { ...nested.schema, type: ["object", "null"] } : nested.schema;
|
|
42
|
+
widenedHere = wasOptional;
|
|
43
|
+
childMap = nested.nullWidening;
|
|
38
44
|
} else if (prop.type === "array" && prop.items) {
|
|
39
45
|
const items = Array.isArray(prop.items) ? prop.items[0] : prop.items;
|
|
40
|
-
const
|
|
46
|
+
const nestedItems = items ? makeStructuredOutputCompatible(items, items.required || []) : void 0;
|
|
47
|
+
properties[propName] = {
|
|
41
48
|
...prop,
|
|
42
|
-
items:
|
|
49
|
+
items: nestedItems ? nestedItems.schema : prop.items,
|
|
50
|
+
...wasOptional ? { type: ["array", "null"] } : {}
|
|
43
51
|
};
|
|
44
|
-
|
|
52
|
+
widenedHere = wasOptional;
|
|
53
|
+
childMap = nestedItems?.nullWidening ? { items: nestedItems.nullWidening } : void 0;
|
|
45
54
|
} else if (wasOptional) {
|
|
46
55
|
if (prop.type && !Array.isArray(prop.type)) {
|
|
47
|
-
properties[propName] = {
|
|
48
|
-
|
|
49
|
-
type: [prop.type, "null"]
|
|
50
|
-
};
|
|
56
|
+
properties[propName] = { ...prop, type: [prop.type, "null"] };
|
|
57
|
+
widenedHere = true;
|
|
51
58
|
} else if (Array.isArray(prop.type) && !prop.type.includes("null")) {
|
|
52
|
-
properties[propName] = {
|
|
53
|
-
|
|
54
|
-
type: [...prop.type, "null"]
|
|
55
|
-
};
|
|
59
|
+
properties[propName] = { ...prop, type: [...prop.type, "null"] };
|
|
60
|
+
widenedHere = true;
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
if (widenedHere || childMap) {
|
|
64
|
+
propertyMaps[propName] = {
|
|
65
|
+
...childMap ?? {},
|
|
66
|
+
...widenedHere ? { widened: true } : {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
58
69
|
}
|
|
59
70
|
result.properties = properties;
|
|
60
71
|
result.required = allPropertyNames;
|
|
61
72
|
result.additionalProperties = false;
|
|
73
|
+
if (Object.keys(propertyMaps).length > 0) map.properties = propertyMaps;
|
|
62
74
|
}
|
|
63
75
|
if (result.type === "array" && result.items) {
|
|
64
76
|
const items = Array.isArray(result.items) ? result.items[0] : result.items;
|
|
65
77
|
if (items) {
|
|
66
|
-
|
|
78
|
+
const nestedItems = makeStructuredOutputCompatible(
|
|
79
|
+
items,
|
|
80
|
+
items.required || []
|
|
81
|
+
);
|
|
82
|
+
result.items = nestedItems.schema;
|
|
83
|
+
if (nestedItems.nullWidening) map.items = nestedItems.nullWidening;
|
|
67
84
|
}
|
|
68
85
|
}
|
|
69
|
-
return result;
|
|
86
|
+
return { schema: result, nullWidening: pruneMap(map) };
|
|
70
87
|
}
|
|
71
|
-
function
|
|
72
|
-
if (!schema) return void 0;
|
|
73
|
-
const { forStructuredOutput = false } = options;
|
|
88
|
+
function toTypedJsonSchema(schema) {
|
|
74
89
|
if (isStandardJSONSchema(schema)) {
|
|
75
90
|
const jsonSchema = schema["~standard"].jsonSchema.input({
|
|
76
91
|
target: "draft-07"
|
|
77
92
|
});
|
|
78
|
-
|
|
79
|
-
if ("properties" in result && !result.type)
|
|
80
|
-
result.type = "object";
|
|
81
|
-
}
|
|
93
|
+
const result = toJsonSchema(jsonSchema);
|
|
94
|
+
if ("properties" in result && !result.type) result.type = "object";
|
|
82
95
|
if (result.type === "object" && !("properties" in result)) {
|
|
83
96
|
result.properties = {};
|
|
84
97
|
}
|
|
85
98
|
if (result.type === "object" && !("required" in result)) {
|
|
86
99
|
result.required = [];
|
|
87
100
|
}
|
|
88
|
-
if (forStructuredOutput) {
|
|
89
|
-
result = makeStructuredOutputCompatible(result, result.required || []);
|
|
90
|
-
}
|
|
91
101
|
return result;
|
|
92
102
|
}
|
|
93
103
|
if (isStandardSchema(schema)) {
|
|
@@ -95,14 +105,31 @@ function convertSchemaToJsonSchema(schema, options = {}) {
|
|
|
95
105
|
"Schema is a Standard Schema validator but does not expose a JSON Schema converter on `~standard.jsonSchema`. Use Zod v4.2+, ArkType v2.1.28+, or wrap a Valibot schema with `toStandardJsonSchema()` from `@valibot/to-json-schema` before passing it as `outputSchema`."
|
|
96
106
|
);
|
|
97
107
|
}
|
|
98
|
-
if (typeof schema !== "object")
|
|
108
|
+
if (typeof schema !== "object") return schema;
|
|
109
|
+
return toJsonSchema(schema);
|
|
110
|
+
}
|
|
111
|
+
function convertSchemaToJsonSchema(schema, options = {}) {
|
|
112
|
+
if (!schema) return void 0;
|
|
113
|
+
const { forStructuredOutput = false } = options;
|
|
114
|
+
if (!forStructuredOutput && !isStandardJSONSchema(schema) && !isStandardSchema(schema)) {
|
|
99
115
|
return schema;
|
|
100
116
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
const base = toTypedJsonSchema(schema);
|
|
118
|
+
if (!base || typeof base !== "object") return base;
|
|
119
|
+
if (!forStructuredOutput) return base;
|
|
120
|
+
return makeStructuredOutputCompatible(base, base.required || []).schema;
|
|
121
|
+
}
|
|
122
|
+
function convertSchemaForStructuredOutput(schema) {
|
|
123
|
+
if (!schema) return { jsonSchema: void 0, nullWideningMap: void 0 };
|
|
124
|
+
const base = toTypedJsonSchema(schema);
|
|
125
|
+
if (!base || typeof base !== "object") {
|
|
126
|
+
return { jsonSchema: base, nullWideningMap: void 0 };
|
|
104
127
|
}
|
|
105
|
-
|
|
128
|
+
const { schema: jsonSchema, nullWidening } = makeStructuredOutputCompatible(
|
|
129
|
+
base,
|
|
130
|
+
base.required || []
|
|
131
|
+
);
|
|
132
|
+
return { jsonSchema, nullWideningMap: nullWidening };
|
|
106
133
|
}
|
|
107
134
|
class StandardSchemaValidationError extends Error {
|
|
108
135
|
name = "StandardSchemaValidationError";
|
|
@@ -131,6 +158,7 @@ function parseWithStandardSchema(schema, data) {
|
|
|
131
158
|
}
|
|
132
159
|
export {
|
|
133
160
|
StandardSchemaValidationError,
|
|
161
|
+
convertSchemaForStructuredOutput,
|
|
134
162
|
convertSchemaToJsonSchema,
|
|
135
163
|
isStandardJSONSchema,
|
|
136
164
|
isStandardSchema,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-converter.js","sources":["../../../../../src/activities/chat/tools/schema-converter.ts"],"sourcesContent":["import type {\n StandardJSONSchemaV1,\n StandardSchemaV1,\n} from '@standard-schema/spec'\nimport type { JSONSchema, SchemaInput } from '../../../types'\n\n/**\n * Build a JSONSchema object from any plain key/value source. The `JSONSchema`\n * interface's `[key: string]: any` index signature makes every property\n * assignable through bracket access without a type cast — copying keys here\n * lets us narrow either `Record<string, unknown>` (returned by\n * `~standard.jsonSchema.input()`) or a `JSONSchema` (from the SchemaInput\n * pass-through arm) into the typed view used by the rest of this module.\n *\n * Accepts `object` so callers don't need a cast when narrowing from union\n * types like `SchemaInput`.\n */\nfunction toJsonSchema(obj: object): JSONSchema {\n const result: JSONSchema = {}\n for (const [key, value] of Object.entries(obj)) {\n if (key === '$schema') continue // not needed by LLM providers\n result[key] = value\n }\n return result\n}\n\n/**\n * Whether a value can carry a `~standard` property. Most schema libraries\n * (Zod, Valibot) return plain objects, but ArkType's `type()` returns a\n * *callable function* with `~standard` attached — so `typeof` must accept\n * both `'object'` and `'function'` or ArkType schemas are missed entirely\n * (issue #276).\n */\nfunction isPropertyCarrier(schema: unknown): schema is Record<string, unknown> {\n return (\n (typeof schema === 'object' || typeof schema === 'function') &&\n schema !== null\n )\n}\n\n/**\n * Check if a value is a Standard JSON Schema compliant schema.\n * Standard JSON Schema compliant libraries (Zod v4+, ArkType, Valibot with toStandardJsonSchema, etc.)\n * implement the '~standard' property with jsonSchema converter methods.\n */\nexport function isStandardJSONSchema(\n schema: unknown,\n): schema is StandardJSONSchemaV1 {\n if (!isPropertyCarrier(schema) || !('~standard' in schema)) return false\n\n const standard = schema['~standard']\n if (\n typeof standard !== 'object' ||\n standard === null ||\n !('version' in standard) ||\n standard.version !== 1 ||\n !('jsonSchema' in standard) ||\n typeof standard.jsonSchema !== 'object' ||\n standard.jsonSchema === null ||\n !('input' in standard.jsonSchema)\n ) {\n return false\n }\n\n return typeof standard.jsonSchema.input === 'function'\n}\n\n/**\n * Check if a value is a Standard Schema compliant schema (for validation).\n * Standard Schema compliant libraries implement the '~standard' property with a validate function.\n */\nexport function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {\n return (\n isPropertyCarrier(schema) &&\n '~standard' in schema &&\n typeof schema['~standard'] === 'object' &&\n schema['~standard'] !== null &&\n 'version' in schema['~standard'] &&\n schema['~standard'].version === 1 &&\n 'validate' in schema['~standard'] &&\n typeof schema['~standard'].validate === 'function'\n )\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 *\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 */\nfunction makeStructuredOutputCompatible(\n schema: JSONSchema,\n originalRequired: Array<string> = [],\n): JSONSchema {\n const result: JSONSchema = { ...schema }\n\n // Handle object types\n if (result.type === 'object' && result.properties) {\n const properties: Record<string, JSONSchema> = { ...result.properties }\n const allPropertyNames = Object.keys(properties)\n\n // Transform each property\n for (const propName of allPropertyNames) {\n const prop = properties[propName]\n if (!prop) continue\n const wasOptional = !originalRequired.includes(propName)\n\n // Recursively transform nested objects/arrays\n if (prop.type === 'object' && prop.properties) {\n const transformed = makeStructuredOutputCompatible(\n prop,\n prop.required || [],\n )\n properties[propName] = wasOptional\n ? { ...transformed, type: ['object', 'null'] }\n : transformed\n } else if (prop.type === 'array' && prop.items) {\n const items = Array.isArray(prop.items) ? prop.items[0] : prop.items\n const transformed: JSONSchema = {\n ...prop,\n items: items\n ? makeStructuredOutputCompatible(items, items.required || [])\n : prop.items,\n }\n properties[propName] = wasOptional\n ? { ...transformed, type: ['array', 'null'] }\n : transformed\n } else if (wasOptional) {\n // Make optional fields nullable by adding null to the type\n if (prop.type && !Array.isArray(prop.type)) {\n properties[propName] = {\n ...prop,\n type: [prop.type, 'null'],\n }\n } else if (Array.isArray(prop.type) && !prop.type.includes('null')) {\n properties[propName] = {\n ...prop,\n type: [...prop.type, 'null'],\n }\n }\n }\n }\n\n result.properties = properties\n // ALL properties must be required for OpenAI structured output\n result.required = allPropertyNames\n // additionalProperties must be false\n result.additionalProperties = false\n }\n\n // Handle array types with object items\n if (result.type === 'array' && result.items) {\n const items = Array.isArray(result.items) ? result.items[0] : result.items\n if (items) {\n result.items = makeStructuredOutputCompatible(items, items.required || [])\n }\n }\n\n return result\n}\n\n/**\n * Options for schema conversion\n */\nexport interface ConvertSchemaOptions {\n /**\n * When true, transforms the schema to be compatible with OpenAI's structured output requirements:\n * - All properties are added to the `required` array\n * - Optional fields get null added to their type union\n * - additionalProperties is set to false for all objects\n *\n * @default false\n */\n forStructuredOutput?: boolean\n}\n\n/**\n * Converts a Standard JSON Schema compliant schema or plain JSONSchema to JSON Schema format\n * compatible with LLM providers.\n *\n * Supports any schema library that implements the Standard JSON Schema spec (v1):\n * - Zod v4+ (natively supports StandardJSONSchemaV1)\n * - ArkType (natively supports StandardJSONSchemaV1)\n * - Valibot (via `toStandardJsonSchema()` from `@valibot/to-json-schema`)\n *\n * If the input is already a plain JSONSchema object, it is returned as-is.\n *\n * @param schema - Standard JSON Schema compliant schema or plain JSONSchema object to convert\n * @param options - Conversion options\n * @returns JSON Schema object that can be sent to LLM providers\n *\n * @example\n * ```typescript\n * // Using Zod v4+ (natively supports Standard JSON Schema)\n * import * as z from 'zod';\n *\n * const zodSchema = z.object({\n * location: z.string().describe('City name'),\n * unit: z.enum(['celsius', 'fahrenheit']).optional()\n * });\n *\n * const jsonSchema = convertSchemaToJsonSchema(zodSchema);\n *\n * @example\n * // Using ArkType (natively supports Standard JSON Schema)\n * import { type } from 'arktype';\n *\n * const arkSchema = type({\n * location: 'string',\n * unit: \"'celsius' | 'fahrenheit'\"\n * });\n *\n * const jsonSchema = convertSchemaToJsonSchema(arkSchema);\n *\n * @example\n * // Using Valibot (via toStandardJsonSchema)\n * import * as v from 'valibot';\n * import { toStandardJsonSchema } from '@valibot/to-json-schema';\n *\n * const valibotSchema = toStandardJsonSchema(v.object({\n * location: v.string(),\n * unit: v.optional(v.picklist(['celsius', 'fahrenheit']))\n * }));\n *\n * const jsonSchema = convertSchemaToJsonSchema(valibotSchema);\n *\n * @example\n * // Using JSONSchema directly (passes through unchanged)\n * const rawSchema = {\n * type: 'object',\n * properties: { location: { type: 'string' } },\n * required: ['location']\n * };\n * const result = convertSchemaToJsonSchema(rawSchema);\n * ```\n */\nexport function convertSchemaToJsonSchema(\n schema: SchemaInput | undefined,\n options: ConvertSchemaOptions = {},\n): JSONSchema | undefined {\n if (!schema) return undefined\n\n const { forStructuredOutput = false } = options\n\n // If it's a Standard JSON Schema compliant schema, use the standard interface\n if (isStandardJSONSchema(schema)) {\n const jsonSchema = schema['~standard'].jsonSchema.input({\n target: 'draft-07',\n })\n\n // Rebuild structurally so the typed JSONSchema view is acquired without\n // a `Record<string, unknown> as JSONSchema` cast; `toJsonSchema()` also\n // drops the `$schema` key which LLM providers don't need.\n let result: JSONSchema = toJsonSchema(jsonSchema)\n\n // Ensure object schemas always have type: \"object\"\n // If it has properties (even empty), it should be an object type\n if ('properties' in result && !result.type) {\n result.type = 'object'\n }\n\n // Ensure properties exists for object types (even if empty)\n if (result.type === 'object' && !('properties' in result)) {\n result.properties = {}\n }\n\n // Ensure required exists for object types (even if empty array)\n if (result.type === 'object' && !('required' in result)) {\n result.required = []\n }\n\n // Apply structured output transformation if requested\n if (forStructuredOutput) {\n result = makeStructuredOutputCompatible(result, result.required || [])\n }\n\n return result\n }\n\n // Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't\n // expose a `~standard.jsonSchema` converter. These would otherwise fall\n // through to the JSONSchema pass-through below and ship `{ '~standard': … }`\n // straight to the LLM provider, producing an opaque downstream error. Fail\n // fast with actionable guidance instead.\n if (isStandardSchema(schema)) {\n throw new Error(\n 'Schema is a Standard Schema validator but does not expose a JSON Schema ' +\n 'converter on `~standard.jsonSchema`. Use Zod v4.2+, ArkType v2.1.28+, ' +\n 'or wrap a Valibot schema with `toStandardJsonSchema()` from ' +\n '`@valibot/to-json-schema` before passing it as `outputSchema`.',\n )\n }\n\n // If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through\n // Still apply structured output transformation if requested\n\n // At this branch, `schema` is the plain `JSONSchema` arm of `SchemaInput`\n // (the two `~standard` arms were handled above). When no transformation\n // is requested we pass the schema through by reference to preserve\n // identity for callers that compare via `===`.\n if (typeof schema !== 'object') {\n // The SchemaInput union is object-shaped on every arm; if we ever hit a\n // non-object here, propagate it untouched and let the downstream\n // provider error loudly rather than silently widen.\n return schema\n }\n\n if (forStructuredOutput) {\n // Build a typed view structurally so we don't need a SchemaInput→JSONSchema\n // cast on the transformation path.\n const typedView = toJsonSchema(schema)\n return makeStructuredOutputCompatible(typedView, typedView.required || [])\n }\n\n return schema\n}\n\n/**\n * Validates data against a Standard Schema compliant schema.\n *\n * @param schema - Standard Schema compliant schema\n * @param data - Data to validate\n * @returns Validation result with success status, data or issues\n */\nexport async function validateWithStandardSchema<T>(\n schema: unknown,\n data: unknown,\n): Promise<\n | { success: true; data: T }\n | {\n success: false\n issues: Array<{ message: string; path?: Array<string> | undefined }>\n }\n> {\n if (!isStandardSchema(schema)) {\n // If it's not a Standard Schema, just return the data as-is\n return { success: true, data: data as T }\n }\n\n const result = await schema['~standard'].validate(data)\n\n if (!result.issues) {\n return { success: true, data: result.value as T }\n }\n\n return {\n success: false,\n issues: result.issues.map((issue) => ({\n message: issue.message || 'Validation failed',\n path: issue.path?.map(String),\n })),\n }\n}\n\n/**\n * Error thrown when Standard Schema validation fails. Carries the original\n * `issues` array so consumers (middleware `onError`, callers catching from\n * `chat({ outputSchema })`) can programmatically inspect each failure.\n */\nexport class StandardSchemaValidationError extends Error {\n override readonly name = 'StandardSchemaValidationError'\n readonly issues: ReadonlyArray<StandardSchemaV1.Issue>\n\n constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>) {\n super(\n `Validation failed: ${issues\n .map((i) => i.message || 'Validation failed')\n .join(', ')}`,\n )\n this.issues = issues\n }\n}\n\n/**\n * Synchronously validates data against a Standard Schema compliant schema.\n * Note: Some Standard Schema implementations may only support async validation.\n * In those cases, this function will throw.\n *\n * @param schema - Standard Schema compliant schema\n * @param data - Data to validate\n * @returns Parsed/validated data\n * @throws StandardSchemaValidationError if validation fails; Error if the\n * schema only supports async validation.\n */\nexport function parseWithStandardSchema<T>(schema: unknown, data: unknown): T {\n if (!isStandardSchema(schema)) {\n // If it's not a Standard Schema, just return the data as-is\n return data as T\n }\n\n const result = schema['~standard'].validate(data)\n\n // Handle async result (Promise)\n if (result instanceof Promise) {\n throw new Error(\n 'Schema validation returned a Promise. Use validateWithStandardSchema for async validation.',\n )\n }\n // Standard Schema validation returns { value } for success or { issues } for failure\n if (!result.issues) {\n return result.value as T\n }\n\n throw new StandardSchemaValidationError(result.issues)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"schema-converter.js","sources":["../../../../../src/activities/chat/tools/schema-converter.ts"],"sourcesContent":["import type {\n StandardJSONSchemaV1,\n StandardSchemaV1,\n} from '@standard-schema/spec'\nimport type { NullWideningMap } from '@tanstack/ai-utils'\nimport type { JSONSchema, SchemaInput } from '../../../types'\n\n/**\n * Build a JSONSchema object from any plain key/value source. The `JSONSchema`\n * interface's `[key: string]: any` index signature makes every property\n * assignable through bracket access without a type cast — copying keys here\n * lets us narrow either `Record<string, unknown>` (returned by\n * `~standard.jsonSchema.input()`) or a `JSONSchema` (from the SchemaInput\n * pass-through arm) into the typed view used by the rest of this module.\n *\n * Accepts `object` so callers don't need a cast when narrowing from union\n * types like `SchemaInput`.\n */\nfunction toJsonSchema(obj: object): JSONSchema {\n const result: JSONSchema = {}\n for (const [key, value] of Object.entries(obj)) {\n if (key === '$schema') continue // not needed by LLM providers\n result[key] = value\n }\n return result\n}\n\n/**\n * Whether a value can carry a `~standard` property. Most schema libraries\n * (Zod, Valibot) return plain objects, but ArkType's `type()` returns a\n * *callable function* with `~standard` attached — so `typeof` must accept\n * both `'object'` and `'function'` or ArkType schemas are missed entirely\n * (issue #276).\n */\nfunction isPropertyCarrier(schema: unknown): schema is Record<string, unknown> {\n return (\n (typeof schema === 'object' || typeof schema === 'function') &&\n schema !== null\n )\n}\n\n/**\n * Check if a value is a Standard JSON Schema compliant schema.\n * Standard JSON Schema compliant libraries (Zod v4+, ArkType, Valibot with toStandardJsonSchema, etc.)\n * implement the '~standard' property with jsonSchema converter methods.\n */\nexport function isStandardJSONSchema(\n schema: unknown,\n): schema is StandardJSONSchemaV1 {\n if (!isPropertyCarrier(schema) || !('~standard' in schema)) return false\n\n const standard = schema['~standard']\n if (\n typeof standard !== 'object' ||\n standard === null ||\n !('version' in standard) ||\n standard.version !== 1 ||\n !('jsonSchema' in standard) ||\n typeof standard.jsonSchema !== 'object' ||\n standard.jsonSchema === null ||\n !('input' in standard.jsonSchema)\n ) {\n return false\n }\n\n return typeof standard.jsonSchema.input === 'function'\n}\n\n/**\n * Check if a value is a Standard Schema compliant schema (for validation).\n * Standard Schema compliant libraries implement the '~standard' property with a validate function.\n */\nexport function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {\n return (\n isPropertyCarrier(schema) &&\n '~standard' in schema &&\n typeof schema['~standard'] === 'object' &&\n schema['~standard'] !== null &&\n 'version' in schema['~standard'] &&\n schema['~standard'].version === 1 &&\n 'validate' in schema['~standard'] &&\n typeof schema['~standard'].validate === 'function'\n )\n}\n\n/**\n * Result of {@link makeStructuredOutputCompatible}: the strict-ready schema plus\n * a {@link NullWideningMap} recording every position where a `null` was\n * synthesized, so the response can be un-widened before validation without\n * re-deriving (or guessing) which nulls were synthetic.\n */\ninterface StructuredOutputConversion {\n schema: JSONSchema\n nullWidening: NullWideningMap | undefined\n}\n\n/** Drop an empty map to `undefined` so leaf/no-op subtrees don't litter it. */\nfunction pruneMap(map: NullWideningMap): NullWideningMap | undefined {\n return Object.keys(map).length > 0 ? map : undefined\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 *\n * Alongside the transformed schema it returns a {@link NullWideningMap} marking\n * exactly the positions where `null` was added, so `undoNullWidening` can strip\n * those synthesized nulls (and only those) from the provider's response.\n *\n * @param schema - JSON schema to transform\n * @param originalRequired - Original required array (to know which fields were optional)\n * @returns Transformed schema + the null-widening map for the round trip\n */\nfunction makeStructuredOutputCompatible(\n schema: JSONSchema,\n originalRequired: Array<string> = [],\n): StructuredOutputConversion {\n const result: JSONSchema = { ...schema }\n const map: NullWideningMap = {}\n\n // Handle object types\n if (result.type === 'object' && result.properties) {\n const properties: Record<string, JSONSchema> = { ...result.properties }\n const allPropertyNames = Object.keys(properties)\n const propertyMaps: Record<string, NullWideningMap> = {}\n\n // Transform each property\n for (const propName of allPropertyNames) {\n const prop = properties[propName]\n if (!prop) continue\n const wasOptional = !originalRequired.includes(propName)\n // `null` synthesized AT this property (the field itself can come back null).\n let widenedHere = false\n // Map describing widened positions INSIDE this property.\n let childMap: NullWideningMap | undefined\n\n // Recursively transform nested objects/arrays\n if (prop.type === 'object' && prop.properties) {\n const nested = makeStructuredOutputCompatible(prop, prop.required || [])\n properties[propName] = wasOptional\n ? { ...nested.schema, type: ['object', 'null'] }\n : nested.schema\n widenedHere = wasOptional\n childMap = nested.nullWidening\n } else if (prop.type === 'array' && prop.items) {\n const items = Array.isArray(prop.items) ? prop.items[0] : prop.items\n const nestedItems = items\n ? makeStructuredOutputCompatible(items, items.required || [])\n : undefined\n properties[propName] = {\n ...prop,\n items: nestedItems ? nestedItems.schema : prop.items,\n ...(wasOptional ? { type: ['array', 'null'] } : {}),\n }\n widenedHere = wasOptional\n childMap = nestedItems?.nullWidening\n ? { items: nestedItems.nullWidening }\n : undefined\n } else if (wasOptional) {\n // Make optional fields nullable by adding null to the type. Mark\n // `widenedHere` only where we actually add `null`; a field already\n // typed nullable (`.nullish()`) is left as-is and keeps its null.\n if (prop.type && !Array.isArray(prop.type)) {\n properties[propName] = { ...prop, type: [prop.type, 'null'] }\n widenedHere = true\n } else if (Array.isArray(prop.type) && !prop.type.includes('null')) {\n properties[propName] = { ...prop, type: [...prop.type, 'null'] }\n widenedHere = true\n }\n }\n\n if (widenedHere || childMap) {\n propertyMaps[propName] = {\n ...(childMap ?? {}),\n ...(widenedHere ? { widened: true } : {}),\n }\n }\n }\n\n result.properties = properties\n // ALL properties must be required for OpenAI structured output\n result.required = allPropertyNames\n // additionalProperties must be false\n result.additionalProperties = false\n if (Object.keys(propertyMaps).length > 0) map.properties = propertyMaps\n }\n\n // Handle array types with object items\n if (result.type === 'array' && result.items) {\n const items = Array.isArray(result.items) ? result.items[0] : result.items\n if (items) {\n const nestedItems = makeStructuredOutputCompatible(\n items,\n items.required || [],\n )\n result.items = nestedItems.schema\n if (nestedItems.nullWidening) map.items = nestedItems.nullWidening\n }\n }\n\n return { schema: result, nullWidening: pruneMap(map) }\n}\n\n/**\n * Options for schema conversion\n */\nexport interface ConvertSchemaOptions {\n /**\n * When true, transforms the schema to be compatible with OpenAI's structured output requirements:\n * - All properties are added to the `required` array\n * - Optional fields get null added to their type union\n * - additionalProperties is set to false for all objects\n *\n * @default false\n */\n forStructuredOutput?: boolean\n}\n\n/**\n * Normalize any supported schema input to a typed, UN-widened `JSONSchema` —\n * the shared first half of conversion, before any structured-output widening.\n *\n * - Standard JSON Schemas are rebuilt structurally (dropping `$schema`, which\n * LLM providers ignore) and given the explicit `type`/`properties`/`required`\n * defaults object shapes need downstream.\n * - Plain `JSONSchema` inputs are rebuilt into the typed view; non-object inputs\n * are surfaced untouched (they can't be widened).\n * - Standard Schema validators lacking a `~standard.jsonSchema` converter throw\n * with actionable guidance, rather than shipping `{ '~standard': … }` to the\n * provider and producing an opaque downstream error.\n */\nfunction toTypedJsonSchema(schema: SchemaInput): JSONSchema | undefined {\n if (isStandardJSONSchema(schema)) {\n const jsonSchema = schema['~standard'].jsonSchema.input({\n target: 'draft-07',\n })\n const result: JSONSchema = toJsonSchema(jsonSchema)\n if ('properties' in result && !result.type) result.type = 'object'\n if (result.type === 'object' && !('properties' in result)) {\n result.properties = {}\n }\n if (result.type === 'object' && !('required' in result)) {\n result.required = []\n }\n return result\n }\n\n if (isStandardSchema(schema)) {\n throw new Error(\n 'Schema is a Standard Schema validator but does not expose a JSON Schema ' +\n 'converter on `~standard.jsonSchema`. Use Zod v4.2+, ArkType v2.1.28+, ' +\n 'or wrap a Valibot schema with `toStandardJsonSchema()` from ' +\n '`@valibot/to-json-schema` before passing it as `outputSchema`.',\n )\n }\n\n if (typeof schema !== 'object') return schema\n return toJsonSchema(schema)\n}\n\n/**\n * Converts a Standard JSON Schema compliant schema or plain JSONSchema to JSON Schema format\n * compatible with LLM providers.\n *\n * Supports any schema library that implements the Standard JSON Schema spec (v1):\n * - Zod v4+ (natively supports StandardJSONSchemaV1)\n * - ArkType (natively supports StandardJSONSchemaV1)\n * - Valibot (via `toStandardJsonSchema()` from `@valibot/to-json-schema`)\n *\n * If the input is already a plain JSONSchema object, it is returned as-is.\n *\n * @param schema - Standard JSON Schema compliant schema or plain JSONSchema object to convert\n * @param options - Conversion options\n * @returns JSON Schema object that can be sent to LLM providers\n *\n * @example\n * ```typescript\n * // Using Zod v4+ (natively supports Standard JSON Schema)\n * import * as z from 'zod';\n *\n * const zodSchema = z.object({\n * location: z.string().describe('City name'),\n * unit: z.enum(['celsius', 'fahrenheit']).optional()\n * });\n *\n * const jsonSchema = convertSchemaToJsonSchema(zodSchema);\n *\n * @example\n * // Using ArkType (natively supports Standard JSON Schema)\n * import { type } from 'arktype';\n *\n * const arkSchema = type({\n * location: 'string',\n * unit: \"'celsius' | 'fahrenheit'\"\n * });\n *\n * const jsonSchema = convertSchemaToJsonSchema(arkSchema);\n *\n * @example\n * // Using Valibot (via toStandardJsonSchema)\n * import * as v from 'valibot';\n * import { toStandardJsonSchema } from '@valibot/to-json-schema';\n *\n * const valibotSchema = toStandardJsonSchema(v.object({\n * location: v.string(),\n * unit: v.optional(v.picklist(['celsius', 'fahrenheit']))\n * }));\n *\n * const jsonSchema = convertSchemaToJsonSchema(valibotSchema);\n *\n * @example\n * // Using JSONSchema directly (passes through unchanged)\n * const rawSchema = {\n * type: 'object',\n * properties: { location: { type: 'string' } },\n * required: ['location']\n * };\n * const result = convertSchemaToJsonSchema(rawSchema);\n * ```\n */\nexport function convertSchemaToJsonSchema(\n schema: SchemaInput | undefined,\n options: ConvertSchemaOptions = {},\n): JSONSchema | undefined {\n if (!schema) return undefined\n\n const { forStructuredOutput = false } = options\n\n // Plain-JSONSchema passthrough: with no widening requested, return the schema\n // by reference so callers comparing via `===` keep identity. Only the widening\n // path needs the rebuilt, normalized view from `toTypedJsonSchema`.\n if (\n !forStructuredOutput &&\n !isStandardJSONSchema(schema) &&\n !isStandardSchema(schema)\n ) {\n return schema\n }\n\n const base = toTypedJsonSchema(schema)\n // Non-object inputs can't be widened; surface them untouched.\n if (!base || typeof base !== 'object') return base\n if (!forStructuredOutput) return base\n return makeStructuredOutputCompatible(base, base.required || []).schema\n}\n\n/**\n * Convert a schema for structured output AND capture the {@link NullWideningMap}\n * recording every `null` the strict-mode widening synthesized. The map lets the\n * caller undo that widening on the provider's response (via `undoNullWidening`)\n * before validating against the original schema — optional fields read back as\n * absent while genuine `.nullable()` nulls survive. The map is `undefined` when\n * the schema isn't a widenable object or when no field needed widening.\n */\nexport function convertSchemaForStructuredOutput(\n schema: SchemaInput | undefined,\n): {\n jsonSchema: JSONSchema | undefined\n nullWideningMap: NullWideningMap | undefined\n} {\n if (!schema) return { jsonSchema: undefined, nullWideningMap: undefined }\n const base = toTypedJsonSchema(schema)\n if (!base || typeof base !== 'object') {\n return { jsonSchema: base, nullWideningMap: undefined }\n }\n const { schema: jsonSchema, nullWidening } = makeStructuredOutputCompatible(\n base,\n base.required || [],\n )\n return { jsonSchema, nullWideningMap: nullWidening }\n}\n\n/**\n * Validates data against a Standard Schema compliant schema.\n *\n * @param schema - Standard Schema compliant schema\n * @param data - Data to validate\n * @returns Validation result with success status, data or issues\n */\nexport async function validateWithStandardSchema<T>(\n schema: unknown,\n data: unknown,\n): Promise<\n | { success: true; data: T }\n | {\n success: false\n issues: Array<{ message: string; path?: Array<string> | undefined }>\n }\n> {\n if (!isStandardSchema(schema)) {\n // If it's not a Standard Schema, just return the data as-is\n return { success: true, data: data as T }\n }\n\n const result = await schema['~standard'].validate(data)\n\n if (!result.issues) {\n return { success: true, data: result.value as T }\n }\n\n return {\n success: false,\n issues: result.issues.map((issue) => ({\n message: issue.message || 'Validation failed',\n path: issue.path?.map(String),\n })),\n }\n}\n\n/**\n * Error thrown when Standard Schema validation fails. Carries the original\n * `issues` array so consumers (middleware `onError`, callers catching from\n * `chat({ outputSchema })`) can programmatically inspect each failure.\n */\nexport class StandardSchemaValidationError extends Error {\n override readonly name = 'StandardSchemaValidationError'\n readonly issues: ReadonlyArray<StandardSchemaV1.Issue>\n\n constructor(issues: ReadonlyArray<StandardSchemaV1.Issue>) {\n super(\n `Validation failed: ${issues\n .map((i) => i.message || 'Validation failed')\n .join(', ')}`,\n )\n this.issues = issues\n }\n}\n\n/**\n * Synchronously validates data against a Standard Schema compliant schema.\n * Note: Some Standard Schema implementations may only support async validation.\n * In those cases, this function will throw.\n *\n * @param schema - Standard Schema compliant schema\n * @param data - Data to validate\n * @returns Parsed/validated data\n * @throws StandardSchemaValidationError if validation fails; Error if the\n * schema only supports async validation.\n */\nexport function parseWithStandardSchema<T>(schema: unknown, data: unknown): T {\n if (!isStandardSchema(schema)) {\n // If it's not a Standard Schema, just return the data as-is\n return data as T\n }\n\n const result = schema['~standard'].validate(data)\n\n // Handle async result (Promise)\n if (result instanceof Promise) {\n throw new Error(\n 'Schema validation returned a Promise. Use validateWithStandardSchema for async validation.',\n )\n }\n // Standard Schema validation returns { value } for success or { issues } for failure\n if (!result.issues) {\n return result.value as T\n }\n\n throw new StandardSchemaValidationError(result.issues)\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
4
4
|
"description": "Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,7 +76,8 @@
|
|
|
76
76
|
"@ag-ui/core": "^0.0.52",
|
|
77
77
|
"@standard-schema/spec": "^1.1.0",
|
|
78
78
|
"partial-json": "^0.1.7",
|
|
79
|
-
"@tanstack/ai-event-client": "0.6.
|
|
79
|
+
"@tanstack/ai-event-client": "0.6.6",
|
|
80
|
+
"@tanstack/ai-utils": "0.3.0"
|
|
80
81
|
},
|
|
81
82
|
"peerDependencies": {
|
|
82
83
|
"@opentelemetry/api": ">=1.9.0"
|
|
@@ -3,10 +3,10 @@ name: ai-core/media-generation
|
|
|
3
3
|
description: >
|
|
4
4
|
Image, audio, video, speech (TTS), and transcription generation using
|
|
5
5
|
activity-specific adapters: generateImage() with openaiImage/geminiImage,
|
|
6
|
-
generateAudio() with geminiAudio/falAudio, generateVideo() with
|
|
7
|
-
openaiVideo/geminiVideo
|
|
8
|
-
generateSpeech() with openaiSpeech, generateTranscription()
|
|
9
|
-
openaiTranscription. React hooks: useGenerateImage, useGenerateAudio,
|
|
6
|
+
generateAudio() with geminiAudio/falAudio, generateVideo() with async
|
|
7
|
+
polling (openaiVideo/geminiVideo/grokVideo/falVideo, per-model typed
|
|
8
|
+
durations), generateSpeech() with openaiSpeech, generateTranscription()
|
|
9
|
+
with openaiTranscription. React hooks: useGenerateImage, useGenerateAudio,
|
|
10
10
|
useGenerateSpeech, useTranscription, useGenerateVideo.
|
|
11
11
|
TanStack Start server function integration with toServerSentEventsResponse.
|
|
12
12
|
type: sub-skill
|
|
@@ -454,6 +454,13 @@ const { jobId } = await generateVideo({
|
|
|
454
454
|
// (x-goog-api-key header or ?key= query parameter).
|
|
455
455
|
```
|
|
456
456
|
|
|
457
|
+
Other video adapters: `openaiVideo('sora-2')` (pixel sizes like `'1280x720'`,
|
|
458
|
+
durations 4/8/12s, single `input_reference` image prompt part), `grokVideo(...)`
|
|
459
|
+
(`grok-imagine-video` does text-to-video + image-to-video; `grok-imagine-video-1.5` is
|
|
460
|
+
image-to-video only — needs an `image` prompt part as the starting frame, text-only throws;
|
|
461
|
+
aspect-ratio size template like `'16:9_720p'`, integer durations 1-15s, reports
|
|
462
|
+
`usage.unitsBilled` seconds and exact `usage.cost`), and `falVideo(...)` (hosted models, see cost tracking below).
|
|
463
|
+
|
|
457
464
|
Client hook with job tracking:
|
|
458
465
|
|
|
459
466
|
```tsx
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { devtoolsMiddleware } from '@tanstack/ai-event-client'
|
|
9
|
+
import { undoNullWidening } from '@tanstack/ai-utils'
|
|
9
10
|
import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
|
|
10
11
|
import { streamToText } from '../../stream-to-response.js'
|
|
11
12
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
executeToolCalls,
|
|
19
20
|
} from './tools/tool-calls'
|
|
20
21
|
import {
|
|
22
|
+
convertSchemaForStructuredOutput,
|
|
21
23
|
convertSchemaToJsonSchema,
|
|
22
24
|
isStandardSchema,
|
|
23
25
|
parseWithStandardSchema,
|
|
@@ -422,11 +424,21 @@ interface TextEngineConfig<
|
|
|
422
424
|
* (used by runStreamingStructuredOutput). When false, chunks are
|
|
423
425
|
* consumed internally for middleware visibility but not yielded
|
|
424
426
|
* (used by runAgenticStructuredOutput).
|
|
425
|
-
* -
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* `
|
|
429
|
-
*
|
|
427
|
+
* - normalize: optional schema-aware transform applied to the captured
|
|
428
|
+
* structured-output object the moment it enters the engine — BEFORE it is
|
|
429
|
+
* stored, validated, or yielded. Used to undo strict-mode null-widening
|
|
430
|
+
* (`undoNullWidening`): strict schemas widen optional fields to
|
|
431
|
+
* `required` + nullable so the provider returns `null` for an absent
|
|
432
|
+
* optional, and this strips exactly those synthesized nulls while keeping
|
|
433
|
+
* the ones a `.nullable()` field genuinely allows. Applied here (not in
|
|
434
|
+
* the adapter) because the engine is the only layer holding the original
|
|
435
|
+
* schema's null-widening map, and applying it at capture fixes BOTH the
|
|
436
|
+
* streaming chunk and the Promise<T> result with one transform.
|
|
437
|
+
* - validate: optional callback invoked AFTER `normalize` and AFTER the
|
|
438
|
+
* structured-output result is captured, but BEFORE the terminal hook
|
|
439
|
+
* fires. If it throws, the engine records a `finalizationError` and fires
|
|
440
|
+
* `onError` instead of `onFinish` (per spec §7.3). On success, the
|
|
441
|
+
* returned value is stored as the validated result and retrievable via
|
|
430
442
|
* `getValidatedStructuredOutput()`. Used by `runAgenticStructuredOutput`
|
|
431
443
|
* to perform Standard Schema validation inside the engine.
|
|
432
444
|
* - nativeCombined: when true, the adapter declared
|
|
@@ -441,6 +453,7 @@ interface TextEngineConfig<
|
|
|
441
453
|
finalStructuredOutput?: {
|
|
442
454
|
jsonSchema: JSONSchema
|
|
443
455
|
yieldChunks: boolean
|
|
456
|
+
normalize?: (data: unknown) => unknown
|
|
444
457
|
validate?: (data: unknown) => unknown
|
|
445
458
|
nativeCombined?: boolean
|
|
446
459
|
}
|
|
@@ -546,8 +559,8 @@ class TextEngine<
|
|
|
546
559
|
// to carry, so the client matches it to the streaming text deltas.
|
|
547
560
|
private combinedStructuredMessageId: string | null = null
|
|
548
561
|
// Holds the validated value when `finalStructuredOutput.validate` is provided
|
|
549
|
-
// and succeeds. Distinct from `structuredOutputResult.data` (the
|
|
550
|
-
// unvalidated payload from the structured-output.complete chunk).
|
|
562
|
+
// and succeeds. Distinct from `structuredOutputResult.data` (the normalized
|
|
563
|
+
// but unvalidated payload from the structured-output.complete chunk).
|
|
551
564
|
private validatedStructuredOutput: unknown = undefined
|
|
552
565
|
private hasValidatedStructuredOutput = false
|
|
553
566
|
private finalizationError: {
|
|
@@ -558,6 +571,7 @@ class TextEngine<
|
|
|
558
571
|
private readonly finalStructuredOutput?: {
|
|
559
572
|
jsonSchema: JSONSchema
|
|
560
573
|
yieldChunks: boolean
|
|
574
|
+
normalize?: (data: unknown) => unknown
|
|
561
575
|
validate?: (data: unknown) => unknown
|
|
562
576
|
nativeCombined?: boolean
|
|
563
577
|
}
|
|
@@ -2087,15 +2101,29 @@ class TextEngine<
|
|
|
2087
2101
|
// All narrowing below is via the discriminated-union `chunk.type`
|
|
2088
2102
|
// — no `as` casts.
|
|
2089
2103
|
|
|
2104
|
+
// The chunk forwarded to middleware/consumers. Replaced below only for
|
|
2105
|
+
// the structured-output.complete event, whose `object` we normalize
|
|
2106
|
+
// (un-widen) so streaming consumers see the same cleaned payload the
|
|
2107
|
+
// Promise<T> path validates and returns.
|
|
2108
|
+
let outboundChunk: StreamChunk = chunk
|
|
2109
|
+
|
|
2090
2110
|
if (
|
|
2091
2111
|
chunk.type === EventType.CUSTOM &&
|
|
2092
2112
|
chunk.name === 'structured-output.complete'
|
|
2093
2113
|
) {
|
|
2094
2114
|
const parsed = readStructuredOutputCompleteValue(chunk.value)
|
|
2095
2115
|
if (parsed) {
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2116
|
+
const object = this.finalStructuredOutput.normalize
|
|
2117
|
+
? this.finalStructuredOutput.normalize(parsed.object)
|
|
2118
|
+
: parsed.object
|
|
2119
|
+
this.structuredOutputResult = { data: object, rawText: parsed.raw }
|
|
2120
|
+
// Rewrite the outbound event so the yielded chunk carries the
|
|
2121
|
+
// normalized object (the original `chunk.value` still holds the
|
|
2122
|
+
// widened one). Preserve every other field — `raw`, `reasoning` —
|
|
2123
|
+
// by spreading the original value.
|
|
2124
|
+
const value = chunk.value
|
|
2125
|
+
if (object !== parsed.object && value && typeof value === 'object') {
|
|
2126
|
+
outboundChunk = { ...chunk, value: { ...value, object } }
|
|
2099
2127
|
}
|
|
2100
2128
|
}
|
|
2101
2129
|
}
|
|
@@ -2119,7 +2147,7 @@ class TextEngine<
|
|
|
2119
2147
|
// 7b. Pipe through middleware
|
|
2120
2148
|
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
2121
2149
|
this.middlewareCtx,
|
|
2122
|
-
|
|
2150
|
+
outboundChunk,
|
|
2123
2151
|
)
|
|
2124
2152
|
|
|
2125
2153
|
// 7c. Decide consumer visibility — only yieldChunks=true callers get them.
|
|
@@ -2276,7 +2304,14 @@ class TextEngine<
|
|
|
2276
2304
|
} else {
|
|
2277
2305
|
try {
|
|
2278
2306
|
const parsed: unknown = JSON.parse(rawText)
|
|
2279
|
-
|
|
2307
|
+
// Normalize (un-widen) before storing so the synthesized
|
|
2308
|
+
// structured-output.complete chunk and the Promise<T> result both
|
|
2309
|
+
// carry the cleaned payload. JSON.parse preserves provider nulls, so
|
|
2310
|
+
// this is where native-combined output gets its widening undone.
|
|
2311
|
+
const data = this.finalStructuredOutput.normalize
|
|
2312
|
+
? this.finalStructuredOutput.normalize(parsed)
|
|
2313
|
+
: parsed
|
|
2314
|
+
this.structuredOutputResult = { data, rawText }
|
|
2280
2315
|
} catch (err: unknown) {
|
|
2281
2316
|
const detail =
|
|
2282
2317
|
rawText.slice(0, 200) + (rawText.length > 200 ? '...' : '')
|
|
@@ -2733,18 +2768,31 @@ async function runAgenticStructuredOutput<
|
|
|
2733
2768
|
|
|
2734
2769
|
// Same strict-conversion as the streaming path (`forStructuredOutput: true`)
|
|
2735
2770
|
// so the same Zod schema produces the same JSON Schema regardless of
|
|
2736
|
-
// stream mode — Promise<T> and stream:true must not diverge here.
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2771
|
+
// stream mode — Promise<T> and stream:true must not diverge here. The same
|
|
2772
|
+
// pass also records a `nullWideningMap`: optional fields are widened to
|
|
2773
|
+
// `required` + nullable for the provider, which then returns `null` for an
|
|
2774
|
+
// absent optional — a `null` the original `.optional()` (`T | undefined`)
|
|
2775
|
+
// schema would otherwise reject. The map pinpoints exactly those synthesized
|
|
2776
|
+
// nulls so `undoNullWidening` can drop them while preserving the ones a
|
|
2777
|
+
// `.nullable()` field genuinely allows.
|
|
2778
|
+
const { jsonSchema, nullWideningMap } =
|
|
2779
|
+
convertSchemaForStructuredOutput(outputSchema)
|
|
2740
2780
|
if (!jsonSchema) {
|
|
2741
2781
|
throw new Error('Failed to convert output schema to JSON Schema')
|
|
2742
2782
|
}
|
|
2743
2783
|
|
|
2784
|
+
// Un-widening runs in the engine the moment the structured output is
|
|
2785
|
+
// captured (`finalStructuredOutput.normalize`), so it applies uniformly to
|
|
2786
|
+
// every adapter and to both stream modes — the engine is the only layer
|
|
2787
|
+
// holding the schema's `nullWideningMap`. Validation then runs on the
|
|
2788
|
+
// already-normalized data, so `validate` is a plain Standard Schema parse.
|
|
2789
|
+
const normalize = (data: unknown): unknown =>
|
|
2790
|
+
undoNullWidening(data, nullWideningMap)
|
|
2791
|
+
|
|
2744
2792
|
// Validation runs INSIDE the engine (per spec §7.3) so validation failures
|
|
2745
2793
|
// route through the engine's terminal-hook chooser as `onError`. We pass a
|
|
2746
2794
|
// `validate` callback when the schema is a Standard Schema; otherwise we
|
|
2747
|
-
// pass through the
|
|
2795
|
+
// pass through the (normalized) data and the engine returns it unchanged.
|
|
2748
2796
|
const validate = isStandardSchema(outputSchema)
|
|
2749
2797
|
? (data: unknown): unknown =>
|
|
2750
2798
|
parseWithStandardSchema<InferSchemaType<TSchema>>(outputSchema, data)
|
|
@@ -2776,6 +2824,7 @@ async function runAgenticStructuredOutput<
|
|
|
2776
2824
|
finalStructuredOutput: {
|
|
2777
2825
|
jsonSchema,
|
|
2778
2826
|
yieldChunks: false,
|
|
2827
|
+
normalize,
|
|
2779
2828
|
...(validate ? { validate } : {}),
|
|
2780
2829
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
2781
2830
|
},
|
|
@@ -2954,17 +3003,23 @@ async function* fallbackStructuredOutputStream(
|
|
|
2954
3003
|
* RUN_STARTED/RUN_FINISHED are suppressed; the structured-output finalization
|
|
2955
3004
|
* step's pair brackets the run for the consumer.
|
|
2956
3005
|
*
|
|
2957
|
-
* Schema validation is intentionally NOT run on this path — it is
|
|
2958
|
-
* consumer's responsibility.
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
* which DOES run Standard Schema validation inside the engine and routes
|
|
2962
|
-
* validation failures through `onError`. The reason for the asymmetry:
|
|
3006
|
+
* Standard Schema *validation* is intentionally NOT run on this path — it is
|
|
3007
|
+
* the consumer's responsibility. This is a deliberate asymmetry vs.
|
|
3008
|
+
* `runAgenticStructuredOutput` (Promise<T> path), which DOES validate inside
|
|
3009
|
+
* the engine and routes validation failures through `onError`. The reason:
|
|
2963
3010
|
* streaming consumers typically render partial JSON progressively (via
|
|
2964
3011
|
* `parsePartialJSON` or `useChat`'s `partial` slot) and validate downstream
|
|
2965
3012
|
* after assembly. Running validation server-side would force a hard error
|
|
2966
3013
|
* on partial-by-design payloads. See `docs/structured-outputs/overview.md`.
|
|
2967
3014
|
*
|
|
3015
|
+
* Null-widening normalization, however, IS run on both paths: the
|
|
3016
|
+
* `structured-output.complete` CUSTOM event is forwarded with its `value.object`
|
|
3017
|
+
* already un-widened (synthesized strict-mode nulls dropped, genuine
|
|
3018
|
+
* `.nullable()` nulls kept), so a consumer validating the assembled object
|
|
3019
|
+
* against the original schema doesn't choke on a `null` for an `.optional()`
|
|
3020
|
+
* field. Same `convertSchemaForStructuredOutput` pass and same
|
|
3021
|
+
* `undoNullWidening` map as the Promise<T> path — the two must not diverge.
|
|
3022
|
+
*
|
|
2968
3023
|
* Pre-flight validation (missing schema, unconvertible schema) throws
|
|
2969
3024
|
* synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
|
|
2970
3025
|
* those are programmer errors, not runtime conditions.
|
|
@@ -2982,14 +3037,17 @@ function runStreamingStructuredOutput<
|
|
|
2982
3037
|
}
|
|
2983
3038
|
|
|
2984
3039
|
// forStructuredOutput strict-converts the schema once at the activity
|
|
2985
|
-
// boundary
|
|
2986
|
-
//
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
}
|
|
3040
|
+
// boundary, capturing the null-widening map so the engine can un-widen the
|
|
3041
|
+
// provider's response before it reaches the consumer. Adapters can re-convert
|
|
3042
|
+
// if their wire format diverges, but the default flow hands them a
|
|
3043
|
+
// strict-ready schema.
|
|
3044
|
+
const { jsonSchema, nullWideningMap } =
|
|
3045
|
+
convertSchemaForStructuredOutput(outputSchema)
|
|
2990
3046
|
if (!jsonSchema) {
|
|
2991
3047
|
throw new Error('Failed to convert output schema to JSON Schema')
|
|
2992
3048
|
}
|
|
3049
|
+
const normalize = (data: unknown): unknown =>
|
|
3050
|
+
undoNullWidening(data, nullWideningMap)
|
|
2993
3051
|
|
|
2994
3052
|
// The implementation generator yields the broader internal type
|
|
2995
3053
|
// (`StreamChunk | StructuredOutputCompleteEvent<T>`) so agent-loop
|
|
@@ -3000,6 +3058,7 @@ function runStreamingStructuredOutput<
|
|
|
3000
3058
|
return runStreamingStructuredOutputImpl(
|
|
3001
3059
|
options,
|
|
3002
3060
|
jsonSchema,
|
|
3061
|
+
normalize,
|
|
3003
3062
|
) as StructuredOutputStream<InferSchemaType<TSchema>>
|
|
3004
3063
|
}
|
|
3005
3064
|
|
|
@@ -3025,6 +3084,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3025
3084
|
>(
|
|
3026
3085
|
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
3027
3086
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
3087
|
+
normalize: (data: unknown) => unknown,
|
|
3028
3088
|
): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
|
|
3029
3089
|
const {
|
|
3030
3090
|
adapter,
|
|
@@ -3069,6 +3129,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3069
3129
|
finalStructuredOutput: {
|
|
3070
3130
|
jsonSchema,
|
|
3071
3131
|
yieldChunks: true,
|
|
3132
|
+
normalize,
|
|
3072
3133
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
3073
3134
|
},
|
|
3074
3135
|
},
|
|
@@ -3083,9 +3144,10 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3083
3144
|
await mcpManager.dispose()
|
|
3084
3145
|
}
|
|
3085
3146
|
|
|
3086
|
-
// Schema validation for the streaming variant remains the
|
|
3087
|
-
// responsibility — they read the CUSTOM 'structured-output.complete'
|
|
3088
|
-
// the yielded stream.
|
|
3147
|
+
// Standard Schema validation for the streaming variant remains the
|
|
3148
|
+
// consumer's responsibility — they read the CUSTOM 'structured-output.complete'
|
|
3149
|
+
// from the yielded stream. (Null-widening normalization, by contrast, already
|
|
3150
|
+
// ran inside the engine via `normalize`, so the object they read is un-widened.)
|
|
3089
3151
|
void outputSchema
|
|
3090
3152
|
}
|
|
3091
3153
|
|