@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.
- package/README.md +2 -6
- package/dist/esm/activities/chat/index.js +94 -23
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.js +4 -2
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +2 -0
- package/dist/esm/activities/chat/middleware/metadata.d.ts +16 -0
- package/dist/esm/activities/chat/middleware/metadata.js +8 -0
- package/dist/esm/activities/chat/middleware/metadata.js.map +1 -0
- package/dist/esm/activities/chat/middleware/types.d.ts +9 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.js +1 -0
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +21 -10
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +2 -1
- package/package.json +2 -2
- package/skills/ai-core/media-generation/SKILL.md +8 -5
- package/src/activities/chat/index.ts +139 -44
- package/src/activities/chat/middleware/compose.ts +14 -2
- package/src/activities/chat/middleware/index.ts +3 -0
- package/src/activities/chat/middleware/metadata.ts +20 -0
- package/src/activities/chat/middleware/types.ts +9 -0
- package/src/activities/chat/stream/processor.ts +6 -0
- package/src/activities/chat/tools/schema-converter.ts +30 -17
- package/src/index.ts +4 -0
|
@@ -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
|
|
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
|
|
108
|
+
items: nestedItems.schema,
|
|
95
109
|
...wasOptional ? { type: ["array", "null"] } : {}
|
|
96
110
|
};
|
|
97
111
|
widenedHere = wasOptional;
|
|
98
|
-
childMap = nestedItems
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
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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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
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
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
2928
|
-
this.
|
|
2929
|
-
|
|
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 =
|
|
3449
|
-
this.
|
|
3450
|
-
|
|
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 =
|
|
3460
|
-
this.middlewareCtx,
|
|
3461
|
-
|
|
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.
|
|
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
|
|
4459
|
-
while (
|
|
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
|
-
|
|
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,
|
|
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 = {
|
|
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 = {
|
|
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(',')}`,
|