universal-llm-client 4.0.0 → 4.2.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.
- package/dist/ai-model.d.ts +20 -22
- package/dist/ai-model.d.ts.map +1 -1
- package/dist/ai-model.js +26 -23
- package/dist/ai-model.js.map +1 -1
- package/dist/client.d.ts +5 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -9
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +1 -0
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +49 -11
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js +14 -0
- package/dist/interfaces.js.map +1 -1
- package/dist/providers/anthropic.d.ts +56 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +524 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/google.d.ts +5 -0
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +64 -8
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +38 -11
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +9 -7
- package/dist/providers/openai.js.map +1 -1
- package/dist/router.d.ts +13 -33
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +33 -57
- package/dist/router.js.map +1 -1
- package/dist/stream-decoder.d.ts +29 -2
- package/dist/stream-decoder.d.ts.map +1 -1
- package/dist/stream-decoder.js +39 -11
- package/dist/stream-decoder.js.map +1 -1
- package/dist/structured-output.d.ts +107 -181
- package/dist/structured-output.d.ts.map +1 -1
- package/dist/structured-output.js +137 -192
- package/dist/structured-output.js.map +1 -1
- package/dist/zod-adapter.d.ts +44 -0
- package/dist/zod-adapter.d.ts.map +1 -0
- package/dist/zod-adapter.js +61 -0
- package/dist/zod-adapter.js.map +1 -0
- package/package.json +9 -1
- package/src/ai-model.ts +350 -0
- package/src/auditor.ts +213 -0
- package/src/client.ts +402 -0
- package/src/debug/debug-google-streaming.ts +97 -0
- package/src/debug/debug-tool-execution.ts +86 -0
- package/src/debug/test-lmstudio-tools.ts +155 -0
- package/src/demos/README.md +47 -0
- package/src/demos/basic/universal-llm-examples.ts +161 -0
- package/src/demos/mcp/astrid-memory-demo.ts +295 -0
- package/src/demos/mcp/astrid-persona-memory.ts +357 -0
- package/src/demos/mcp/mcp-mongodb-demo.ts +275 -0
- package/src/demos/mcp/simple-astrid-memory.ts +148 -0
- package/src/demos/mcp/simple-mcp-demo.ts +68 -0
- package/src/demos/mcp/working-mcp-demo.ts +62 -0
- package/src/demos/model-alias-demo.ts +0 -0
- package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +267 -0
- package/src/demos/tools/astrid-memory-demo.ts +270 -0
- package/src/demos/tools/astrid-production-memory-clean.ts +785 -0
- package/src/demos/tools/astrid-production-memory.ts +558 -0
- package/src/demos/tools/basic-translation-test.ts +66 -0
- package/src/demos/tools/chromadb-similarity-tuning.ts +390 -0
- package/src/demos/tools/clean-multilingual-conversation.ts +209 -0
- package/src/demos/tools/clean-translation-test.ts +119 -0
- package/src/demos/tools/clean-universal-multilingual-test.ts +131 -0
- package/src/demos/tools/complete-rag-demo.ts +369 -0
- package/src/demos/tools/complete-tool-demo.ts +132 -0
- package/src/demos/tools/demo-tool-calling.ts +124 -0
- package/src/demos/tools/dynamic-language-switching-test.ts +251 -0
- package/src/demos/tools/hybrid-thinking-test.ts +154 -0
- package/src/demos/tools/memory-integration-test.ts +420 -0
- package/src/demos/tools/multilingual-memory-system.ts +802 -0
- package/src/demos/tools/ondemand-translation-demo.ts +655 -0
- package/src/demos/tools/production-tool-demo.ts +245 -0
- package/src/demos/tools/revolutionary-multilingual-test.ts +151 -0
- package/src/demos/tools/rigorous-language-analysis.ts +218 -0
- package/src/demos/tools/test-universal-memory-system.ts +126 -0
- package/src/demos/tools/translation-integration-guide.ts +346 -0
- package/src/demos/tools/universal-memory-system.ts +560 -0
- package/src/http.ts +247 -0
- package/src/index.ts +161 -0
- package/src/interfaces.ts +657 -0
- package/src/mcp.ts +345 -0
- package/src/providers/anthropic.ts +762 -0
- package/src/providers/google.ts +620 -0
- package/src/providers/index.ts +8 -0
- package/src/providers/ollama.ts +469 -0
- package/src/providers/openai.ts +392 -0
- package/src/router.ts +780 -0
- package/src/stream-decoder.ts +361 -0
- package/src/structured-output.ts +759 -0
- package/src/test-scripts/test-advanced-tools.ts +310 -0
- package/src/test-scripts/test-google-streaming-enhanced.ts +147 -0
- package/src/test-scripts/test-google-streaming.ts +63 -0
- package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -0
- package/src/test-scripts/test-mcp-config.ts +28 -0
- package/src/test-scripts/test-mcp-connection.ts +29 -0
- package/src/test-scripts/test-system-message-positions.ts +163 -0
- package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -0
- package/src/test-scripts/test-tool-calling.ts +231 -0
- package/src/tests/ai-model.test.ts +1614 -0
- package/src/tests/auditor.test.ts +224 -0
- package/src/tests/http.test.ts +200 -0
- package/src/tests/interfaces.test.ts +117 -0
- package/src/tests/providers/google.test.ts +660 -0
- package/src/tests/providers/ollama.test.ts +954 -0
- package/src/tests/providers/openai.test.ts +1122 -0
- package/src/tests/router.test.ts +254 -0
- package/src/tests/stream-decoder.test.ts +179 -0
- package/src/tests/structured-output.test.ts +1450 -0
- package/src/tests/tools.test.ts +175 -0
- package/src/tools.ts +246 -0
- package/src/zod-adapter.ts +72 -0
|
@@ -0,0 +1,759 @@
|
|
|
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
|
+
* Strip markdown code fences from raw LLM output.
|
|
454
|
+
*
|
|
455
|
+
* Some providers/models — notably Gemma variants via Ollama — wrap
|
|
456
|
+
* structured JSON output in ` ```json ... ``` ` fences even when a JSON
|
|
457
|
+
* schema is passed via the `format` field. This helper extracts the
|
|
458
|
+
* inner payload so it can be fed to `JSON.parse`.
|
|
459
|
+
*
|
|
460
|
+
* Behavior:
|
|
461
|
+
* - If the input contains no triple-backtick fences, the input is
|
|
462
|
+
* returned unchanged (zero risk of disturbing well-formed output).
|
|
463
|
+
* - Otherwise, the first fenced block is extracted and returned trimmed.
|
|
464
|
+
* Language tags (` ```json `, ` ```JSON `, etc.) are handled.
|
|
465
|
+
* - Callers should still treat the return value as untrusted — it may
|
|
466
|
+
* not be valid JSON (e.g. the model emitted prose, not JSON).
|
|
467
|
+
*
|
|
468
|
+
* This helper is idempotent: `stripJsonFences(stripJsonFences(x)) === stripJsonFences(x)`.
|
|
469
|
+
*
|
|
470
|
+
* @param rawOutput The raw LLM response text
|
|
471
|
+
* @returns Fence-stripped content if fences were present, otherwise `rawOutput` unchanged
|
|
472
|
+
*/
|
|
473
|
+
export function stripJsonFences(rawOutput: string): string {
|
|
474
|
+
if (!rawOutput.includes('```')) return rawOutput;
|
|
475
|
+
|
|
476
|
+
// Match the first fenced block, optionally with a language tag.
|
|
477
|
+
// The non-greedy `[\s\S]*?` keeps us from swallowing content after
|
|
478
|
+
// the first closing fence. Whitespace/newlines around the payload
|
|
479
|
+
// are tolerated so we accept both inline and multi-line fences.
|
|
480
|
+
const fenceMatch = rawOutput.match(
|
|
481
|
+
/```(?:[a-zA-Z0-9_-]*)[ \t]*\n?([\s\S]*?)\n?[ \t]*```/,
|
|
482
|
+
);
|
|
483
|
+
if (fenceMatch && fenceMatch[1] !== undefined) {
|
|
484
|
+
return fenceMatch[1].trim();
|
|
485
|
+
}
|
|
486
|
+
return rawOutput;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Parse and validate structured output from raw LLM response text.
|
|
491
|
+
*
|
|
492
|
+
* This function:
|
|
493
|
+
* 1. Parses JSON from the raw output string (with fallback fence stripping
|
|
494
|
+
* for models that wrap structured output in ` ```json ... ``` `)
|
|
495
|
+
* 2. Validates using the SchemaConfig's validate function (if provided)
|
|
496
|
+
* 3. Throws StructuredOutputError on failure
|
|
497
|
+
*
|
|
498
|
+
* @param config The schema configuration with optional validator
|
|
499
|
+
* @param rawOutput The raw string output from the LLM
|
|
500
|
+
* @returns The validated and typed data
|
|
501
|
+
* @throws StructuredOutputError if JSON parsing fails or validation fails
|
|
502
|
+
*/
|
|
503
|
+
export function parseStructured<T>(
|
|
504
|
+
config: SchemaConfig<T>,
|
|
505
|
+
rawOutput: string,
|
|
506
|
+
): T {
|
|
507
|
+
// Step 1: Parse JSON. Try the raw output first — well-behaved providers
|
|
508
|
+
// return clean JSON and we don't want to pay any cost in the fast path.
|
|
509
|
+
// If that fails, fall back to stripping markdown code fences before
|
|
510
|
+
// re-trying; some models (e.g. Gemma via Ollama) wrap structured output
|
|
511
|
+
// in ` ```json ... ``` ` even when a JSON schema is requested.
|
|
512
|
+
let parsed: unknown;
|
|
513
|
+
let parseError: SyntaxError | undefined;
|
|
514
|
+
try {
|
|
515
|
+
parsed = JSON.parse(rawOutput);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
parseError = error instanceof SyntaxError
|
|
518
|
+
? error
|
|
519
|
+
: new SyntaxError(String(error));
|
|
520
|
+
|
|
521
|
+
const fenceStripped = stripJsonFences(rawOutput);
|
|
522
|
+
if (fenceStripped !== rawOutput) {
|
|
523
|
+
try {
|
|
524
|
+
parsed = JSON.parse(fenceStripped);
|
|
525
|
+
parseError = undefined;
|
|
526
|
+
} catch {
|
|
527
|
+
// Fence stripping didn't help — fall through to throw the
|
|
528
|
+
// original parse error so error messages reflect what the
|
|
529
|
+
// model actually emitted, not the post-processed variant.
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (parseError) {
|
|
535
|
+
throw new StructuredOutputError(
|
|
536
|
+
`Failed to parse JSON: ${parseError.message}`,
|
|
537
|
+
{ rawOutput, cause: parseError },
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Step 2: Validate if validator is provided
|
|
542
|
+
if (config.validate) {
|
|
543
|
+
try {
|
|
544
|
+
return config.validate(parsed);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const validationError = error instanceof Error ? error : new Error(String(error));
|
|
547
|
+
throw new StructuredOutputError(
|
|
548
|
+
`Validation failed: ${validationError.message}`,
|
|
549
|
+
{ rawOutput, cause: validationError },
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// No validator — return as-is (unsafe cast, user chose to skip validation)
|
|
555
|
+
return parsed as T;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Try to parse and validate structured output, returning a result object.
|
|
560
|
+
*
|
|
561
|
+
* This is the non-throwing variant of `parseStructured`. Instead of throwing
|
|
562
|
+
* on validation failure, it returns a result object with `ok: false` and
|
|
563
|
+
* the error details.
|
|
564
|
+
*
|
|
565
|
+
* @param config The schema configuration with optional validator
|
|
566
|
+
* @param rawOutput The raw string output from the LLM
|
|
567
|
+
* @returns A result object: `{ ok: true, value }` on success, `{ ok: false, error, rawOutput }` on failure
|
|
568
|
+
*/
|
|
569
|
+
export function tryParseStructured<T>(
|
|
570
|
+
config: SchemaConfig<T>,
|
|
571
|
+
rawOutput: string,
|
|
572
|
+
): StructuredOutputResult<T> {
|
|
573
|
+
try {
|
|
574
|
+
const value = parseStructured(config, rawOutput);
|
|
575
|
+
return { ok: true, value };
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (error instanceof StructuredOutputError) {
|
|
578
|
+
return {
|
|
579
|
+
ok: false,
|
|
580
|
+
error,
|
|
581
|
+
rawOutput,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
// Re-throw unexpected errors
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Validate already-parsed data using a SchemaConfig's validator.
|
|
591
|
+
*
|
|
592
|
+
* This is useful when you have already parsed JSON and need to validate it.
|
|
593
|
+
*
|
|
594
|
+
* @param config The schema configuration with optional validator
|
|
595
|
+
* @param data The parsed data to validate
|
|
596
|
+
* @param rawOutput Optional raw output string for error messages
|
|
597
|
+
* @returns The validated and typed data
|
|
598
|
+
* @throws StructuredOutputError if validation fails
|
|
599
|
+
*/
|
|
600
|
+
export function validateStructuredOutput<T>(
|
|
601
|
+
config: SchemaConfig<T>,
|
|
602
|
+
data: unknown,
|
|
603
|
+
rawOutput?: string,
|
|
604
|
+
): T {
|
|
605
|
+
if (config.validate) {
|
|
606
|
+
try {
|
|
607
|
+
return config.validate(data);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
const rawData = rawOutput ?? JSON.stringify(data);
|
|
610
|
+
const validationError = error instanceof Error ? error : new Error(String(error));
|
|
611
|
+
throw new StructuredOutputError(
|
|
612
|
+
`Validation failed: ${validationError.message}`,
|
|
613
|
+
{ rawOutput: rawData, cause: validationError },
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return data as T;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ============================================================================
|
|
621
|
+
// Streaming JSON Parsing
|
|
622
|
+
// ============================================================================
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Incremental JSON parser for streaming structured output.
|
|
626
|
+
*
|
|
627
|
+
* Allows parsing partial JSON as it streams in, returning validated partial
|
|
628
|
+
* objects when possible. Useful for structured output streaming where you
|
|
629
|
+
* want to see partial results before the complete JSON arrives.
|
|
630
|
+
*/
|
|
631
|
+
export class StreamingJsonParser<T> {
|
|
632
|
+
private buffer = '';
|
|
633
|
+
private readonly validateFn?: (data: unknown) => T;
|
|
634
|
+
|
|
635
|
+
constructor(config: SchemaConfig<T>) {
|
|
636
|
+
this.validateFn = config.validate;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Feed a chunk of JSON text to the parser.
|
|
641
|
+
* Returns a validated partial object if the current buffer can be parsed
|
|
642
|
+
* as valid JSON that passes validation, or undefined if not yet valid.
|
|
643
|
+
*/
|
|
644
|
+
feed(chunk: string): { partial: T | undefined; complete: boolean } {
|
|
645
|
+
this.buffer += chunk;
|
|
646
|
+
|
|
647
|
+
// Try to parse as complete JSON first
|
|
648
|
+
try {
|
|
649
|
+
const parsed = JSON.parse(this.buffer);
|
|
650
|
+
if (this.validateFn) {
|
|
651
|
+
try {
|
|
652
|
+
const validated = this.validateFn(parsed);
|
|
653
|
+
return { partial: validated, complete: true };
|
|
654
|
+
} catch {
|
|
655
|
+
// Validation failed on complete JSON — return parsed but not validated
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return { partial: parsed as T, complete: true };
|
|
659
|
+
} catch {
|
|
660
|
+
// Not yet valid complete JSON
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Try to create a valid partial by closing braces
|
|
664
|
+
const partialResult = this.tryParsePartial();
|
|
665
|
+
return { partial: partialResult, complete: false };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Get the current buffer content.
|
|
670
|
+
*/
|
|
671
|
+
getBuffer(): string {
|
|
672
|
+
return this.buffer;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Reset the parser state.
|
|
677
|
+
*/
|
|
678
|
+
reset(): void {
|
|
679
|
+
this.buffer = '';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Attempt to parse partial JSON by adding closing brackets.
|
|
684
|
+
*/
|
|
685
|
+
private tryParsePartial(): T | undefined {
|
|
686
|
+
// Count unclosed brackets and braces
|
|
687
|
+
let braceCount = 0;
|
|
688
|
+
let bracketCount = 0;
|
|
689
|
+
let inString = false;
|
|
690
|
+
let escaped = false;
|
|
691
|
+
|
|
692
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
693
|
+
const char = this.buffer[i];
|
|
694
|
+
|
|
695
|
+
if (escaped) {
|
|
696
|
+
escaped = false;
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (char === '\\' && inString) {
|
|
701
|
+
escaped = true;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (char === '"') {
|
|
706
|
+
inString = !inString;
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (inString) continue;
|
|
711
|
+
|
|
712
|
+
if (char === '{') braceCount++;
|
|
713
|
+
else if (char === '}') braceCount--;
|
|
714
|
+
else if (char === '[') bracketCount++;
|
|
715
|
+
else if (char === ']') bracketCount--;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Build closing sequence
|
|
719
|
+
let closing = '';
|
|
720
|
+
while (bracketCount > 0) {
|
|
721
|
+
closing += ']';
|
|
722
|
+
bracketCount--;
|
|
723
|
+
}
|
|
724
|
+
while (braceCount > 0) {
|
|
725
|
+
closing += '}';
|
|
726
|
+
braceCount--;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Try parsing with closing
|
|
730
|
+
const candidate = this.buffer + closing;
|
|
731
|
+
try {
|
|
732
|
+
const parsed = JSON.parse(candidate);
|
|
733
|
+
if (this.validateFn) {
|
|
734
|
+
try {
|
|
735
|
+
return this.validateFn(parsed);
|
|
736
|
+
} catch {
|
|
737
|
+
// Partial validation failed — that's expected for partials
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return parsed as T;
|
|
741
|
+
} catch {
|
|
742
|
+
// Silently fail for partial JSON
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return undefined;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Streaming structured output result.
|
|
751
|
+
* Each partial yield from the generator is a validated object (possibly partial).
|
|
752
|
+
* The final return value is the complete validated object.
|
|
753
|
+
*/
|
|
754
|
+
export interface StreamingStructuredResult<T> {
|
|
755
|
+
/** Whether this is partial (incomplete) data */
|
|
756
|
+
partial: boolean;
|
|
757
|
+
/** The validated partial or complete object */
|
|
758
|
+
value: T;
|
|
759
|
+
}
|