@tanstack/ai 0.51.0 → 0.52.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.
@@ -50,6 +50,21 @@ function isStandardSchema(schema) {
50
50
  function pruneMap(map) {
51
51
  return Object.keys(map).length > 0 ? map : void 0;
52
52
  }
53
+ function coerceArrayItems(items) {
54
+ if (Array.isArray(items)) {
55
+ const nested = items.map((item) => makeStructuredOutputCompatible(item, item.required || []));
56
+ const itemMaps = nested.map((entry) => entry.nullWidening ?? {});
57
+ return {
58
+ schema: nested.map((entry) => entry.schema),
59
+ itemMap: itemMaps.some((entry) => Object.keys(entry).length > 0) ? itemMaps : void 0
60
+ };
61
+ }
62
+ const nested = makeStructuredOutputCompatible(items, items.required || []);
63
+ return {
64
+ schema: nested.schema,
65
+ itemMap: nested.nullWidening
66
+ };
67
+ }
53
68
  /**
54
69
  * Transform a JSON schema to be compatible with OpenAI's structured output requirements.
55
70
  * OpenAI requires:
@@ -87,15 +102,14 @@ function makeStructuredOutputCompatible(schema, originalRequired = []) {
87
102
  widenedHere = wasOptional;
88
103
  childMap = nested.nullWidening;
89
104
  } else if (prop.type === "array" && prop.items) {
90
- const items = Array.isArray(prop.items) ? prop.items[0] : prop.items;
91
- const nestedItems = items ? makeStructuredOutputCompatible(items, items.required || []) : void 0;
105
+ const nestedItems = coerceArrayItems(prop.items);
92
106
  properties[propName] = {
93
107
  ...prop,
94
- items: nestedItems ? nestedItems.schema : prop.items,
108
+ items: nestedItems.schema,
95
109
  ...wasOptional ? { type: ["array", "null"] } : {}
96
110
  };
97
111
  widenedHere = wasOptional;
98
- childMap = nestedItems?.nullWidening ? { items: nestedItems.nullWidening } : void 0;
112
+ childMap = nestedItems.itemMap ? { items: nestedItems.itemMap } : void 0;
99
113
  } else if (wasOptional) {
100
114
  if (prop.type && !Array.isArray(prop.type)) {
101
115
  properties[propName] = {
@@ -122,12 +136,9 @@ function makeStructuredOutputCompatible(schema, originalRequired = []) {
122
136
  if (Object.keys(propertyMaps).length > 0) map.properties = propertyMaps;
123
137
  }
124
138
  if (result.type === "array" && result.items) {
125
- const items = Array.isArray(result.items) ? result.items[0] : result.items;
126
- if (items) {
127
- const nestedItems = makeStructuredOutputCompatible(items, items.required || []);
128
- result.items = nestedItems.schema;
129
- if (nestedItems.nullWidening) map.items = nestedItems.nullWidening;
130
- }
139
+ const nestedItems = coerceArrayItems(result.items);
140
+ result.items = nestedItems.schema;
141
+ if (nestedItems.itemMap) map.items = nestedItems.itemMap;
131
142
  }
132
143
  return {
133
144
  schema: result,
@@ -1 +1 @@
1
- {"version":3,"file":"schema-converter.js","names":[],"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"],"mappings":"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"}
1
+ {"version":3,"file":"schema-converter.js","names":[],"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\nfunction coerceArrayItems(items: JSONSchema | Array<JSONSchema>): {\n schema: JSONSchema | Array<JSONSchema>\n itemMap: NullWideningMap | Array<NullWideningMap> | undefined\n} {\n if (Array.isArray(items)) {\n const nested = items.map((item) =>\n makeStructuredOutputCompatible(item, item.required || []),\n )\n const itemMaps = nested.map((entry) => entry.nullWidening ?? {})\n return {\n schema: nested.map((entry) => entry.schema),\n itemMap: itemMaps.some((entry) => Object.keys(entry).length > 0)\n ? itemMaps\n : undefined,\n }\n }\n const nested = makeStructuredOutputCompatible(items, items.required || [])\n return { schema: nested.schema, itemMap: nested.nullWidening }\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 nestedItems = coerceArrayItems(prop.items)\n properties[propName] = {\n ...prop,\n items: nestedItems.schema,\n ...(wasOptional ? { type: ['array', 'null'] } : {}),\n }\n widenedHere = wasOptional\n childMap = nestedItems.itemMap\n ? { items: nestedItems.itemMap }\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 item schemas. A tuple (`items: [a, b, …]`) keeps every\n // position. A homogeneous schema stays a single items map so\n // `undoNullWidening` applies it to every element.\n if (result.type === 'array' && result.items) {\n const nestedItems = coerceArrayItems(result.items)\n result.items = nestedItems.schema\n if (nestedItems.itemMap) map.items = nestedItems.itemMap\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"],"mappings":"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"}
@@ -41,8 +41,8 @@ export type { InterruptDefinition, GenericInterruptRequest, InterruptDefinitionO
41
41
  export { INTERRUPT_BINDING_VERSION, canonicalizeInterruptResolutions, } from './interrupts.js';
42
42
  export type { BatchInterruptError, BatchInterruptErrorCode, InterruptBinding, InterruptCorrelation, InterruptSubmissionError, ItemInterruptError, ItemInterruptErrorCode, ToolApprovalResolution, UnopenedInterruptBinding, } from './interrupts.js';
43
43
  export type { GenerationMiddleware, GenerationMiddlewareContext, GenerationActivity, GenerationUsageInfo, GenerationFinishInfo, GenerationAbortInfo, GenerationErrorInfo, AnyGenerationMiddleware, GenerationResultTransform, GenerationResultTransformContext, } from './activities/middleware/index.js';
44
- export { createCapability, defineChatMiddleware, createChatMiddleware, } from './activities/chat/middleware/index.js';
45
- export type { Capability, CapabilityHandle, CapabilityContext, CapabilityGetter, CapabilityProvider, DefinedChatMiddleware, AnyChatMiddleware, } from './activities/chat/middleware/index.js';
44
+ export { createCapability, defineChatMiddleware, createChatMiddleware, MetadataCapability, getMetadata, provideMetadata, } from './activities/chat/middleware/index.js';
45
+ export type { Capability, CapabilityHandle, CapabilityContext, CapabilityGetter, CapabilityProvider, DefinedChatMiddleware, AnyChatMiddleware, MetadataStore, } from './activities/chat/middleware/index.js';
46
46
  export { isRunStatus, isTerminalRunStatus, defineRunStore, InMemoryRunStore, } from './activities/chat/middleware/index.js';
47
47
  export type { RunStatus, TerminalRunStatus, RunRecord, RunError, RunStore, } from './activities/chat/middleware/index.js';
48
48
  export { DetachableRunCapability, getDetachableRun, provideDetachableRun, } from './activities/chat/middleware/run-store.js';
package/dist/esm/index.js CHANGED
@@ -46,6 +46,7 @@ import { createFrozenRegistry, createToolRegistry } from "./tool-registry.js";
46
46
  import { INTERRUPT_BOUNDARY_PHASES, INTERRUPT_TOOL_RESUMES } from "./activities/chat/middleware/types.js";
47
47
  import { defineChatMiddleware } from "./activities/chat/middleware/define.js";
48
48
  import { createChatMiddleware } from "./activities/chat/middleware/builder.js";
49
+ import { MetadataCapability, getMetadata, provideMetadata } from "./activities/chat/middleware/metadata.js";
49
50
  import { CUSTOM_EVENT, isCustomEvent } from "./custom-events.js";
50
51
  import { buildBaseUsage } from "./utilities/usage.js";
51
52
  import { normalizeSystemPrompts } from "./system-prompts.js";
@@ -58,4 +59,4 @@ import { BatchStrategy, CompositeStrategy, ImmediateStrategy, PunctuationStrateg
58
59
  import { StreamProcessor, createReplayStream } from "./activities/chat/stream/processor.js";
59
60
  import { generationParamsFromBody, generationParamsFromRequest } from "./client.js";
60
61
  import { createModel, extendAdapter } from "./extend-adapter.js";
61
- export { BaseRerankAdapter, BatchStrategy, CUSTOM_EVENT, CompositeStrategy, ConsoleLogger, DISCOVERY_TOOL_NAME, DetachableRunCapability, DuplicateToolNameError, EventType, INTERRUPT_BINDING_METADATA_KEY, INTERRUPT_BINDING_VERSION, INTERRUPT_BOUNDARY_PHASES, INTERRUPT_CONTINUATION_METADATA_KEY, INTERRUPT_CONTINUATION_VERSION, INTERRUPT_PAYLOAD_METADATA_KEY, INTERRUPT_TOOL_RESUMES, ImmediateStrategy, InMemoryRunStore, InterruptResumeValidationError, MCPDuplicateToolNameError, PartialJSONParser, PunctuationStrategy, RUN_ACCEPTED_EVENT, RUN_CANCEL_REASON, RunDetachedCapability, SkillLimitError, StandardSchemaValidationError, StreamProcessor, ToolCallManager, WordBoundaryStrategy, brandProviderTool, buildBaseUsage, canonicalInterruptJson, canonicalizeInterruptResolutions, chat, chatParamsFromRequest, chatParamsFromRequestBody, cloneAndDeepFreezeJson, combineStrategies, convertMessagesToModelMessages, convertSchemaToJsonSchema, countEmbeddingInputModalities, createAudioOptions, createCapability, createChatMiddleware, createChatOptions, createEmbedOptions, createFrozenRegistry, createImageOptions, createInterruptBinding, createModel, createRealtimeEventEmitter, createReplayStream, createRerankOptions, createSpeechOptions, createSummarizeOptions, createToolRegistry, createTranscriptionOptions, createVideoOptions, decodeWsFrame, defaultJSONParser, defineChatMiddleware, defineInterrupt, defineRunStore, detectImageMimeType, digestInterruptJson, embed, encodeWsFrame, extendAdapter, firstSentence, fromSpecTokenUsage, generateAudio, generateImage, generateMessageId, generateSpeech, generateTranscription, generateVideo, generationParamsFromBody, generationParamsFromRequest, genericInterruptContinuationFromDescriptor, getChunkRunId, getChunkThreadId, getDetachableRun, getProviderExecutedMetadata, getRunDetached, getVideoJobStatus, hashSchemaInput, interruptItemError, isCancelRequestedReason, isContentPart, isContentPartArray, isCustomEvent, isProviderExecutedToolCall, isRunStatus, isStandardSchema, isTerminalRunStatus, maxIterations, memoryStream, mergeAgentTools, mergeMetadata, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeApprovalSchema, normalizeStreamChunk, normalizeSystemPrompts, normalizeToUIMessage, normalizeToolResult, parsePartialJSON, parseWithStandardSchema, provideDetachableRun, provideRunDetached, readGenericInterruptContinuation, readInterruptBinding, readUnopenedInterruptBinding, realtimeToken, renderLazyCatalogEntry, replayRunStream, requestRunCancel, requireTextOnlyEmbeddingInput, rerank, resolveEmbeddingInput, resolveMediaPrompt, resolveResumeRunId, resumeHttpResponse, resumeServerSentEventsResponse, resumeWebSocketResponse, resumeWebSocketStream, streamToText, summarize, toHttpResponse, toHttpStream, toServerSentEventsResponse, toServerSentEventsStream, toSpecTokenUsage, toWebSocketResponse, toWebSocketStream, toolDefinition, uiMessageToModelMessages, uiMessagesToWire, untilFinishReason, validateInterruptResumeBatch, validateWithStandardSchema, wasCancelRequested, withInterruptBinding, withTanstackMetadata, withoutInterruptBinding, wrapGenericInterruptContinuation };
62
+ export { BaseRerankAdapter, BatchStrategy, CUSTOM_EVENT, CompositeStrategy, ConsoleLogger, DISCOVERY_TOOL_NAME, DetachableRunCapability, DuplicateToolNameError, EventType, INTERRUPT_BINDING_METADATA_KEY, INTERRUPT_BINDING_VERSION, INTERRUPT_BOUNDARY_PHASES, INTERRUPT_CONTINUATION_METADATA_KEY, INTERRUPT_CONTINUATION_VERSION, INTERRUPT_PAYLOAD_METADATA_KEY, INTERRUPT_TOOL_RESUMES, ImmediateStrategy, InMemoryRunStore, InterruptResumeValidationError, MCPDuplicateToolNameError, MetadataCapability, PartialJSONParser, PunctuationStrategy, RUN_ACCEPTED_EVENT, RUN_CANCEL_REASON, RunDetachedCapability, SkillLimitError, StandardSchemaValidationError, StreamProcessor, ToolCallManager, WordBoundaryStrategy, brandProviderTool, buildBaseUsage, canonicalInterruptJson, canonicalizeInterruptResolutions, chat, chatParamsFromRequest, chatParamsFromRequestBody, cloneAndDeepFreezeJson, combineStrategies, convertMessagesToModelMessages, convertSchemaToJsonSchema, countEmbeddingInputModalities, createAudioOptions, createCapability, createChatMiddleware, createChatOptions, createEmbedOptions, createFrozenRegistry, createImageOptions, createInterruptBinding, createModel, createRealtimeEventEmitter, createReplayStream, createRerankOptions, createSpeechOptions, createSummarizeOptions, createToolRegistry, createTranscriptionOptions, createVideoOptions, decodeWsFrame, defaultJSONParser, defineChatMiddleware, defineInterrupt, defineRunStore, detectImageMimeType, digestInterruptJson, embed, encodeWsFrame, extendAdapter, firstSentence, fromSpecTokenUsage, generateAudio, generateImage, generateMessageId, generateSpeech, generateTranscription, generateVideo, generationParamsFromBody, generationParamsFromRequest, genericInterruptContinuationFromDescriptor, getChunkRunId, getChunkThreadId, getDetachableRun, getMetadata, getProviderExecutedMetadata, getRunDetached, getVideoJobStatus, hashSchemaInput, interruptItemError, isCancelRequestedReason, isContentPart, isContentPartArray, isCustomEvent, isProviderExecutedToolCall, isRunStatus, isStandardSchema, isTerminalRunStatus, maxIterations, memoryStream, mergeAgentTools, mergeMetadata, modelMessageToUIMessage, modelMessagesToUIMessages, normalizeApprovalSchema, normalizeStreamChunk, normalizeSystemPrompts, normalizeToUIMessage, normalizeToolResult, parsePartialJSON, parseWithStandardSchema, provideDetachableRun, provideMetadata, provideRunDetached, readGenericInterruptContinuation, readInterruptBinding, readUnopenedInterruptBinding, realtimeToken, renderLazyCatalogEntry, replayRunStream, requestRunCancel, requireTextOnlyEmbeddingInput, rerank, resolveEmbeddingInput, resolveMediaPrompt, resolveResumeRunId, resumeHttpResponse, resumeServerSentEventsResponse, resumeWebSocketResponse, resumeWebSocketStream, streamToText, summarize, toHttpResponse, toHttpStream, toServerSentEventsResponse, toServerSentEventsStream, toSpecTokenUsage, toWebSocketResponse, toWebSocketStream, toolDefinition, uiMessageToModelMessages, uiMessagesToWire, untilFinishReason, validateInterruptResumeBatch, validateWithStandardSchema, wasCancelRequested, withInterruptBinding, withTanstackMetadata, withoutInterruptBinding, wrapGenericInterruptContinuation };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai",
3
- "version": "0.51.0",
3
+ "version": "0.52.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",
@@ -88,7 +88,7 @@
88
88
  "@ag-ui/core": "0.1.1-canary.beta.0",
89
89
  "@standard-schema/spec": "^1.1.0",
90
90
  "partial-json": "^0.1.7",
91
- "@tanstack/ai-event-client": "^0.11.1",
91
+ "@tanstack/ai-event-client": "^0.11.2",
92
92
  "@tanstack/ai-utils": "^0.4.0"
93
93
  },
94
94
  "peerDependencies": {
@@ -521,23 +521,26 @@ const { jobId } = await generateVideo({
521
521
  // (x-goog-api-key header or ?key= query parameter).
522
522
  ```
523
523
 
524
- Gemini Omni Flash (`geminiVideo('gemini-omni-flash-preview')`) is served by
524
+ Gemini Omni Flash (`geminiVideo('gemini-omni-1.1-flash')`) is served by
525
525
  the Interactions API instead of Veo's operations flow — same adapter, routed
526
- by model. Clips are 720p; `duration` is any number of seconds in the 3–10
526
+ by model. `duration` is any number of seconds in the 3–10
527
527
  range (fractional ok, default 10 — availableDurations() reports the range),
528
- `size` is the aspect ratio (`'16:9' | '9:16'`), and the finished video arrives
528
+ `size` is an `aspectRatio_resolution` template (`'16:9'` or `'16:9_1080p'`;
529
+ suffix `'360p' | '720p' | '1080p' | '4k'`, default 720p), and the finished video arrives
529
530
  **inline** as a `data:video/mp4;base64,…` URL (no key needed to use it).
530
531
  Image/video prompt parts are sent as interaction content blocks, grouped
531
532
  as images, then videos, then text (no
532
533
  `metadata.role` routing); `data` sources go inline, `url` sources pass
533
534
  through as-is (never downloaded — use Gemini Files API URIs for remote
534
535
  media). For conversational editing, pass a prior generation's `jobId` as
535
- `modelOptions.previous_interaction_id` with a prompt describing the change:
536
+ `modelOptions.previous_interaction_id` with a prompt describing the change.
537
+ `gemini-omni-flash-preview` remains a deprecated alias until it shuts down
538
+ on 2026-09-30.
536
539
 
537
540
  ```typescript
538
541
  import { geminiVideo } from '@tanstack/ai-gemini'
539
542
 
540
- const omni = geminiVideo('gemini-omni-flash-preview')
543
+ const omni = geminiVideo('gemini-omni-1.1-flash')
541
544
  const first = await generateVideo({
542
545
  adapter: omni,
543
546
  prompt: 'A violinist outdoors',
@@ -788,6 +788,7 @@ class TextEngine<
788
788
  private readonly effectiveSignal?: AbortSignal
789
789
 
790
790
  private messages: Array<ModelMessage>
791
+ private providerMessages: Array<ModelMessage>
791
792
  private iterationCount = 0
792
793
  /** Cumulative tool calls counted in this run (emitted + pending resume). */
793
794
  private toolCallCount = 0
@@ -845,6 +846,9 @@ class TextEngine<
845
846
  >
846
847
  private readonly middlewareCtx: ChatMiddlewareContext<TContext>
847
848
  private readonly sandboxFileQueue: Array<StreamChunk> = []
849
+ private readonly middlewareCustomQueue: Array<StreamChunk> = []
850
+ private middlewareCustomWaiters: Array<() => void> = []
851
+ private drainingMiddlewareCustom = false
848
852
  private readonly deferredPromises: Array<Promise<unknown>> = []
849
853
  private abortReason?: string
850
854
  private readonly middlewareAbortController?: AbortController
@@ -935,6 +939,7 @@ class TextEngine<
935
939
  // Convert messages to ModelMessage format (handles both UIMessage and ModelMessage input)
936
940
  // This ensures consistent internal format regardless of what the client sends
937
941
  this.messages = convertMessagesToModelMessages(config.params.messages)
942
+ this.providerMessages = this.messages
938
943
 
939
944
  // Initialize lazy tool manager after messages are converted (needs message history for scanning)
940
945
  assertUniqueToolNames(config.params.tools || [])
@@ -993,6 +998,14 @@ class TextEngine<
993
998
  this.abortReason = reason
994
999
  this.middlewareAbortController?.abort(reason)
995
1000
  },
1001
+ emitCustomEvent: (name, value) => {
1002
+ this.middlewareCustomQueue.push(
1003
+ this.createCustomEventChunk(name, value),
1004
+ )
1005
+ const waiters = this.middlewareCustomWaiters
1006
+ this.middlewareCustomWaiters = []
1007
+ for (const waiter of waiters) waiter()
1008
+ },
996
1009
  context: config.context as TContext,
997
1010
  defer: (promise: Promise<unknown>) => {
998
1011
  this.deferredPromises.push(promise)
@@ -1128,21 +1141,24 @@ class TextEngine<
1128
1141
 
1129
1142
  try {
1130
1143
  // Provision capabilities before any consumer (onConfig onward) can read them
1131
- await this.middlewareRunner.runSetup(this.middlewareCtx)
1144
+ yield* this.runWhileYielding(
1145
+ this.middlewareRunner.runSetup(this.middlewareCtx),
1146
+ )
1132
1147
 
1133
1148
  // Run initial onConfig (phase = init)
1134
1149
  this.middlewareCtx.phase = 'init'
1135
1150
  const initialConfig = this.buildMiddlewareConfig()
1136
- const transformedConfig = await this.middlewareRunner.runOnConfig(
1137
- this.middlewareCtx,
1138
- initialConfig,
1151
+ const transformedConfig = yield* this.runWhileYielding(
1152
+ this.middlewareRunner.runOnConfig(this.middlewareCtx, initialConfig),
1139
1153
  )
1140
1154
  this.applyMiddlewareConfig(transformedConfig)
1141
1155
  await this.applyEphemeralInterruptResume(transformedConfig)
1142
1156
  await this.applyDurableGenericInterruptResolution()
1143
1157
 
1144
1158
  // Run onStart (devtools middleware emits text:request:started and initial messages here)
1145
- await this.middlewareRunner.runOnStart(this.middlewareCtx)
1159
+ yield* this.runWhileYielding(
1160
+ this.middlewareRunner.runOnStart(this.middlewareCtx),
1161
+ )
1146
1162
 
1147
1163
  if (this.earlyTermination) {
1148
1164
  yield* this.emitSuccessfulEarlyTermination()
@@ -1189,18 +1205,16 @@ class TextEngine<
1189
1205
  iteration: this.middlewareCtx.iteration,
1190
1206
  })
1191
1207
 
1192
- await this.beginCycle()
1208
+ yield* this.runWhileYielding(this.beginCycle())
1193
1209
 
1194
1210
  if (this.cyclePhase === 'processText') {
1195
1211
  // Run onConfig before each model call (phase = beforeModel)
1196
1212
  this.middlewareCtx.phase = 'beforeModel'
1197
1213
  this.middlewareCtx.iteration = this.iterationCount
1198
1214
  const iterConfig = this.buildMiddlewareConfig()
1199
- const iterTransformedConfig =
1200
- await this.middlewareRunner.runOnConfig(
1201
- this.middlewareCtx,
1202
- iterConfig,
1203
- )
1215
+ const iterTransformedConfig = yield* this.runWhileYielding(
1216
+ this.middlewareRunner.runOnConfig(this.middlewareCtx, iterConfig),
1217
+ )
1204
1218
  this.applyMiddlewareConfig(iterTransformedConfig)
1205
1219
 
1206
1220
  if (
@@ -1239,7 +1253,7 @@ class TextEngine<
1239
1253
  }
1240
1254
 
1241
1255
  this.endCycle()
1242
- } while (await this.shouldContinue())
1256
+ } while (yield* this.runWhileYielding(this.shouldContinue()))
1243
1257
  }
1244
1258
 
1245
1259
  this.logger.agentLoop('run finished', {
@@ -1497,7 +1511,7 @@ class TextEngine<
1497
1511
 
1498
1512
  for await (const raw of this.adapter.chatStream({
1499
1513
  model: this.params.model,
1500
- messages: this.messages,
1514
+ messages: this.providerMessages,
1501
1515
  tools: toolsWithJsonSchemas,
1502
1516
  metadata,
1503
1517
  request: this.effectiveRequest,
@@ -1632,6 +1646,7 @@ class TextEngine<
1632
1646
  continue
1633
1647
  }
1634
1648
  if (spec.type === EventType.RUN_STARTED) {
1649
+ if (this.hasPublicRunStarted) continue
1635
1650
  this.hasPublicRunStarted = true
1636
1651
  }
1637
1652
  this.logger.output(`type=${spec.type}`, { chunk: spec })
@@ -1646,6 +1661,7 @@ class TextEngine<
1646
1661
 
1647
1662
  // Drain any sandbox.file events emitted while processing this chunk.
1648
1663
  yield* this.drainSandboxFileQueue()
1664
+ yield* this.drainMiddlewareCustomQueue()
1649
1665
 
1650
1666
  if (this.earlyTermination) {
1651
1667
  break
@@ -1654,6 +1670,7 @@ class TextEngine<
1654
1670
 
1655
1671
  // Drain any remaining sandbox.file events emitted after the stream ended.
1656
1672
  yield* this.drainSandboxFileQueue()
1673
+ yield* this.drainMiddlewareCustomQueue()
1657
1674
  }
1658
1675
 
1659
1676
  private handleStreamChunk(chunk: AdapterYieldChunk): void {
@@ -2047,12 +2064,14 @@ class TextEngine<
2047
2064
  const allResults = [...executionResult.results, ...deferredErrorResults]
2048
2065
 
2049
2066
  // Notify middleware of tool phase completion (devtools emits aggregate events here)
2050
- await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
2051
- toolCalls: pendingToolCalls,
2052
- results: allResults,
2053
- needsApproval: executionResult.needsApproval,
2054
- needsClientExecution: executionResult.needsClientExecution,
2055
- })
2067
+ yield* this.runWhileYielding(
2068
+ this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
2069
+ toolCalls: pendingToolCalls,
2070
+ results: allResults,
2071
+ needsApproval: executionResult.needsApproval,
2072
+ needsClientExecution: executionResult.needsClientExecution,
2073
+ }),
2074
+ )
2056
2075
 
2057
2076
  if (
2058
2077
  executionResult.needsApproval.length > 0 ||
@@ -2228,23 +2247,26 @@ class TextEngine<
2228
2247
  const allResults = [...executionResult.results, ...deferredErrorResults]
2229
2248
 
2230
2249
  // Notify middleware of tool phase completion (devtools emits aggregate events here)
2231
- await this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
2232
- toolCalls,
2233
- results: allResults,
2234
- needsApproval: executionResult.needsApproval,
2235
- needsClientExecution: executionResult.needsClientExecution,
2236
- })
2250
+ yield* this.runWhileYielding(
2251
+ this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
2252
+ toolCalls,
2253
+ results: allResults,
2254
+ needsApproval: executionResult.needsApproval,
2255
+ needsClientExecution: executionResult.needsClientExecution,
2256
+ }),
2257
+ )
2237
2258
 
2238
2259
  const afterToolBoundaryChunks = this.buildToolResultChunks(
2239
2260
  allResults,
2240
2261
  finishEvent,
2241
2262
  )
2242
- const afterToolRequests =
2243
- await this.middlewareRunner.runOnInterruptBoundary(
2263
+ const afterToolRequests = yield* this.runWhileYielding(
2264
+ this.middlewareRunner.runOnInterruptBoundary(
2244
2265
  this.middlewareCtx as ChatMiddlewareContext<TContext> & {
2245
2266
  phase: 'afterTools'
2246
2267
  },
2247
- )
2268
+ ),
2269
+ )
2248
2270
  if (afterToolRequests.length > 0) {
2249
2271
  for (const chunk of afterToolBoundaryChunks) {
2250
2272
  yield* this.pipeThroughMiddleware(chunk)
@@ -2924,10 +2946,12 @@ class TextEngine<
2924
2946
  this.middlewareCtx.phase = phase
2925
2947
  const boundaryRequests =
2926
2948
  requests ??
2927
- (await this.middlewareRunner.runOnInterruptBoundary(
2928
- this.middlewareCtx as ChatMiddlewareContext<TContext> & {
2929
- phase: typeof phase
2930
- },
2949
+ (yield* this.runWhileYielding(
2950
+ this.middlewareRunner.runOnInterruptBoundary(
2951
+ this.middlewareCtx as ChatMiddlewareContext<TContext> & {
2952
+ phase: typeof phase
2953
+ },
2954
+ ),
2931
2955
  ))
2932
2956
  if (boundaryRequests.length === 0) return false
2933
2957
  for (const request of boundaryRequests) {
@@ -3445,9 +3469,11 @@ class TextEngine<
3445
3469
  }
3446
3470
 
3447
3471
  // 1) onStructuredOutputConfig — middleware can transform messages, options, outputSchema
3448
- structuredConfig = await this.middlewareRunner.runOnStructuredOutputConfig(
3449
- this.middlewareCtx,
3450
- structuredConfig,
3472
+ structuredConfig = yield* this.runWhileYielding(
3473
+ this.middlewareRunner.runOnStructuredOutputConfig(
3474
+ this.middlewareCtx,
3475
+ structuredConfig,
3476
+ ),
3451
3477
  )
3452
3478
 
3453
3479
  // 2) onConfig — phase-aware general-purpose middleware re-runs at the
@@ -3456,9 +3482,11 @@ class TextEngine<
3456
3482
  // call — same constraint applies — but the view is consistent with the
3457
3483
  // ChatMiddlewareConfig shape).
3458
3484
  const { outputSchema: pinnedSchema, ...chatConfigSlice } = structuredConfig
3459
- const postOnConfig = await this.middlewareRunner.runOnConfig(
3460
- this.middlewareCtx,
3461
- { ...chatConfigSlice, tools: baseConfig.tools },
3485
+ const postOnConfig = yield* this.runWhileYielding(
3486
+ this.middlewareRunner.runOnConfig(this.middlewareCtx, {
3487
+ ...chatConfigSlice,
3488
+ tools: baseConfig.tools,
3489
+ }),
3462
3490
  )
3463
3491
 
3464
3492
  // Apply merged config back to engine state
@@ -3470,7 +3498,7 @@ class TextEngine<
3470
3498
  const structuredCallOptions = {
3471
3499
  chatOptions: {
3472
3500
  model: this.params.model,
3473
- messages: this.messages,
3501
+ messages: this.providerMessages,
3474
3502
  metadata: postOnConfig.metadata,
3475
3503
  modelOptions: postOnConfig.modelOptions,
3476
3504
  systemPrompts: postOnConfig.systemPrompts,
@@ -3949,6 +3977,7 @@ class TextEngine<
3949
3977
  private buildMiddlewareConfig(): ChatMiddlewareConfig {
3950
3978
  return {
3951
3979
  messages: this.messages,
3980
+ providerMessages: this.messages,
3952
3981
  systemPrompts: [...this.systemPrompts],
3953
3982
  tools: [...this.tools],
3954
3983
  resume: this.params.resume,
@@ -4367,6 +4396,7 @@ class TextEngine<
4367
4396
  private applyMiddlewareConfig(config: ChatMiddlewareConfig): void {
4368
4397
  this.applyResumeToolState(config.resumeToolState)
4369
4398
  this.messages = config.messages
4399
+ this.providerMessages = config.providerMessages ?? config.messages
4370
4400
  this.systemPrompts = config.systemPrompts
4371
4401
  assertUniqueToolNames(config.tools)
4372
4402
  this.tools = config.tools
@@ -4399,6 +4429,7 @@ class TextEngine<
4399
4429
  for (const spec of normalizeStreamChunk(output as AdapterYieldChunk)) {
4400
4430
  restorePublicUsage(spec)
4401
4431
  if (spec.type === EventType.RUN_STARTED) {
4432
+ if (this.hasPublicRunStarted) continue
4402
4433
  this.hasPublicRunStarted = true
4403
4434
  }
4404
4435
  yield spec
@@ -4419,6 +4450,69 @@ class TextEngine<
4419
4450
  chunk,
4420
4451
  )
4421
4452
  yield* this.emitPublicChunks(afterMw)
4453
+ if (!this.drainingMiddlewareCustom) {
4454
+ yield* this.drainMiddlewareCustomQueue()
4455
+ }
4456
+ }
4457
+
4458
+ /**
4459
+ * Drain CUSTOM chunks pushed by `ctx.emitCustomEvent` through middleware
4460
+ * and into the public stream. If the run has not yet sent `RUN_STARTED`,
4461
+ * emit that first so CUSTOM events are not the first wire event.
4462
+ */
4463
+ private async *drainMiddlewareCustomQueue(): AsyncGenerator<StreamChunk> {
4464
+ if (this.drainingMiddlewareCustom) return
4465
+ if (this.middlewareCustomQueue.length === 0) return
4466
+ this.drainingMiddlewareCustom = true
4467
+ try {
4468
+ yield* this.emitSyntheticRunStarted(this.createSyntheticFinishedEvent())
4469
+ while (this.middlewareCustomQueue.length > 0) {
4470
+ const chunk = this.middlewareCustomQueue.shift()
4471
+ if (chunk) yield* this.pipeThroughMiddleware(chunk)
4472
+ }
4473
+ } finally {
4474
+ this.drainingMiddlewareCustom = false
4475
+ }
4476
+ }
4477
+
4478
+ /**
4479
+ * Await `work` while yielding any `emitCustomEvent` chunks as they arrive.
4480
+ */
4481
+ private async *runWhileYielding<T>(
4482
+ work: Promise<T>,
4483
+ ): AsyncGenerator<StreamChunk, T> {
4484
+ let settled = false
4485
+ let result: T | undefined
4486
+ let error: unknown
4487
+ const done = work.then(
4488
+ (value) => {
4489
+ settled = true
4490
+ result = value
4491
+ },
4492
+ (err: unknown) => {
4493
+ settled = true
4494
+ error = err
4495
+ },
4496
+ )
4497
+
4498
+ while (!settled) {
4499
+ yield* this.drainMiddlewareCustomQueue()
4500
+ if (settled) break
4501
+ await Promise.race([
4502
+ done,
4503
+ new Promise<void>((resolve) => {
4504
+ if (this.middlewareCustomQueue.length > 0) {
4505
+ resolve()
4506
+ return
4507
+ }
4508
+ this.middlewareCustomWaiters.push(resolve)
4509
+ }),
4510
+ ])
4511
+ }
4512
+
4513
+ yield* this.drainMiddlewareCustomQueue()
4514
+ if (error !== undefined) throw error
4515
+ return result as T
4422
4516
  }
4423
4517
 
4424
4518
  /**
@@ -4455,17 +4549,18 @@ class TextEngine<
4455
4549
  },
4456
4550
  void
4457
4551
  > {
4458
- let next = await generator.next()
4459
- while (!next.done) {
4552
+ let pending = generator.next()
4553
+ while (true) {
4554
+ const next = yield* this.runWhileYielding(pending)
4555
+ if (next.done) return next.value
4460
4556
  yield* this.pipeThroughMiddleware(next.value)
4461
- next = await generator.next()
4557
+ pending = generator.next()
4462
4558
  }
4463
- return next.value
4464
4559
  }
4465
4560
 
4466
4561
  private createCustomEventChunk(
4467
4562
  eventName: string,
4468
- value: Record<string, any>,
4563
+ value: Record<string, unknown>,
4469
4564
  ): CustomEvent {
4470
4565
  return {
4471
4566
  type: EventType.CUSTOM,
@@ -166,7 +166,13 @@ export class MiddlewareRunner<
166
166
  const result = await mw.onConfig(ctx, current)
167
167
  const hasTransform = result !== undefined && result !== null
168
168
  if (hasTransform) {
169
- current = { ...current, ...result }
169
+ current = {
170
+ ...current,
171
+ ...result,
172
+ ...('messages' in result && !('providerMessages' in result)
173
+ ? { providerMessages: result.messages }
174
+ : {}),
175
+ }
170
176
  if (!skip) {
171
177
  this.logger.config(
172
178
  `middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,
@@ -221,7 +227,13 @@ export class MiddlewareRunner<
221
227
  const result = await mw.onStructuredOutputConfig(ctx, current)
222
228
  const hasTransform = result !== undefined && result !== null
223
229
  if (hasTransform) {
224
- current = { ...current, ...result }
230
+ current = {
231
+ ...current,
232
+ ...result,
233
+ ...('messages' in result && !('providerMessages' in result)
234
+ ? { providerMessages: result.messages }
235
+ : {}),
236
+ }
225
237
  if (!skip) {
226
238
  this.logger.config(
227
239
  `middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,