ai-props 2.1.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.dev.vars +2 -0
  2. package/CHANGELOG.md +11 -0
  3. package/README.md +2 -0
  4. package/package.json +39 -13
  5. package/src/ai.ts +12 -31
  6. package/src/cascade.ts +795 -0
  7. package/src/client.ts +440 -0
  8. package/src/durable-cascade.ts +743 -0
  9. package/src/event-bridge.ts +478 -0
  10. package/src/generate.ts +14 -12
  11. package/src/hoc.ts +15 -19
  12. package/src/hono-jsx.ts +675 -0
  13. package/src/index.ts +30 -0
  14. package/src/mdx-types.ts +169 -0
  15. package/src/mdx-utils.ts +437 -0
  16. package/src/mdx.ts +1008 -0
  17. package/src/rpc.ts +614 -0
  18. package/src/streaming.ts +618 -0
  19. package/src/validate.ts +15 -29
  20. package/src/worker.ts +547 -0
  21. package/test/cascade.test.ts +338 -0
  22. package/test/durable-cascade.test.ts +319 -0
  23. package/test/event-bridge.test.ts +351 -0
  24. package/test/generate.test.ts +6 -16
  25. package/test/mdx.test.ts +817 -0
  26. package/test/worker/capnweb-rpc.test.ts +1084 -0
  27. package/test/worker/full-flow.integration.test.ts +1463 -0
  28. package/test/worker/hono-jsx.test.ts +1258 -0
  29. package/test/worker/mdx-parsing.test.ts +1148 -0
  30. package/test/worker/setup.ts +56 -0
  31. package/test/worker.test.ts +595 -0
  32. package/tsconfig.json +2 -1
  33. package/vitest.config.js +6 -0
  34. package/vitest.config.ts +15 -1
  35. package/vitest.workers.config.ts +58 -0
  36. package/wrangler.jsonc +27 -0
  37. package/.turbo/turbo-build.log +0 -4
  38. package/LICENSE +0 -21
  39. package/dist/ai.d.ts +0 -125
  40. package/dist/ai.d.ts.map +0 -1
  41. package/dist/ai.js +0 -199
  42. package/dist/ai.js.map +0 -1
  43. package/dist/cache.d.ts +0 -66
  44. package/dist/cache.d.ts.map +0 -1
  45. package/dist/cache.js +0 -183
  46. package/dist/cache.js.map +0 -1
  47. package/dist/generate.d.ts +0 -69
  48. package/dist/generate.d.ts.map +0 -1
  49. package/dist/generate.js +0 -221
  50. package/dist/generate.js.map +0 -1
  51. package/dist/hoc.d.ts +0 -164
  52. package/dist/hoc.d.ts.map +0 -1
  53. package/dist/hoc.js +0 -236
  54. package/dist/hoc.js.map +0 -1
  55. package/dist/index.d.ts +0 -15
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js +0 -21
  58. package/dist/index.js.map +0 -1
  59. package/dist/types.d.ts +0 -152
  60. package/dist/types.d.ts.map +0 -1
  61. package/dist/types.js +0 -7
  62. package/dist/types.js.map +0 -1
  63. package/dist/validate.d.ts +0 -58
  64. package/dist/validate.d.ts.map +0 -1
  65. package/dist/validate.js +0 -253
  66. package/dist/validate.js.map +0 -1
  67. package/src/ai.js +0 -198
  68. package/src/cache.js +0 -182
  69. package/src/generate.js +0 -220
  70. package/src/hoc.js +0 -235
  71. package/src/index.js +0 -20
  72. package/src/types.js +0 -6
  73. package/src/validate.js +0 -252
@@ -0,0 +1,478 @@
1
+ /**
2
+ * EventBridge - Event system with Cloudflare Queues support
3
+ *
4
+ * Provides both sync event handling and Queue-based async event delivery.
5
+ * Supports dead letter queues, event validation, and retry strategies.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Create an EventBridge with a queue
10
+ * const bridge = new EventBridge(env.MY_QUEUE)
11
+ *
12
+ * // Register event handlers
13
+ * bridge.on('user.created', async (data) => {
14
+ * console.log('New user:', data.id)
15
+ * })
16
+ *
17
+ * // Emit events (sent to queue for reliable delivery)
18
+ * await bridge.emit('user.created', { id: '123', name: 'John' })
19
+ *
20
+ * // In worker's queue handler:
21
+ * export default {
22
+ * async queue(batch, env) {
23
+ * const handler = createQueueHandler(bridge)
24
+ * await handler.queue(batch, env)
25
+ * }
26
+ * }
27
+ * ```
28
+ *
29
+ * @packageDocumentation
30
+ */
31
+
32
+ /**
33
+ * Queued event structure
34
+ */
35
+ export interface QueuedEvent<T = unknown> {
36
+ /** Event type identifier */
37
+ type: string
38
+ /** Event payload data */
39
+ data: T
40
+ /** Event creation timestamp */
41
+ timestamp: number
42
+ /** Unique event ID */
43
+ id: string
44
+ /** Optional metadata */
45
+ metadata?: Record<string, unknown> | undefined
46
+ /** Number of retry attempts */
47
+ attempts?: number | undefined
48
+ /** Error message (for DLQ events) */
49
+ error?: string | undefined
50
+ }
51
+
52
+ /**
53
+ * EventBridge configuration options
54
+ */
55
+ export interface EventBridgeConfig {
56
+ /** Maximum retry attempts before sending to DLQ */
57
+ maxRetries?: number | undefined
58
+ /** Dead letter queue for failed events */
59
+ deadLetterQueue?: Queue | undefined
60
+ /** Event schema validation function */
61
+ validator?: ((event: QueuedEvent) => boolean | Promise<boolean>) | undefined
62
+ /** Custom serializer for event data */
63
+ serializer?: (<T>(data: T) => unknown) | undefined
64
+ /** Custom deserializer for event data */
65
+ deserializer?: (<T>(data: unknown) => T) | undefined
66
+ /** Enable debug logging */
67
+ debug?: boolean | undefined
68
+ }
69
+
70
+ /**
71
+ * Internal resolved config type
72
+ */
73
+ interface ResolvedEventBridgeConfig {
74
+ maxRetries: number
75
+ deadLetterQueue: Queue | undefined
76
+ validator: ((event: QueuedEvent) => boolean | Promise<boolean>) | undefined
77
+ serializer: <T>(data: T) => unknown
78
+ deserializer: <T>(data: unknown) => T
79
+ debug: boolean
80
+ }
81
+
82
+ /**
83
+ * Event handler function type
84
+ */
85
+ export type EventHandler<T = unknown> = (data: T, event: QueuedEvent<T>) => void | Promise<void>
86
+
87
+ /**
88
+ * Queue message interface (Cloudflare Queues)
89
+ */
90
+ export interface QueueMessage<T = unknown> {
91
+ readonly id: string
92
+ readonly timestamp: Date
93
+ readonly body: T
94
+ ack(): void
95
+ retry(options?: { delaySeconds?: number | undefined }): void
96
+ }
97
+
98
+ /**
99
+ * Message batch interface (Cloudflare Queues)
100
+ */
101
+ export interface MessageBatch<T = unknown> {
102
+ readonly queue: string
103
+ readonly messages: readonly QueueMessage<T>[]
104
+ ackAll(): void
105
+ retryAll(options?: { delaySeconds?: number | undefined }): void
106
+ }
107
+
108
+ /**
109
+ * Queue interface (Cloudflare Queues)
110
+ */
111
+ export interface Queue<T = unknown> {
112
+ send(
113
+ message: T,
114
+ options?: { contentType?: string | undefined; delaySeconds?: number | undefined }
115
+ ): Promise<void>
116
+ sendBatch(
117
+ messages: Iterable<{
118
+ body: T
119
+ contentType?: string | undefined
120
+ delaySeconds?: number | undefined
121
+ }>
122
+ ): Promise<void>
123
+ }
124
+
125
+ /**
126
+ * Emit options for customizing event delivery
127
+ */
128
+ export interface EmitOptions {
129
+ /** Delay before the event is processed (in seconds) */
130
+ delaySeconds?: number | undefined
131
+ /** Additional metadata to attach to the event */
132
+ metadata?: Record<string, unknown> | undefined
133
+ }
134
+
135
+ /**
136
+ * EventBridge - Unified event system with Queue support
137
+ *
138
+ * Provides reliable event delivery via Cloudflare Queues with
139
+ * support for multiple handlers, dead letter queues, and retries.
140
+ */
141
+ export class EventBridge {
142
+ private handlers = new Map<string, EventHandler[]>()
143
+ private wildcardHandlers: EventHandler[] = []
144
+ private config: ResolvedEventBridgeConfig
145
+
146
+ /**
147
+ * Create a new EventBridge
148
+ *
149
+ * @param queue - Cloudflare Queue for event delivery
150
+ * @param config - Optional configuration
151
+ */
152
+ constructor(private queue?: Queue, config: EventBridgeConfig = {}) {
153
+ this.config = {
154
+ maxRetries: config.maxRetries ?? 3,
155
+ deadLetterQueue: config.deadLetterQueue,
156
+ validator: config.validator,
157
+ serializer: config.serializer ?? ((data) => data),
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
+ deserializer: config.deserializer ?? ((data) => data as any),
160
+ debug: config.debug ?? false,
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Emit an event
166
+ *
167
+ * If a queue is configured, the event will be sent to the queue for
168
+ * reliable delivery. Otherwise, handlers are invoked synchronously.
169
+ *
170
+ * @param event - Event type identifier
171
+ * @param data - Event payload data
172
+ * @param options - Optional emit options
173
+ */
174
+ async emit<T = unknown>(event: string, data: T, options?: EmitOptions): Promise<void> {
175
+ const queuedEvent: QueuedEvent<T> = {
176
+ type: event,
177
+ data: this.config.serializer(data) as T,
178
+ timestamp: Date.now(),
179
+ id: crypto.randomUUID(),
180
+ metadata: options?.metadata,
181
+ attempts: 0,
182
+ }
183
+
184
+ if (this.config.debug) {
185
+ console.log(`[EventBridge] Emitting event: ${event}`, queuedEvent.id)
186
+ }
187
+
188
+ // Validate event if validator is configured
189
+ if (this.config.validator) {
190
+ const isValid = await this.config.validator(queuedEvent)
191
+ if (!isValid) {
192
+ throw new Error(`Event validation failed for type: ${event}`)
193
+ }
194
+ }
195
+
196
+ if (this.queue) {
197
+ // Send to queue for reliable delivery
198
+ const sendOptions: { contentType?: string | undefined; delaySeconds?: number | undefined } =
199
+ {}
200
+ if (options?.delaySeconds !== undefined) {
201
+ sendOptions.delaySeconds = options.delaySeconds
202
+ }
203
+ await this.queue.send(queuedEvent, sendOptions)
204
+ } else {
205
+ // No queue - invoke handlers synchronously
206
+ await this.invokeHandlers(queuedEvent)
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Emit multiple events in a batch
212
+ *
213
+ * @param events - Array of events to emit
214
+ */
215
+ async emitBatch<T = unknown>(
216
+ events: Array<{ type: string; data: T; options?: EmitOptions }>
217
+ ): Promise<void> {
218
+ if (!this.queue) {
219
+ // No queue - invoke handlers for each event synchronously
220
+ for (const { type, data, options } of events) {
221
+ await this.emit(type, data, options)
222
+ }
223
+ return
224
+ }
225
+
226
+ const messages = events.map(({ type, data, options }) => {
227
+ const msg: {
228
+ body: QueuedEvent<T>
229
+ contentType?: string | undefined
230
+ delaySeconds?: number | undefined
231
+ } = {
232
+ body: {
233
+ type,
234
+ data: this.config.serializer(data) as T,
235
+ timestamp: Date.now(),
236
+ id: crypto.randomUUID(),
237
+ metadata: options?.metadata,
238
+ attempts: 0,
239
+ },
240
+ }
241
+ if (options?.delaySeconds !== undefined) {
242
+ msg.delaySeconds = options.delaySeconds
243
+ }
244
+ return msg
245
+ })
246
+
247
+ await this.queue.sendBatch(messages)
248
+ }
249
+
250
+ /**
251
+ * Register an event handler
252
+ *
253
+ * @param event - Event type to handle (use '*' for all events)
254
+ * @param handler - Handler function
255
+ */
256
+ on<T = unknown>(event: string, handler: EventHandler<T>): void {
257
+ if (event === '*') {
258
+ this.wildcardHandlers.push(handler as EventHandler)
259
+ return
260
+ }
261
+
262
+ const handlers = this.handlers.get(event) ?? []
263
+ handlers.push(handler as EventHandler)
264
+ this.handlers.set(event, handlers)
265
+ }
266
+
267
+ /**
268
+ * Remove an event handler
269
+ *
270
+ * @param event - Event type
271
+ * @param handler - Handler to remove
272
+ */
273
+ off<T = unknown>(event: string, handler: EventHandler<T>): void {
274
+ if (event === '*') {
275
+ const index = this.wildcardHandlers.indexOf(handler as EventHandler)
276
+ if (index !== -1) {
277
+ this.wildcardHandlers.splice(index, 1)
278
+ }
279
+ return
280
+ }
281
+
282
+ const handlers = this.handlers.get(event)
283
+ if (handlers) {
284
+ const index = handlers.indexOf(handler as EventHandler)
285
+ if (index !== -1) {
286
+ handlers.splice(index, 1)
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Register a one-time event handler
293
+ *
294
+ * @param event - Event type to handle
295
+ * @param handler - Handler function (called once then removed)
296
+ */
297
+ once<T = unknown>(event: string, handler: EventHandler<T>): void {
298
+ const wrapper: EventHandler<T> = async (data, queuedEvent) => {
299
+ this.off(event, wrapper)
300
+ await handler(data, queuedEvent)
301
+ }
302
+ this.on(event, wrapper)
303
+ }
304
+
305
+ /**
306
+ * Handle a message from the queue
307
+ *
308
+ * This method should be called from your worker's queue handler.
309
+ *
310
+ * @param message - Queue message to process
311
+ */
312
+ async handleMessage<T = unknown>(message: QueueMessage<QueuedEvent<T>>): Promise<void> {
313
+ const event = message.body
314
+ const attempts = (event.attempts ?? 0) + 1
315
+ const deserializedEvent: QueuedEvent<T> = {
316
+ ...event,
317
+ data: this.config.deserializer(event.data),
318
+ attempts,
319
+ }
320
+
321
+ if (this.config.debug) {
322
+ console.log(`[EventBridge] Processing event: ${event.type}`, event.id, `attempt: ${attempts}`)
323
+ }
324
+
325
+ try {
326
+ await this.invokeHandlers(deserializedEvent)
327
+ message.ack()
328
+ } catch (error) {
329
+ if (this.config.debug) {
330
+ console.error(`[EventBridge] Error processing event: ${event.type}`, error)
331
+ }
332
+
333
+ // Check if we should send to DLQ
334
+ if (attempts >= this.config.maxRetries) {
335
+ if (this.config.deadLetterQueue) {
336
+ await this.config.deadLetterQueue.send({
337
+ ...deserializedEvent,
338
+ error: error instanceof Error ? error.message : String(error),
339
+ })
340
+ message.ack() // Ack after sending to DLQ
341
+ } else {
342
+ // No DLQ - let it fail (will be dropped after max retries)
343
+ message.ack()
344
+ }
345
+ } else {
346
+ // Retry with exponential backoff
347
+ const delaySeconds = Math.min(60, Math.pow(2, attempts))
348
+ message.retry({ delaySeconds })
349
+ }
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Handle a batch of messages from the queue
355
+ *
356
+ * @param batch - Message batch to process
357
+ */
358
+ async handleBatch<T = unknown>(batch: MessageBatch<QueuedEvent<T>>): Promise<void> {
359
+ // Process messages in parallel
360
+ await Promise.all(batch.messages.map((message) => this.handleMessage(message)))
361
+ }
362
+
363
+ /**
364
+ * Get all registered event types
365
+ */
366
+ getEventTypes(): string[] {
367
+ return Array.from(this.handlers.keys())
368
+ }
369
+
370
+ /**
371
+ * Check if there are handlers for an event type
372
+ */
373
+ hasHandlers(event: string): boolean {
374
+ const handlers = this.handlers.get(event)
375
+ return (handlers && handlers.length > 0) || this.wildcardHandlers.length > 0
376
+ }
377
+
378
+ /**
379
+ * Clear all handlers
380
+ */
381
+ clearHandlers(): void {
382
+ this.handlers.clear()
383
+ this.wildcardHandlers = []
384
+ }
385
+
386
+ /**
387
+ * Invoke handlers for an event
388
+ */
389
+ private async invokeHandlers<T>(event: QueuedEvent<T>): Promise<void> {
390
+ const handlers = this.handlers.get(event.type) ?? []
391
+ const allHandlers = [...handlers, ...this.wildcardHandlers]
392
+
393
+ if (allHandlers.length === 0) {
394
+ if (this.config.debug) {
395
+ console.log(`[EventBridge] No handlers for event: ${event.type}`)
396
+ }
397
+ return
398
+ }
399
+
400
+ // Fan-out to all handlers
401
+ await Promise.all(allHandlers.map((handler) => handler(event.data, event)))
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Create a queue handler for use in a Cloudflare Worker
407
+ *
408
+ * @param bridge - EventBridge instance to handle events
409
+ * @returns Queue handler object with queue method
410
+ *
411
+ * @example
412
+ * ```typescript
413
+ * const bridge = new EventBridge(env.MY_QUEUE)
414
+ * const handler = createQueueHandler(bridge)
415
+ *
416
+ * export default {
417
+ * async queue(batch, env, ctx) {
418
+ * await handler.queue(batch, env, ctx)
419
+ * }
420
+ * }
421
+ * ```
422
+ */
423
+ export function createQueueHandler(bridge: EventBridge): {
424
+ queue: <T>(batch: MessageBatch<QueuedEvent<T>>, env?: unknown, ctx?: unknown) => Promise<void>
425
+ } {
426
+ return {
427
+ async queue<T>(
428
+ batch: MessageBatch<QueuedEvent<T>>,
429
+ _env?: unknown,
430
+ _ctx?: unknown
431
+ ): Promise<void> {
432
+ await bridge.handleBatch(batch)
433
+ },
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Create an EventBridge with standard configuration
439
+ *
440
+ * @param queue - Optional queue for async delivery
441
+ * @param dlq - Optional dead letter queue
442
+ * @returns Configured EventBridge instance
443
+ */
444
+ export function createEventBridge(queue?: Queue, dlq?: Queue): EventBridge {
445
+ const config: EventBridgeConfig = {
446
+ maxRetries: 3,
447
+ debug: false,
448
+ }
449
+ if (dlq) {
450
+ config.deadLetterQueue = dlq
451
+ }
452
+ return new EventBridge(queue, config)
453
+ }
454
+
455
+ /**
456
+ * Type helper for strongly-typed event maps
457
+ *
458
+ * @example
459
+ * ```typescript
460
+ * type MyEvents = {
461
+ * 'user.created': { id: string; name: string }
462
+ * 'user.deleted': { id: string }
463
+ * }
464
+ *
465
+ * const bridge = new EventBridge(queue) as TypedEventBridge<MyEvents>
466
+ * bridge.emit('user.created', { id: '1', name: 'John' }) // type-safe!
467
+ * ```
468
+ */
469
+ export interface TypedEventBridge<TEvents extends Record<string, unknown>> {
470
+ emit<K extends keyof TEvents & string>(
471
+ event: K,
472
+ data: TEvents[K],
473
+ options?: EmitOptions
474
+ ): Promise<void>
475
+ on<K extends keyof TEvents & string>(event: K, handler: EventHandler<TEvents[K]>): void
476
+ off<K extends keyof TEvents & string>(event: K, handler: EventHandler<TEvents[K]>): void
477
+ once<K extends keyof TEvents & string>(event: K, handler: EventHandler<TEvents[K]>): void
478
+ }
package/src/generate.ts CHANGED
@@ -17,9 +17,10 @@ import { createCacheKey, getDefaultCache } from './cache.js'
17
17
 
18
18
  /**
19
19
  * Default configuration
20
+ * Note: Use full model ID to avoid alias resolution issues in bundled environments
20
21
  */
21
22
  const DEFAULT_CONFIG: AIPropsConfig = {
22
- model: 'sonnet',
23
+ model: 'anthropic/claude-sonnet-4.5',
23
24
  cache: true,
24
25
  cacheTTL: 5 * 60 * 1000, // 5 minutes
25
26
  }
@@ -159,11 +160,16 @@ export async function generateProps<T = Record<string, unknown>>(
159
160
  }
160
161
 
161
162
  // Generate using AI
163
+ const effectiveModel = model ?? config.model ?? DEFAULT_CONFIG.model!
162
164
  const result = await generateObject({
163
- model: model || config.model || 'sonnet',
165
+ model: effectiveModel,
164
166
  schema: resolvedSchema,
165
167
  prompt: fullPrompt,
166
- system: system || config.system,
168
+ ...(system !== undefined
169
+ ? { system }
170
+ : config.system !== undefined
171
+ ? { system: config.system }
172
+ : {}),
167
173
  })
168
174
 
169
175
  const props = result.object as T
@@ -179,7 +185,7 @@ export async function generateProps<T = Record<string, unknown>>(
179
185
  props,
180
186
  cached: false,
181
187
  metadata: {
182
- model: model || config.model || 'sonnet',
188
+ model: effectiveModel,
183
189
  duration: Date.now() - startTime,
184
190
  },
185
191
  }
@@ -200,9 +206,7 @@ export function getPropsSync<T = Record<string, unknown>>(
200
206
  const cached = cache.get<T>(cacheKey)
201
207
 
202
208
  if (!cached) {
203
- throw new Error(
204
- 'Props not in cache. Use generateProps() first or ensure caching is enabled.'
205
- )
209
+ throw new Error('Props not in cache. Use generateProps() first or ensure caching is enabled.')
206
210
  }
207
211
 
208
212
  return cached.props
@@ -219,9 +223,7 @@ export function getPropsSync<T = Record<string, unknown>>(
219
223
  * ])
220
224
  * ```
221
225
  */
222
- export async function prefetchProps(
223
- requests: GeneratePropsOptions[]
224
- ): Promise<void> {
226
+ export async function prefetchProps(requests: GeneratePropsOptions[]): Promise<void> {
225
227
  await Promise.all(requests.map(generateProps))
226
228
  }
227
229
 
@@ -231,7 +233,7 @@ export async function prefetchProps(
231
233
  export async function generatePropsMany<T = Record<string, unknown>>(
232
234
  requests: GeneratePropsOptions[]
233
235
  ): Promise<GeneratePropsResult<T>[]> {
234
- return Promise.all(requests.map(req => generateProps<T>(req)))
236
+ return Promise.all(requests.map((req) => generateProps<T>(req)))
235
237
  }
236
238
 
237
239
  /**
@@ -248,7 +250,7 @@ export async function mergeWithGenerated<T extends Record<string, unknown>>(
248
250
  const schemaObj = typeof schema === 'string' ? { value: schema } : schema
249
251
  const schemaKeys = Object.keys(schemaObj as Record<string, unknown>)
250
252
  const providedKeys = Object.keys(partialProps)
251
- const missingKeys = schemaKeys.filter(k => !providedKeys.includes(k))
253
+ const missingKeys = schemaKeys.filter((k) => !providedKeys.includes(k))
252
254
 
253
255
  // If all props are provided, return as-is
254
256
  if (missingKeys.length === 0) {
package/src/hoc.ts CHANGED
@@ -62,7 +62,7 @@ export function createPropsEnhancer<P extends Record<string, unknown>>(
62
62
 
63
63
  // Check required props
64
64
  const missingRequired = (required as string[]).filter(
65
- key => propsWithDefaults[key as keyof P] === undefined
65
+ (key) => propsWithDefaults[key as keyof P] === undefined
66
66
  )
67
67
  if (missingRequired.length > 0) {
68
68
  throw new Error(`Missing required props: ${missingRequired.join(', ')}`)
@@ -72,14 +72,10 @@ export function createPropsEnhancer<P extends Record<string, unknown>>(
72
72
  const filteredSchema = filterSchemaKeys(schema, exclude as string[])
73
73
 
74
74
  // Generate missing props
75
- return await mergeWithGenerated<P>(
76
- filteredSchema,
77
- propsWithDefaults as Partial<P>,
78
- {
79
- model: config.model,
80
- system: config.system,
81
- }
82
- )
75
+ return await mergeWithGenerated<P>(filteredSchema, propsWithDefaults as Partial<P>, {
76
+ ...(config.model !== undefined && { model: config.model }),
77
+ ...(config.system !== undefined && { system: config.system }),
78
+ })
83
79
  } catch (error) {
84
80
  if (fallback) {
85
81
  return { ...defaults, ...fallback, ...partialProps } as P
@@ -154,7 +150,7 @@ export function createAsyncPropsProvider<P extends Record<string, unknown>>(
154
150
  revalidate?: number
155
151
  ): Promise<{ props: P; revalidate?: number }> => {
156
152
  const props = await enhancer(context)
157
- return { props, revalidate }
153
+ return { props, ...(revalidate !== undefined && { revalidate }) }
158
154
  },
159
155
  }
160
156
  }
@@ -194,8 +190,8 @@ export function createPropsTransformer<
194
190
  const result = await generateProps({
195
191
  schema,
196
192
  context: input,
197
- model: config.model,
198
- system: config.system,
193
+ ...(config.model !== undefined && { model: config.model }),
194
+ ...(config.system !== undefined && { system: config.system }),
199
195
  })
200
196
 
201
197
  const generated = result.props as Record<string, unknown>
@@ -234,8 +230,8 @@ export function createConditionalGenerator<P extends Record<string, unknown>>(op
234
230
  }
235
231
 
236
232
  return mergeWithGenerated(schema, props, {
237
- model: config.model,
238
- system: config.system,
233
+ ...(config.model !== undefined && { model: config.model }),
234
+ ...(config.system !== undefined && { system: config.system }),
239
235
  })
240
236
  }
241
237
  }
@@ -276,10 +272,10 @@ export function createBatchGenerator<P extends Record<string, unknown>>(options:
276
272
  for (let i = 0; i < items.length; i += concurrency) {
277
273
  const batch = items.slice(i, i + concurrency)
278
274
  const batchResults = await Promise.all(
279
- batch.map(item =>
275
+ batch.map((item) =>
280
276
  mergeWithGenerated<P>(schema, item, {
281
- model: config.model,
282
- system: config.system,
277
+ ...(config.model !== undefined && { model: config.model }),
278
+ ...(config.system !== undefined && { system: config.system }),
283
279
  })
284
280
  )
285
281
  )
@@ -297,8 +293,8 @@ export function createBatchGenerator<P extends Record<string, unknown>>(options:
297
293
 
298
294
  for (const item of items) {
299
295
  const result = await mergeWithGenerated<P>(schema, item, {
300
- model: config.model,
301
- system: config.system,
296
+ ...(config.model !== undefined && { model: config.model }),
297
+ ...(config.system !== undefined && { system: config.system }),
302
298
  })
303
299
  results.push(result)
304
300
  }