@tanstack/ai 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { NullWideningMap } from '@tanstack/ai-utils';
2
3
  import { JSONSchema, SchemaInput } from '../../../types.js';
3
4
  /**
4
5
  * Check if a value is a Standard JSON Schema compliant schema.
@@ -86,6 +87,18 @@ export interface ConvertSchemaOptions {
86
87
  * ```
87
88
  */
88
89
  export declare function convertSchemaToJsonSchema(schema: SchemaInput | undefined, options?: ConvertSchemaOptions): JSONSchema | undefined;
90
+ /**
91
+ * Convert a schema for structured output AND capture the {@link NullWideningMap}
92
+ * recording every `null` the strict-mode widening synthesized. The map lets the
93
+ * caller undo that widening on the provider's response (via `undoNullWidening`)
94
+ * before validating against the original schema — optional fields read back as
95
+ * absent while genuine `.nullable()` nulls survive. The map is `undefined` when
96
+ * the schema isn't a widenable object or when no field needed widening.
97
+ */
98
+ export declare function convertSchemaForStructuredOutput(schema: SchemaInput | undefined): {
99
+ jsonSchema: JSONSchema | undefined;
100
+ nullWideningMap: NullWideningMap | undefined;
101
+ };
89
102
  /**
90
103
  * Validates data against a Standard Schema compliant schema.
91
104
  *
@@ -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 transformed = makeStructuredOutputCompatible(
34
- prop,
35
- prop.required || []
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 transformed = {
46
+ const nestedItems = items ? makeStructuredOutputCompatible(items, items.required || []) : void 0;
47
+ properties[propName] = {
41
48
  ...prop,
42
- items: items ? makeStructuredOutputCompatible(items, items.required || []) : prop.items
49
+ items: nestedItems ? nestedItems.schema : prop.items,
50
+ ...wasOptional ? { type: ["array", "null"] } : {}
43
51
  };
44
- properties[propName] = wasOptional ? { ...transformed, type: ["array", "null"] } : transformed;
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
- ...prop,
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
- ...prop,
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
- result.items = makeStructuredOutputCompatible(items, items.required || []);
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 convertSchemaToJsonSchema(schema, options = {}) {
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
- let result = toJsonSchema(jsonSchema);
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
- if (forStructuredOutput) {
102
- const typedView = toJsonSchema(schema);
103
- return makeStructuredOutputCompatible(typedView, typedView.required || []);
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
- return schema;
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.33.0",
3
+ "version": "0.34.0",
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.4"
79
+ "@tanstack/ai-event-client": "0.6.5",
80
+ "@tanstack/ai-utils": "0.3.0"
80
81
  },
81
82
  "peerDependencies": {
82
83
  "@opentelemetry/api": ">=1.9.0"
@@ -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
- * - validate: optional callback invoked AFTER the structured-output result
426
- * is captured but BEFORE the terminal hook fires. If it throws, the
427
- * engine records a `finalizationError` and fires `onError` instead of
428
- * `onFinish` (per spec §7.3). On success, the returned value is stored
429
- * as the validated result and retrievable via
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 raw,
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
- this.structuredOutputResult = {
2097
- data: parsed.object,
2098
- rawText: parsed.raw,
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
- chunk,
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
- this.structuredOutputResult = { data: parsed, rawText }
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
- const jsonSchema = convertSchemaToJsonSchema(outputSchema, {
2738
- forStructuredOutput: true,
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 raw data and the engine returns it unchanged.
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 the
2958
- * consumer's responsibility. The `structured-output.complete` CUSTOM event
2959
- * is forwarded with the adapter-produced `value.object` as-is. This is a
2960
- * deliberate asymmetry vs. `runAgenticStructuredOutput` (Promise<T> path),
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. Adapters can re-convert if their wire format diverges, but the
2986
- // default flow hands them a strict-ready schema.
2987
- const jsonSchema = convertSchemaToJsonSchema(outputSchema, {
2988
- forStructuredOutput: true,
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 consumer's
3087
- // responsibility — they read the CUSTOM 'structured-output.complete' from
3088
- // the yielded stream. Matches pre-fix behavior.
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