universal-llm-client 4.0.0 → 4.1.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 (127) hide show
  1. package/dist/ai-model.d.ts +20 -22
  2. package/dist/ai-model.d.ts.map +1 -1
  3. package/dist/ai-model.js +26 -23
  4. package/dist/ai-model.js.map +1 -1
  5. package/dist/client.d.ts +5 -5
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +17 -9
  8. package/dist/client.js.map +1 -1
  9. package/dist/http.d.ts +2 -0
  10. package/dist/http.d.ts.map +1 -1
  11. package/dist/http.js +1 -0
  12. package/dist/http.js.map +1 -1
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/interfaces.d.ts +49 -11
  18. package/dist/interfaces.d.ts.map +1 -1
  19. package/dist/interfaces.js +14 -0
  20. package/dist/interfaces.js.map +1 -1
  21. package/dist/providers/anthropic.d.ts +56 -0
  22. package/dist/providers/anthropic.d.ts.map +1 -0
  23. package/dist/providers/anthropic.js +524 -0
  24. package/dist/providers/anthropic.js.map +1 -0
  25. package/dist/providers/google.d.ts +5 -0
  26. package/dist/providers/google.d.ts.map +1 -1
  27. package/dist/providers/google.js +64 -8
  28. package/dist/providers/google.js.map +1 -1
  29. package/dist/providers/index.d.ts +1 -0
  30. package/dist/providers/index.d.ts.map +1 -1
  31. package/dist/providers/index.js +1 -0
  32. package/dist/providers/index.js.map +1 -1
  33. package/dist/providers/ollama.d.ts.map +1 -1
  34. package/dist/providers/ollama.js +38 -11
  35. package/dist/providers/ollama.js.map +1 -1
  36. package/dist/providers/openai.d.ts.map +1 -1
  37. package/dist/providers/openai.js +9 -7
  38. package/dist/providers/openai.js.map +1 -1
  39. package/dist/router.d.ts +13 -33
  40. package/dist/router.d.ts.map +1 -1
  41. package/dist/router.js +33 -57
  42. package/dist/router.js.map +1 -1
  43. package/dist/stream-decoder.d.ts +29 -2
  44. package/dist/stream-decoder.d.ts.map +1 -1
  45. package/dist/stream-decoder.js +39 -11
  46. package/dist/stream-decoder.js.map +1 -1
  47. package/dist/structured-output.d.ts +84 -181
  48. package/dist/structured-output.d.ts.map +1 -1
  49. package/dist/structured-output.js +79 -187
  50. package/dist/structured-output.js.map +1 -1
  51. package/dist/zod-adapter.d.ts +44 -0
  52. package/dist/zod-adapter.d.ts.map +1 -0
  53. package/dist/zod-adapter.js +61 -0
  54. package/dist/zod-adapter.js.map +1 -0
  55. package/package.json +9 -1
  56. package/src/ai-model.ts +350 -0
  57. package/src/auditor.ts +213 -0
  58. package/src/client.ts +402 -0
  59. package/src/debug/debug-google-streaming.ts +97 -0
  60. package/src/debug/debug-tool-execution.ts +86 -0
  61. package/src/debug/test-lmstudio-tools.ts +155 -0
  62. package/src/demos/README.md +47 -0
  63. package/src/demos/basic/universal-llm-examples.ts +161 -0
  64. package/src/demos/mcp/astrid-memory-demo.ts +295 -0
  65. package/src/demos/mcp/astrid-persona-memory.ts +357 -0
  66. package/src/demos/mcp/mcp-mongodb-demo.ts +275 -0
  67. package/src/demos/mcp/simple-astrid-memory.ts +148 -0
  68. package/src/demos/mcp/simple-mcp-demo.ts +68 -0
  69. package/src/demos/mcp/working-mcp-demo.ts +62 -0
  70. package/src/demos/model-alias-demo.ts +0 -0
  71. package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +267 -0
  72. package/src/demos/tools/astrid-memory-demo.ts +270 -0
  73. package/src/demos/tools/astrid-production-memory-clean.ts +785 -0
  74. package/src/demos/tools/astrid-production-memory.ts +558 -0
  75. package/src/demos/tools/basic-translation-test.ts +66 -0
  76. package/src/demos/tools/chromadb-similarity-tuning.ts +390 -0
  77. package/src/demos/tools/clean-multilingual-conversation.ts +209 -0
  78. package/src/demos/tools/clean-translation-test.ts +119 -0
  79. package/src/demos/tools/clean-universal-multilingual-test.ts +131 -0
  80. package/src/demos/tools/complete-rag-demo.ts +369 -0
  81. package/src/demos/tools/complete-tool-demo.ts +132 -0
  82. package/src/demos/tools/demo-tool-calling.ts +124 -0
  83. package/src/demos/tools/dynamic-language-switching-test.ts +251 -0
  84. package/src/demos/tools/hybrid-thinking-test.ts +154 -0
  85. package/src/demos/tools/memory-integration-test.ts +420 -0
  86. package/src/demos/tools/multilingual-memory-system.ts +802 -0
  87. package/src/demos/tools/ondemand-translation-demo.ts +655 -0
  88. package/src/demos/tools/production-tool-demo.ts +245 -0
  89. package/src/demos/tools/revolutionary-multilingual-test.ts +151 -0
  90. package/src/demos/tools/rigorous-language-analysis.ts +218 -0
  91. package/src/demos/tools/test-universal-memory-system.ts +126 -0
  92. package/src/demos/tools/translation-integration-guide.ts +346 -0
  93. package/src/demos/tools/universal-memory-system.ts +560 -0
  94. package/src/http.ts +247 -0
  95. package/src/index.ts +160 -0
  96. package/src/interfaces.ts +657 -0
  97. package/src/mcp.ts +345 -0
  98. package/src/providers/anthropic.ts +762 -0
  99. package/src/providers/google.ts +620 -0
  100. package/src/providers/index.ts +8 -0
  101. package/src/providers/ollama.ts +469 -0
  102. package/src/providers/openai.ts +392 -0
  103. package/src/router.ts +780 -0
  104. package/src/stream-decoder.ts +361 -0
  105. package/src/structured-output.ts +702 -0
  106. package/src/test-scripts/test-advanced-tools.ts +310 -0
  107. package/src/test-scripts/test-google-streaming-enhanced.ts +147 -0
  108. package/src/test-scripts/test-google-streaming.ts +63 -0
  109. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -0
  110. package/src/test-scripts/test-mcp-config.ts +28 -0
  111. package/src/test-scripts/test-mcp-connection.ts +29 -0
  112. package/src/test-scripts/test-system-message-positions.ts +163 -0
  113. package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -0
  114. package/src/test-scripts/test-tool-calling.ts +231 -0
  115. package/src/tests/ai-model.test.ts +1614 -0
  116. package/src/tests/auditor.test.ts +224 -0
  117. package/src/tests/http.test.ts +200 -0
  118. package/src/tests/interfaces.test.ts +117 -0
  119. package/src/tests/providers/google.test.ts +660 -0
  120. package/src/tests/providers/ollama.test.ts +954 -0
  121. package/src/tests/providers/openai.test.ts +1122 -0
  122. package/src/tests/router.test.ts +254 -0
  123. package/src/tests/stream-decoder.test.ts +179 -0
  124. package/src/tests/structured-output.test.ts +1340 -0
  125. package/src/tests/tools.test.ts +175 -0
  126. package/src/tools.ts +246 -0
  127. package/src/zod-adapter.ts +72 -0
@@ -0,0 +1,702 @@
1
+ /**
2
+ * Structured Output Core Types
3
+ *
4
+ * Core types for structured output support in universal-llm-client.
5
+ * Zero-dependency — works with raw JSON Schema and optional validate functions.
6
+ *
7
+ * For Zod integration, use the `universal-llm-client/zod` entrypoint.
8
+ *
9
+ * @module structured-output
10
+ */
11
+
12
+ // ============================================================================
13
+ // JSON Schema Types
14
+ // ============================================================================
15
+
16
+ /**
17
+ * JSON Schema definition for structured output.
18
+ * This is a subset of JSON Schema focused on what providers need.
19
+ */
20
+ export interface JSONSchema {
21
+ type?: string | string[];
22
+ properties?: Record<string, JSONSchema>;
23
+ items?: JSONSchema | JSONSchema[] | { type: string } | { type: string }[];
24
+ required?: string[];
25
+ additionalProperties?: boolean | JSONSchema;
26
+ enum?: (string | number | boolean | null)[];
27
+ const?: unknown;
28
+ oneOf?: JSONSchema[];
29
+ anyOf?: JSONSchema[];
30
+ allOf?: JSONSchema[];
31
+ not?: JSONSchema;
32
+ description?: string;
33
+ default?: unknown;
34
+ examples?: unknown[];
35
+ title?: string;
36
+ format?: string;
37
+ pattern?: string;
38
+ minLength?: number;
39
+ maxLength?: number;
40
+ minimum?: number;
41
+ maximum?: number;
42
+ exclusiveMinimum?: number;
43
+ exclusiveMaximum?: number;
44
+ minItems?: number;
45
+ maxItems?: number;
46
+ uniqueItems?: boolean;
47
+ minProperties?: number;
48
+ maxProperties?: number;
49
+ $ref?: string;
50
+ $id?: string;
51
+ $schema?: string;
52
+ definitions?: Record<string, JSONSchema>;
53
+ $defs?: Record<string, JSONSchema>;
54
+ }
55
+
56
+ // ============================================================================
57
+ // SchemaConfig — The core abstraction replacing z.ZodType<T>
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Universal schema configuration for structured output.
62
+ *
63
+ * This is the Bring-Your-Own-Validator interface. The core library never
64
+ * imports Zod — instead it accepts a JSON Schema + optional validate function.
65
+ *
66
+ * Use `universal-llm-client/zod` → `fromZod()` to create this from Zod schemas.
67
+ *
68
+ * @template T The expected output type
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * // Zero-dep usage (no Zod):
73
+ * const config: SchemaConfig<{ name: string; age: number }> = {
74
+ * jsonSchema: {
75
+ * type: 'object',
76
+ * properties: {
77
+ * name: { type: 'string' },
78
+ * age: { type: 'number' },
79
+ * },
80
+ * required: ['name', 'age'],
81
+ * },
82
+ * validate: (data) => data as { name: string; age: number },
83
+ * };
84
+ *
85
+ * // With Zod (via adapter):
86
+ * import { fromZod } from 'universal-llm-client/zod';
87
+ * const config = fromZod(z.object({ name: z.string(), age: z.number() }));
88
+ * ```
89
+ */
90
+ export interface SchemaConfig<T = unknown> {
91
+ /** JSON Schema sent to the LLM provider */
92
+ readonly jsonSchema: JSONSchema;
93
+ /**
94
+ * Optional validator: parse unknown → T or throw.
95
+ * If omitted, JSON.parse() result is returned as-is (unsafe cast).
96
+ */
97
+ readonly validate?: (data: unknown) => T;
98
+ /** Schema name (for provider guidance, e.g. OpenAI strict mode) */
99
+ readonly name?: string;
100
+ /** Schema description (for provider guidance) */
101
+ readonly description?: string;
102
+ }
103
+
104
+ // ============================================================================
105
+ // Provider Schema Types
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Provider identifier for schema conversion.
110
+ */
111
+ export type SchemaProvider = 'openai' | 'ollama' | 'google';
112
+
113
+ /**
114
+ * Result of converting a schema for a specific provider.
115
+ */
116
+ export interface ProviderSchema {
117
+ /** The JSON Schema for the provider */
118
+ schema: JSONSchema;
119
+ /** Optional schema name (used by OpenAI) */
120
+ name?: string;
121
+ /** Optional schema description (used by OpenAI) */
122
+ description?: string;
123
+ }
124
+
125
+ // ============================================================================
126
+ // Structured Output Error
127
+ // ============================================================================
128
+
129
+ /**
130
+ * Error options for StructuredOutputError
131
+ */
132
+ export interface StructuredOutputErrorOptions {
133
+ /** The raw output from the LLM that failed validation */
134
+ rawOutput: string;
135
+ /** The underlying cause (e.g., validation error) */
136
+ cause?: Error;
137
+ }
138
+
139
+ /**
140
+ * Custom error class for structured output validation failures.
141
+ *
142
+ * Thrown when:
143
+ * - JSON parsing of LLM response fails
144
+ * - Schema validation fails
145
+ *
146
+ * Features:
147
+ * - `rawOutput` property containing the original LLM response
148
+ * - `cause` property for the underlying error
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * try {
153
+ * const result = await model.generateStructured(schema, messages);
154
+ * } catch (error) {
155
+ * if (error instanceof StructuredOutputError) {
156
+ * console.log('Raw LLM output:', error.rawOutput);
157
+ * console.log('Cause:', error.cause);
158
+ * }
159
+ * }
160
+ * ```
161
+ */
162
+ export class StructuredOutputError extends Error {
163
+ /** The raw output from the LLM that failed validation */
164
+ public readonly rawOutput: string;
165
+
166
+ /** The underlying cause (e.g., validation error) */
167
+ public override readonly cause?: Error;
168
+
169
+ constructor(message: string, options: StructuredOutputErrorOptions) {
170
+ super(message);
171
+ this.rawOutput = options.rawOutput;
172
+ this.cause = options.cause;
173
+
174
+ // Maintains proper stack trace for where error was thrown (only available in V8)
175
+ if (Error.captureStackTrace) {
176
+ Error.captureStackTrace(this, StructuredOutputError);
177
+ }
178
+ }
179
+ }
180
+
181
+ // ============================================================================
182
+ // Structured Output Options (legacy — prefer SchemaConfig)
183
+ // ============================================================================
184
+
185
+ /**
186
+ * Options for structured output generation.
187
+ *
188
+ * Accepts either:
189
+ * - A SchemaConfig (recommended)
190
+ * - A raw JSON Schema object for flexibility
191
+ *
192
+ * @template T The expected output type
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * // Using SchemaConfig
197
+ * const options: StructuredOutputOptions<User> = {
198
+ * schemaConfig: mySchemaConfig,
199
+ * };
200
+ * ```
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * // Using raw JSON Schema
205
+ * const options: StructuredOutputOptions<User> = {
206
+ * jsonSchema: {
207
+ * type: 'object',
208
+ * properties: {
209
+ * name: { type: 'string' },
210
+ * age: { type: 'number' },
211
+ * },
212
+ * required: ['name', 'age'],
213
+ * },
214
+ * };
215
+ * ```
216
+ */
217
+ export interface StructuredOutputOptions<T> {
218
+ /**
219
+ * Schema configuration for structured output.
220
+ * Contains JSON Schema + optional validator.
221
+ */
222
+ schemaConfig?: SchemaConfig<T>;
223
+
224
+ /**
225
+ * Raw JSON Schema for structured output.
226
+ * Use this when you have a pre-defined schema without validation.
227
+ */
228
+ jsonSchema?: JSONSchema;
229
+
230
+ /**
231
+ * Optional name for the schema.
232
+ * Used by providers like OpenAI for better LLM guidance.
233
+ */
234
+ name?: string;
235
+
236
+ /**
237
+ * Optional description for the schema.
238
+ * Used by providers like OpenAI for better LLM guidance.
239
+ */
240
+ description?: string;
241
+ }
242
+
243
+ // ============================================================================
244
+ // Structured Output Result
245
+ // ============================================================================
246
+
247
+ /**
248
+ * Successful structured output result.
249
+ *
250
+ * @template T The output type
251
+ */
252
+ export interface StructuredOutputSuccess<T> {
253
+ /** Indicates success */
254
+ readonly ok: true;
255
+ /** The validated output value */
256
+ readonly value: T;
257
+ }
258
+
259
+ /**
260
+ * Failed structured output result.
261
+ *
262
+ * @template _T The expected output type (unused but kept for type alignment with StructuredOutputSuccess)
263
+ */
264
+ export interface StructuredOutputFailure<_T> {
265
+ /** Indicates failure */
266
+ readonly ok: false;
267
+ /** The error that occurred */
268
+ readonly error: StructuredOutputError;
269
+ /** The raw output from the LLM */
270
+ readonly rawOutput: string;
271
+ }
272
+
273
+ /**
274
+ * Result of structured output parsing.
275
+ *
276
+ * Discriminated union type that provides type-safe result handling:
277
+ * - `ok: true` → `{ value: T }`
278
+ * - `ok: false` → `{ error: StructuredOutputError, rawOutput: string }`
279
+ *
280
+ * @template T The output type
281
+ */
282
+ export type StructuredOutputResult<T> =
283
+ | StructuredOutputSuccess<T>
284
+ | StructuredOutputFailure<T>;
285
+
286
+ // ============================================================================
287
+ // Type Guards
288
+ // ============================================================================
289
+
290
+ /**
291
+ * Type guard to check if a structured output result is successful.
292
+ */
293
+ export function isStructuredOutputSuccess<T>(
294
+ result: StructuredOutputResult<T>,
295
+ ): result is StructuredOutputSuccess<T> {
296
+ return result.ok === true;
297
+ }
298
+
299
+ /**
300
+ * Type guard to check if a structured output result is a failure.
301
+ */
302
+ export function isStructuredOutputFailure<T>(
303
+ result: StructuredOutputResult<T>,
304
+ ): result is StructuredOutputFailure<T> {
305
+ return result.ok === false;
306
+ }
307
+
308
+ // ============================================================================
309
+ // Schema Conversion Utilities
310
+ // ============================================================================
311
+
312
+ /**
313
+ * Normalize a raw JSON Schema.
314
+ *
315
+ * Currently passes through without modification.
316
+ * Future versions may add normalization for provider compatibility.
317
+ *
318
+ * @param schema The JSON Schema to normalize
319
+ * @returns Normalized JSON Schema
320
+ */
321
+ export function normalizeJsonSchema(schema: JSONSchema): JSONSchema {
322
+ // Deep clone to avoid mutating the input
323
+ return JSON.parse(JSON.stringify(schema)) as JSONSchema;
324
+ }
325
+
326
+ /**
327
+ * Get the JSON Schema from a SchemaConfig or StructuredOutputOptions.
328
+ *
329
+ * @param options The structured output options
330
+ * @returns JSON Schema
331
+ */
332
+ export function getJsonSchema<T>(options: StructuredOutputOptions<T>): JSONSchema {
333
+ if (options.schemaConfig) {
334
+ return normalizeJsonSchema(options.schemaConfig.jsonSchema);
335
+ }
336
+ if (options.jsonSchema) {
337
+ return normalizeJsonSchema(options.jsonSchema);
338
+ }
339
+ throw new Error('Either schemaConfig or jsonSchema must be provided');
340
+ }
341
+
342
+ /**
343
+ * Get the JSON Schema from a SchemaConfig directly.
344
+ */
345
+ export function getJsonSchemaFromConfig<T>(config: SchemaConfig<T>): JSONSchema {
346
+ return normalizeJsonSchema(config.jsonSchema);
347
+ }
348
+
349
+ /**
350
+ * Features that some providers don't support.
351
+ * These are removed when transforming schemas for those providers.
352
+ */
353
+ const GOOGLE_UNSUPPORTED_FEATURES = [
354
+ 'pattern',
355
+ 'minLength',
356
+ 'maxLength',
357
+ 'minimum',
358
+ 'maximum',
359
+ 'exclusiveMinimum',
360
+ 'exclusiveMaximum',
361
+ // Google doesn't support additionalProperties in response schema
362
+ 'additionalProperties',
363
+ ] as const;
364
+
365
+ /**
366
+ * Strip unsupported features from a JSON Schema for a specific provider.
367
+ *
368
+ * Google/Gemini doesn't support certain JSON Schema features like pattern, min/max.
369
+ * This function removes those recursively.
370
+ *
371
+ * @param schema The JSON Schema to transform
372
+ * @param provider The target provider
373
+ * @returns Cleaned JSON Schema
374
+ */
375
+ export function stripUnsupportedFeatures(
376
+ schema: JSONSchema,
377
+ provider: SchemaProvider,
378
+ ): JSONSchema {
379
+ // Only Google needs transformation currently
380
+ if (provider !== 'google') {
381
+ return schema;
382
+ }
383
+
384
+ // Deep clone to avoid mutating input
385
+ const result: JSONSchema = JSON.parse(JSON.stringify(schema));
386
+
387
+ // Remove unsupported top-level properties
388
+ for (const feature of GOOGLE_UNSUPPORTED_FEATURES) {
389
+ delete (result as Record<string, unknown>)[feature];
390
+ }
391
+
392
+ // Recursively clean nested schemas
393
+ if (result.properties) {
394
+ for (const key of Object.keys(result.properties)) {
395
+ if (result.properties[key]) {
396
+ result.properties[key] = stripUnsupportedFeatures(result.properties[key], provider);
397
+ }
398
+ }
399
+ }
400
+
401
+ if (result['items']) {
402
+ if (Array.isArray(result['items'])) {
403
+ (result as Record<string, unknown>)['items'] = result['items'].map(item => stripUnsupportedFeatures(item as JSONSchema, provider));
404
+ } else {
405
+ result['items'] = stripUnsupportedFeatures(result['items'] as JSONSchema, provider);
406
+ }
407
+ }
408
+
409
+ // Handle oneOf, anyOf, allOf
410
+ for (const key of ['oneOf', 'anyOf', 'allOf'] as const) {
411
+ const schemas = result[key];
412
+ if (Array.isArray(schemas)) {
413
+ (result as Record<string, unknown>)[key] = schemas.map(s => stripUnsupportedFeatures(s, provider));
414
+ }
415
+ }
416
+
417
+ return result;
418
+ }
419
+
420
+ /**
421
+ * Convert structured output options to a provider-specific schema.
422
+ *
423
+ * This function:
424
+ * 1. Extracts/converts the JSON Schema from options
425
+ * 2. Applies provider-specific transformations (e.g., removing unsupported features for Google)
426
+ * 3. Adds name/description for LLM guidance
427
+ */
428
+ export function convertToProviderSchema<T>(
429
+ provider: SchemaProvider,
430
+ options: StructuredOutputOptions<T>,
431
+ ): ProviderSchema {
432
+ // Get the JSON Schema
433
+ const jsonSchema = getJsonSchema(options);
434
+
435
+ // Apply provider-specific transformations
436
+ const schema = stripUnsupportedFeatures(jsonSchema, provider);
437
+
438
+ // Generate a default name if not provided (some providers require it)
439
+ const name = options.name ?? options.schemaConfig?.name ?? 'response';
440
+
441
+ return {
442
+ schema,
443
+ name,
444
+ description: options.description ?? options.schemaConfig?.description,
445
+ };
446
+ }
447
+
448
+ // ============================================================================
449
+ // Validation Functions
450
+ // ============================================================================
451
+
452
+ /**
453
+ * Parse and validate structured output from raw LLM response text.
454
+ *
455
+ * This function:
456
+ * 1. Parses JSON from the raw output string
457
+ * 2. Validates using the SchemaConfig's validate function (if provided)
458
+ * 3. Throws StructuredOutputError on failure
459
+ *
460
+ * @param config The schema configuration with optional validator
461
+ * @param rawOutput The raw string output from the LLM
462
+ * @returns The validated and typed data
463
+ * @throws StructuredOutputError if JSON parsing fails or validation fails
464
+ */
465
+ export function parseStructured<T>(
466
+ config: SchemaConfig<T>,
467
+ rawOutput: string,
468
+ ): T {
469
+ // Step 1: Parse JSON
470
+ let parsed: unknown;
471
+ try {
472
+ parsed = JSON.parse(rawOutput);
473
+ } catch (error) {
474
+ // JSON parsing failed - wrap in StructuredOutputError
475
+ const syntaxError = error instanceof SyntaxError
476
+ ? error
477
+ : new SyntaxError(String(error));
478
+ throw new StructuredOutputError(
479
+ `Failed to parse JSON: ${syntaxError.message}`,
480
+ { rawOutput, cause: syntaxError },
481
+ );
482
+ }
483
+
484
+ // Step 2: Validate if validator is provided
485
+ if (config.validate) {
486
+ try {
487
+ return config.validate(parsed);
488
+ } catch (error) {
489
+ const validationError = error instanceof Error ? error : new Error(String(error));
490
+ throw new StructuredOutputError(
491
+ `Validation failed: ${validationError.message}`,
492
+ { rawOutput, cause: validationError },
493
+ );
494
+ }
495
+ }
496
+
497
+ // No validator — return as-is (unsafe cast, user chose to skip validation)
498
+ return parsed as T;
499
+ }
500
+
501
+ /**
502
+ * Try to parse and validate structured output, returning a result object.
503
+ *
504
+ * This is the non-throwing variant of `parseStructured`. Instead of throwing
505
+ * on validation failure, it returns a result object with `ok: false` and
506
+ * the error details.
507
+ *
508
+ * @param config The schema configuration with optional validator
509
+ * @param rawOutput The raw string output from the LLM
510
+ * @returns A result object: `{ ok: true, value }` on success, `{ ok: false, error, rawOutput }` on failure
511
+ */
512
+ export function tryParseStructured<T>(
513
+ config: SchemaConfig<T>,
514
+ rawOutput: string,
515
+ ): StructuredOutputResult<T> {
516
+ try {
517
+ const value = parseStructured(config, rawOutput);
518
+ return { ok: true, value };
519
+ } catch (error) {
520
+ if (error instanceof StructuredOutputError) {
521
+ return {
522
+ ok: false,
523
+ error,
524
+ rawOutput,
525
+ };
526
+ }
527
+ // Re-throw unexpected errors
528
+ throw error;
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Validate already-parsed data using a SchemaConfig's validator.
534
+ *
535
+ * This is useful when you have already parsed JSON and need to validate it.
536
+ *
537
+ * @param config The schema configuration with optional validator
538
+ * @param data The parsed data to validate
539
+ * @param rawOutput Optional raw output string for error messages
540
+ * @returns The validated and typed data
541
+ * @throws StructuredOutputError if validation fails
542
+ */
543
+ export function validateStructuredOutput<T>(
544
+ config: SchemaConfig<T>,
545
+ data: unknown,
546
+ rawOutput?: string,
547
+ ): T {
548
+ if (config.validate) {
549
+ try {
550
+ return config.validate(data);
551
+ } catch (error) {
552
+ const rawData = rawOutput ?? JSON.stringify(data);
553
+ const validationError = error instanceof Error ? error : new Error(String(error));
554
+ throw new StructuredOutputError(
555
+ `Validation failed: ${validationError.message}`,
556
+ { rawOutput: rawData, cause: validationError },
557
+ );
558
+ }
559
+ }
560
+ return data as T;
561
+ }
562
+
563
+ // ============================================================================
564
+ // Streaming JSON Parsing
565
+ // ============================================================================
566
+
567
+ /**
568
+ * Incremental JSON parser for streaming structured output.
569
+ *
570
+ * Allows parsing partial JSON as it streams in, returning validated partial
571
+ * objects when possible. Useful for structured output streaming where you
572
+ * want to see partial results before the complete JSON arrives.
573
+ */
574
+ export class StreamingJsonParser<T> {
575
+ private buffer = '';
576
+ private readonly validateFn?: (data: unknown) => T;
577
+
578
+ constructor(config: SchemaConfig<T>) {
579
+ this.validateFn = config.validate;
580
+ }
581
+
582
+ /**
583
+ * Feed a chunk of JSON text to the parser.
584
+ * Returns a validated partial object if the current buffer can be parsed
585
+ * as valid JSON that passes validation, or undefined if not yet valid.
586
+ */
587
+ feed(chunk: string): { partial: T | undefined; complete: boolean } {
588
+ this.buffer += chunk;
589
+
590
+ // Try to parse as complete JSON first
591
+ try {
592
+ const parsed = JSON.parse(this.buffer);
593
+ if (this.validateFn) {
594
+ try {
595
+ const validated = this.validateFn(parsed);
596
+ return { partial: validated, complete: true };
597
+ } catch {
598
+ // Validation failed on complete JSON — return parsed but not validated
599
+ }
600
+ }
601
+ return { partial: parsed as T, complete: true };
602
+ } catch {
603
+ // Not yet valid complete JSON
604
+ }
605
+
606
+ // Try to create a valid partial by closing braces
607
+ const partialResult = this.tryParsePartial();
608
+ return { partial: partialResult, complete: false };
609
+ }
610
+
611
+ /**
612
+ * Get the current buffer content.
613
+ */
614
+ getBuffer(): string {
615
+ return this.buffer;
616
+ }
617
+
618
+ /**
619
+ * Reset the parser state.
620
+ */
621
+ reset(): void {
622
+ this.buffer = '';
623
+ }
624
+
625
+ /**
626
+ * Attempt to parse partial JSON by adding closing brackets.
627
+ */
628
+ private tryParsePartial(): T | undefined {
629
+ // Count unclosed brackets and braces
630
+ let braceCount = 0;
631
+ let bracketCount = 0;
632
+ let inString = false;
633
+ let escaped = false;
634
+
635
+ for (let i = 0; i < this.buffer.length; i++) {
636
+ const char = this.buffer[i];
637
+
638
+ if (escaped) {
639
+ escaped = false;
640
+ continue;
641
+ }
642
+
643
+ if (char === '\\' && inString) {
644
+ escaped = true;
645
+ continue;
646
+ }
647
+
648
+ if (char === '"') {
649
+ inString = !inString;
650
+ continue;
651
+ }
652
+
653
+ if (inString) continue;
654
+
655
+ if (char === '{') braceCount++;
656
+ else if (char === '}') braceCount--;
657
+ else if (char === '[') bracketCount++;
658
+ else if (char === ']') bracketCount--;
659
+ }
660
+
661
+ // Build closing sequence
662
+ let closing = '';
663
+ while (bracketCount > 0) {
664
+ closing += ']';
665
+ bracketCount--;
666
+ }
667
+ while (braceCount > 0) {
668
+ closing += '}';
669
+ braceCount--;
670
+ }
671
+
672
+ // Try parsing with closing
673
+ const candidate = this.buffer + closing;
674
+ try {
675
+ const parsed = JSON.parse(candidate);
676
+ if (this.validateFn) {
677
+ try {
678
+ return this.validateFn(parsed);
679
+ } catch {
680
+ // Partial validation failed — that's expected for partials
681
+ }
682
+ }
683
+ return parsed as T;
684
+ } catch {
685
+ // Silently fail for partial JSON
686
+ }
687
+
688
+ return undefined;
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Streaming structured output result.
694
+ * Each partial yield from the generator is a validated object (possibly partial).
695
+ * The final return value is the complete validated object.
696
+ */
697
+ export interface StreamingStructuredResult<T> {
698
+ /** Whether this is partial (incomplete) data */
699
+ partial: boolean;
700
+ /** The validated partial or complete object */
701
+ value: T;
702
+ }