universal-llm-client 4.1.0 → 4.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 (103) hide show
  1. package/CHANGELOG.md +139 -103
  2. package/LICENSE +21 -21
  3. package/README.md +591 -591
  4. package/dist/ai-model.js.map +1 -1
  5. package/dist/auditor.js.map +1 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/http.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/interfaces.d.ts +20 -0
  13. package/dist/interfaces.d.ts.map +1 -1
  14. package/dist/interfaces.js.map +1 -1
  15. package/dist/mcp.js.map +1 -1
  16. package/dist/providers/anthropic.js.map +1 -1
  17. package/dist/providers/google.d.ts.map +1 -1
  18. package/dist/providers/google.js +2 -0
  19. package/dist/providers/google.js.map +1 -1
  20. package/dist/providers/index.js.map +1 -1
  21. package/dist/providers/ollama.js.map +1 -1
  22. package/dist/providers/openai.js.map +1 -1
  23. package/dist/router.js.map +1 -1
  24. package/dist/stream-decoder.js.map +1 -1
  25. package/dist/structured-output.d.ts +24 -1
  26. package/dist/structured-output.d.ts.map +1 -1
  27. package/dist/structured-output.js +58 -5
  28. package/dist/structured-output.js.map +1 -1
  29. package/dist/tools.js.map +1 -1
  30. package/dist/zod-adapter.js.map +1 -1
  31. package/package.json +115 -116
  32. package/src/ai-model.ts +0 -350
  33. package/src/auditor.ts +0 -213
  34. package/src/client.ts +0 -402
  35. package/src/debug/debug-google-streaming.ts +0 -97
  36. package/src/debug/debug-tool-execution.ts +0 -86
  37. package/src/debug/test-lmstudio-tools.ts +0 -155
  38. package/src/demos/README.md +0 -47
  39. package/src/demos/basic/universal-llm-examples.ts +0 -161
  40. package/src/demos/mcp/astrid-memory-demo.ts +0 -295
  41. package/src/demos/mcp/astrid-persona-memory.ts +0 -357
  42. package/src/demos/mcp/mcp-mongodb-demo.ts +0 -275
  43. package/src/demos/mcp/simple-astrid-memory.ts +0 -148
  44. package/src/demos/mcp/simple-mcp-demo.ts +0 -68
  45. package/src/demos/mcp/working-mcp-demo.ts +0 -62
  46. package/src/demos/model-alias-demo.ts +0 -0
  47. package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +0 -267
  48. package/src/demos/tools/astrid-memory-demo.ts +0 -270
  49. package/src/demos/tools/astrid-production-memory-clean.ts +0 -785
  50. package/src/demos/tools/astrid-production-memory.ts +0 -558
  51. package/src/demos/tools/basic-translation-test.ts +0 -66
  52. package/src/demos/tools/chromadb-similarity-tuning.ts +0 -390
  53. package/src/demos/tools/clean-multilingual-conversation.ts +0 -209
  54. package/src/demos/tools/clean-translation-test.ts +0 -119
  55. package/src/demos/tools/clean-universal-multilingual-test.ts +0 -131
  56. package/src/demos/tools/complete-rag-demo.ts +0 -369
  57. package/src/demos/tools/complete-tool-demo.ts +0 -132
  58. package/src/demos/tools/demo-tool-calling.ts +0 -124
  59. package/src/demos/tools/dynamic-language-switching-test.ts +0 -251
  60. package/src/demos/tools/hybrid-thinking-test.ts +0 -154
  61. package/src/demos/tools/memory-integration-test.ts +0 -420
  62. package/src/demos/tools/multilingual-memory-system.ts +0 -802
  63. package/src/demos/tools/ondemand-translation-demo.ts +0 -655
  64. package/src/demos/tools/production-tool-demo.ts +0 -245
  65. package/src/demos/tools/revolutionary-multilingual-test.ts +0 -151
  66. package/src/demos/tools/rigorous-language-analysis.ts +0 -218
  67. package/src/demos/tools/test-universal-memory-system.ts +0 -126
  68. package/src/demos/tools/translation-integration-guide.ts +0 -346
  69. package/src/demos/tools/universal-memory-system.ts +0 -560
  70. package/src/http.ts +0 -247
  71. package/src/index.ts +0 -160
  72. package/src/interfaces.ts +0 -657
  73. package/src/mcp.ts +0 -345
  74. package/src/providers/anthropic.ts +0 -762
  75. package/src/providers/google.ts +0 -620
  76. package/src/providers/index.ts +0 -8
  77. package/src/providers/ollama.ts +0 -469
  78. package/src/providers/openai.ts +0 -392
  79. package/src/router.ts +0 -780
  80. package/src/stream-decoder.ts +0 -361
  81. package/src/structured-output.ts +0 -702
  82. package/src/test-scripts/test-advanced-tools.ts +0 -310
  83. package/src/test-scripts/test-google-streaming-enhanced.ts +0 -147
  84. package/src/test-scripts/test-google-streaming.ts +0 -63
  85. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +0 -189
  86. package/src/test-scripts/test-mcp-config.ts +0 -28
  87. package/src/test-scripts/test-mcp-connection.ts +0 -29
  88. package/src/test-scripts/test-system-message-positions.ts +0 -163
  89. package/src/test-scripts/test-system-prompt-improvement-demo.ts +0 -83
  90. package/src/test-scripts/test-tool-calling.ts +0 -231
  91. package/src/tests/ai-model.test.ts +0 -1614
  92. package/src/tests/auditor.test.ts +0 -224
  93. package/src/tests/http.test.ts +0 -200
  94. package/src/tests/interfaces.test.ts +0 -117
  95. package/src/tests/providers/google.test.ts +0 -660
  96. package/src/tests/providers/ollama.test.ts +0 -954
  97. package/src/tests/providers/openai.test.ts +0 -1122
  98. package/src/tests/router.test.ts +0 -254
  99. package/src/tests/stream-decoder.test.ts +0 -179
  100. package/src/tests/structured-output.test.ts +0 -1340
  101. package/src/tests/tools.test.ts +0 -175
  102. package/src/tools.ts +0 -246
  103. package/src/zod-adapter.ts +0 -72
package/src/auditor.ts DELETED
@@ -1,213 +0,0 @@
1
- /**
2
- * Universal LLM Client v3 — Auditor (Observability)
3
- *
4
- * Every LLM interaction (request, response, tool call, retry, failover)
5
- * is recorded through the Auditor interface. Frameworks inject their own
6
- * Auditor for dashboards, cost tracking, or behavioral scoring.
7
- */
8
-
9
- import type { TokenUsageInfo, ToolExecutionResult } from './interfaces.js';
10
-
11
- // ============================================================================
12
- // Audit Event
13
- // ============================================================================
14
-
15
- export type AuditEventType =
16
- | 'request'
17
- | 'response'
18
- | 'stream_start'
19
- | 'stream_end'
20
- | 'tool_call'
21
- | 'tool_result'
22
- | 'error'
23
- | 'retry'
24
- | 'failover'
25
- | 'structured_request'
26
- | 'structured_response'
27
- | 'structured_validation_error';
28
-
29
- export interface AuditEvent {
30
- /** Unix timestamp in ms */
31
- timestamp: number;
32
- /** Event type */
33
- type: AuditEventType;
34
- /** Provider that generated this event */
35
- provider?: string;
36
- /** Model name */
37
- model?: string;
38
- /** Duration in ms (for request/response pairs) */
39
- duration?: number;
40
- /** Token usage (for response events) */
41
- usage?: TokenUsageInfo;
42
- /** Tool execution details (for tool_call/tool_result events) */
43
- toolExecution?: ToolExecutionResult;
44
- /** Error message (for error/retry events) */
45
- error?: string;
46
- /** Arbitrary metadata for framework-specific data */
47
- metadata?: Record<string, unknown>;
48
- /** Schema name for structured output events */
49
- schemaName?: string;
50
- /** Raw output snippet for validation errors */
51
- rawOutput?: string;
52
- }
53
-
54
- // ============================================================================
55
- // Auditor Interface
56
- // ============================================================================
57
-
58
- /**
59
- * Interface for LLM observability.
60
- *
61
- * Implement this to capture all LLM lifecycle events.
62
- * The library calls `record()` at every interaction point.
63
- */
64
- export interface Auditor {
65
- /** Record an audit event */
66
- record(event: AuditEvent): void;
67
- /** Flush any buffered events (optional) */
68
- flush?(): Promise<void>;
69
- }
70
-
71
- // ============================================================================
72
- // Built-in Auditors
73
- // ============================================================================
74
-
75
- /**
76
- * Zero-overhead auditor that discards all events.
77
- * Used as the default when no auditor is configured.
78
- */
79
- export class NoopAuditor implements Auditor {
80
- record(_event: AuditEvent): void {
81
- // Intentionally empty
82
- }
83
- }
84
-
85
- /**
86
- * Structured console logging auditor.
87
- * Useful for development and debugging.
88
- */
89
- export class ConsoleAuditor implements Auditor {
90
- private prefix: string;
91
-
92
- constructor(prefix: string = '[LLM]') {
93
- this.prefix = prefix;
94
- }
95
-
96
- record(event: AuditEvent): void {
97
- const parts = [
98
- this.prefix,
99
- event.type.toUpperCase(),
100
- event.provider ? `[${event.provider}]` : '',
101
- event.model ? `(${event.model})` : '',
102
- ].filter(Boolean);
103
-
104
- switch (event.type) {
105
- case 'request':
106
- console.log(parts.join(' '), '→');
107
- break;
108
- case 'response':
109
- console.log(
110
- parts.join(' '),
111
- event.duration ? `${event.duration}ms` : '',
112
- event.usage ? `${event.usage.totalTokens} tokens` : '',
113
- );
114
- break;
115
- case 'stream_start':
116
- console.log(parts.join(' '), 'streaming...');
117
- break;
118
- case 'stream_end':
119
- console.log(
120
- parts.join(' '),
121
- 'done',
122
- event.duration ? `${event.duration}ms` : '',
123
- );
124
- break;
125
- case 'tool_call':
126
- console.log(parts.join(' '), event.toolExecution?.tool_call_id ?? '');
127
- break;
128
- case 'tool_result':
129
- console.log(
130
- parts.join(' '),
131
- event.toolExecution?.error ? '❌' : '✅',
132
- event.toolExecution?.duration ? `${event.toolExecution.duration}ms` : '',
133
- );
134
- break;
135
- case 'error':
136
- console.error(parts.join(' '), event.error ?? 'Unknown error');
137
- break;
138
- case 'retry':
139
- console.warn(parts.join(' '), event.error ?? '', event.metadata ?? '');
140
- break;
141
- case 'failover':
142
- console.warn(parts.join(' '), '→', event.metadata?.['nextProvider'] ?? '');
143
- break;
144
- case 'structured_request':
145
- console.log(
146
- parts.join(' '),
147
- `schema=${event.schemaName ?? 'unknown'}`,
148
- '→',
149
- );
150
- break;
151
- case 'structured_response':
152
- console.log(
153
- parts.join(' '),
154
- event.duration ? `${event.duration}ms` : '',
155
- `schema=${event.schemaName ?? 'unknown'}`,
156
- );
157
- break;
158
- case 'structured_validation_error':
159
- console.error(
160
- parts.join(' '),
161
- `schema=${event.schemaName ?? 'unknown'}`,
162
- event.error ?? 'Validation failed',
163
- event.rawOutput ? `raw=${event.rawOutput.slice(0, 50)}...` : '',
164
- );
165
- break;
166
- }
167
- }
168
- }
169
-
170
- /**
171
- * Buffered auditor that collects events for batch processing.
172
- * Useful for custom sinks (OpenTelemetry, DataDog, databases, etc.)
173
- */
174
- export class BufferedAuditor implements Auditor {
175
- private events: AuditEvent[] = [];
176
- private maxBufferSize: number;
177
- private onFlush?: (events: AuditEvent[]) => Promise<void>;
178
-
179
- constructor(options: {
180
- maxBufferSize?: number;
181
- onFlush?: (events: AuditEvent[]) => Promise<void>;
182
- } = {}) {
183
- this.maxBufferSize = options.maxBufferSize ?? 1000;
184
- this.onFlush = options.onFlush;
185
- }
186
-
187
- record(event: AuditEvent): void {
188
- this.events.push(event);
189
- if (this.events.length >= this.maxBufferSize) {
190
- // Auto-flush when buffer is full (fire and forget)
191
- this.flush().catch(() => {});
192
- }
193
- }
194
-
195
- /** Get all buffered events */
196
- getEvents(): ReadonlyArray<AuditEvent> {
197
- return this.events;
198
- }
199
-
200
- /** Flush buffered events to the configured sink */
201
- async flush(): Promise<void> {
202
- if (this.events.length === 0) return;
203
- const batch = this.events.splice(0);
204
- if (this.onFlush) {
205
- await this.onFlush(batch);
206
- }
207
- }
208
-
209
- /** Clear all buffered events without flushing */
210
- clear(): void {
211
- this.events.length = 0;
212
- }
213
- }
package/src/client.ts DELETED
@@ -1,402 +0,0 @@
1
- /**
2
- * Universal LLM Client v3 — Base LLM Client
3
- *
4
- * Abstract base class for all LLM providers.
5
- * Handles tool registration, execution, and the autonomous
6
- * multi-turn tool execution loop.
7
- */
8
-
9
- import type {
10
- LLMClientOptions,
11
- LLMChatMessage,
12
- LLMChatResponse,
13
- LLMToolDefinition,
14
- LLMToolCall,
15
- LLMFunction,
16
- ToolRegistry,
17
- ToolHandler,
18
- ToolExecutionResult,
19
- ChatOptions,
20
- ModelMetadata,
21
- } from './interfaces.js';
22
- import {
23
- StructuredOutputError,
24
- type StructuredOutputOptions,
25
- type SchemaConfig,
26
- } from './structured-output.js';
27
- import type { DecodedEvent } from './stream-decoder.js';
28
- import type { Auditor } from './auditor.js';
29
- import { NoopAuditor } from './auditor.js';
30
-
31
- // ============================================================================
32
- // Abstract Base Client
33
- // ============================================================================
34
-
35
- export abstract class BaseLLMClient {
36
- protected options: LLMClientOptions;
37
- protected toolRegistry: ToolRegistry = {};
38
- protected auditor: Auditor;
39
- protected debug: boolean;
40
-
41
- constructor(options: LLMClientOptions, auditor?: Auditor) {
42
- this.options = options;
43
- this.auditor = auditor ?? new NoopAuditor();
44
- this.debug = options.debug ?? false;
45
- }
46
-
47
- // ========================================================================
48
- // Abstract Methods (implemented by providers)
49
- // ========================================================================
50
-
51
- /** Send a chat request and get a response */
52
- abstract chat(
53
- messages: LLMChatMessage[],
54
- options?: ChatOptions,
55
- ): Promise<LLMChatResponse>;
56
-
57
- /** Stream a chat response as decoded events */
58
- abstract chatStream(
59
- messages: LLMChatMessage[],
60
- options?: ChatOptions,
61
- ): AsyncGenerator<DecodedEvent, LLMChatResponse | void, unknown>;
62
-
63
- /** Get available models */
64
- abstract getModels(): Promise<string[]>;
65
-
66
- /** Generate embeddings for text */
67
- abstract embed(text: string): Promise<number[]>;
68
-
69
- /** Generate embeddings for multiple texts */
70
- async embedArray(texts: string[]): Promise<number[][]> {
71
- return Promise.all(texts.map(t => this.embed(t)));
72
- }
73
-
74
- /**
75
- * Get metadata about a model (context length, architecture, etc.)
76
- * Override per-provider for accurate data.
77
- */
78
- async getModelInfo(_modelName?: string): Promise<ModelMetadata> {
79
- return { contextLength: 8192 }; // Conservative default
80
- }
81
-
82
- // ========================================================================
83
- // Tool Registration
84
- // ========================================================================
85
-
86
- /**
87
- * Sanitize tool name for LLM compatibility.
88
- * LLM APIs require function names matching [a-zA-Z0-9_-].
89
- * Module-prefixed names like "@core/computer:list_windows" are cleaned.
90
- */
91
- private sanitizeToolName(name: string): string {
92
- return name
93
- .replace(/^@[^:]+:/, '') // Strip module prefix
94
- .replace(/[^a-zA-Z0-9_-]/g, '_') // Replace illegal chars
95
- .replace(/_+/g, '_') // Collapse
96
- .replace(/^_|_$/g, ''); // Trim
97
- }
98
-
99
- /** Register a tool/function callable by the model */
100
- registerTool(
101
- name: string,
102
- description: string,
103
- parameters: LLMFunction['parameters'],
104
- handler: ToolHandler,
105
- ): void {
106
- const safeName = this.sanitizeToolName(name);
107
- this.toolRegistry[name] = {
108
- definition: { name: safeName, description, parameters },
109
- handler,
110
- };
111
- // Index by sanitized name for reverse lookup
112
- if (safeName !== name && !this.toolRegistry[safeName]) {
113
- this.toolRegistry[safeName] = this.toolRegistry[name]!;
114
- }
115
- this.debugLog(`Registered tool: ${name} (LLM name: ${safeName})`);
116
- }
117
-
118
- /** Register multiple tools at once */
119
- registerTools(
120
- tools: Array<{
121
- name: string;
122
- description: string;
123
- parameters: LLMFunction['parameters'];
124
- handler: ToolHandler;
125
- }>,
126
- ): void {
127
- for (const tool of tools) {
128
- this.registerTool(tool.name, tool.description, tool.parameters, tool.handler);
129
- }
130
- }
131
-
132
- /** Get all registered tool definitions (deduplicated by sanitized name) */
133
- getToolDefinitions(): LLMToolDefinition[] {
134
- const seen = new Set<string>();
135
- const defs: LLMToolDefinition[] = [];
136
- for (const { definition } of Object.values(this.toolRegistry)) {
137
- if (seen.has(definition.name)) continue;
138
- seen.add(definition.name);
139
- defs.push({ type: 'function' as const, function: definition });
140
- }
141
- return defs;
142
- }
143
-
144
- // ========================================================================
145
- // Tool Execution
146
- // ========================================================================
147
-
148
- /** Execute a single tool call with fuzzy name matching */
149
- async executeTool(toolCall: LLMToolCall): Promise<ToolExecutionResult> {
150
- const toolName = toolCall.function.name;
151
- const start = Date.now();
152
- let tool = this.toolRegistry[toolName];
153
-
154
- // Fuzzy lookup: try suffix match (LLM stripped module prefix)
155
- if (!tool) {
156
- const entries = Object.entries(this.toolRegistry);
157
- const bySuffix = entries.find(([k]) => k.endsWith(`:${toolName}`));
158
- if (bySuffix) {
159
- tool = bySuffix[1];
160
- this.debugLog(`Fuzzy tool match: "${toolName}" → "${bySuffix[0]}"`);
161
- }
162
-
163
- // Try prefix match: if only one tool in that module, use it
164
- if (!tool) {
165
- const byPrefix = entries.filter(([k]) => k.startsWith(`${toolName}:`));
166
- if (byPrefix.length === 1) {
167
- tool = byPrefix[0]![1];
168
- this.debugLog(`Fuzzy tool match (single): "${toolName}" → "${byPrefix[0]![0]}"`);
169
- }
170
- }
171
- }
172
-
173
- if (!tool) {
174
- const result: ToolExecutionResult = {
175
- tool_call_id: toolCall.id,
176
- output: null,
177
- error: `Unknown tool: ${toolName}`,
178
- duration: Date.now() - start,
179
- };
180
- this.auditor.record({
181
- timestamp: Date.now(),
182
- type: 'tool_result',
183
- toolExecution: result,
184
- error: result.error,
185
- });
186
- return result;
187
- }
188
-
189
- this.auditor.record({
190
- timestamp: Date.now(),
191
- type: 'tool_call',
192
- metadata: { toolName, arguments: toolCall.function.arguments },
193
- });
194
-
195
- try {
196
- const args = JSON.parse(toolCall.function.arguments);
197
- const output = await tool.handler(args);
198
- const result: ToolExecutionResult = {
199
- tool_call_id: toolCall.id,
200
- output,
201
- duration: Date.now() - start,
202
- };
203
- this.auditor.record({
204
- timestamp: Date.now(),
205
- type: 'tool_result',
206
- toolExecution: result,
207
- });
208
- return result;
209
- } catch (error) {
210
- const result: ToolExecutionResult = {
211
- tool_call_id: toolCall.id,
212
- output: null,
213
- error: error instanceof Error ? error.message : String(error),
214
- duration: Date.now() - start,
215
- };
216
- this.auditor.record({
217
- timestamp: Date.now(),
218
- type: 'tool_result',
219
- toolExecution: result,
220
- error: result.error,
221
- });
222
- return result;
223
- }
224
- }
225
-
226
- /** Execute multiple tool calls in parallel */
227
- async executeTools(toolCalls: LLMToolCall[]): Promise<ToolExecutionResult[]> {
228
- return Promise.all(toolCalls.map(tc => this.executeTool(tc)));
229
- }
230
-
231
- // ========================================================================
232
- // Chat with Tools (multi-turn autonomous loop)
233
- // ========================================================================
234
-
235
- /**
236
- * Chat with automatic tool execution.
237
- * Continues until the model stops calling tools or max iterations reached.
238
- * Returns the complete execution trace in `toolExecutions`.
239
- */
240
- async chatWithTools(
241
- messages: LLMChatMessage[],
242
- options?: ChatOptions & { maxIterations?: number },
243
- ): Promise<LLMChatResponse> {
244
- const maxIterations = options?.maxIterations ?? 10;
245
- const conversationMessages = [...messages];
246
- const allToolExecutions: ToolExecutionResult[] = [];
247
- let iterations = 0;
248
-
249
- while (iterations < maxIterations) {
250
- const response = await this.chat(conversationMessages, {
251
- ...options,
252
- tools: this.getToolDefinitions(),
253
- });
254
-
255
- // If no tool calls, return with full trace
256
- if (!response.message.tool_calls?.length) {
257
- return {
258
- ...response,
259
- toolExecutions: allToolExecutions.length > 0 ? allToolExecutions : undefined,
260
- };
261
- }
262
-
263
- // Add assistant message with tool calls
264
- conversationMessages.push(response.message);
265
-
266
- // Execute tools in parallel
267
- const toolResults = await this.executeTools(response.message.tool_calls);
268
- allToolExecutions.push(...toolResults);
269
-
270
- // Add tool results as messages
271
- for (const result of toolResults) {
272
- conversationMessages.push({
273
- role: 'tool',
274
- content: typeof result.output === 'string'
275
- ? result.output
276
- : JSON.stringify(result.output),
277
- tool_call_id: result.tool_call_id,
278
- });
279
- }
280
-
281
- iterations++;
282
- }
283
-
284
- // Max iterations — final call without tools
285
- const finalResponse = await this.chat(conversationMessages);
286
- return {
287
- ...finalResponse,
288
- toolExecutions: allToolExecutions,
289
- };
290
- }
291
-
292
- // ========================================================================
293
- // Helpers
294
- // ========================================================================
295
-
296
- /** Set the model name at runtime */
297
- setModel(modelName: string): void {
298
- this.options.model = modelName;
299
- this.debugLog(`Model switched to: ${modelName}`);
300
- }
301
-
302
- /** Get the current model name */
303
- get model(): string {
304
- return this.options.model;
305
- }
306
-
307
- /** Get the base URL */
308
- get url(): string {
309
- return this.options.url;
310
- }
311
-
312
- /** Set the auditor instance */
313
- setAuditor(auditor: Auditor): void {
314
- this.auditor = auditor;
315
- }
316
-
317
- protected debugLog(message: string, data?: unknown): void {
318
- if (this.debug) {
319
- console.log(`[LLM:${this.options.model}] ${message}`, data ?? '');
320
- }
321
- }
322
-
323
- /**
324
- * Generate a unique ID for tool calls when the provider doesn't provide one.
325
- */
326
- protected generateToolCallId(): string {
327
- return `call_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
328
- }
329
-
330
- // ========================================================================
331
- // Structured Output Helpers (shared across all providers)
332
- // ========================================================================
333
-
334
- /**
335
- * Extract schema options from ChatOptions.
336
- * Returns null if no schema is provided.
337
- * Returns a SchemaConfig if a schema was found.
338
- */
339
- protected extractSchemaOptions(options?: ChatOptions): (StructuredOutputOptions<unknown> & { schemaConfig: SchemaConfig<unknown> }) | null {
340
- if (!options) return null;
341
-
342
- if (options.schema) {
343
- return {
344
- schemaConfig: options.schema,
345
- name: options.schemaName,
346
- description: options.schemaDescription,
347
- };
348
- }
349
-
350
- if (options.jsonSchema) {
351
- // Raw JSON Schema without validation
352
- const config: SchemaConfig<unknown> = {
353
- jsonSchema: options.jsonSchema,
354
- };
355
- return {
356
- schemaConfig: config,
357
- name: options.schemaName,
358
- description: options.schemaDescription,
359
- };
360
- }
361
-
362
- return null;
363
- }
364
-
365
- /**
366
- * Validate structured response using a SchemaConfig.
367
- * Throws StructuredOutputError on failure.
368
- */
369
- protected validateStructuredResponse(content: string, config: SchemaConfig<unknown>): void {
370
- if (!content) {
371
- throw new StructuredOutputError(
372
- 'Empty response from LLM',
373
- { rawOutput: content },
374
- );
375
- }
376
-
377
- let parsed: unknown;
378
- try {
379
- parsed = JSON.parse(content);
380
- } catch (error) {
381
- const syntaxError = error instanceof SyntaxError
382
- ? error
383
- : new SyntaxError(String(error));
384
- throw new StructuredOutputError(
385
- `Failed to parse JSON: ${syntaxError.message}`,
386
- { rawOutput: content, cause: syntaxError },
387
- );
388
- }
389
-
390
- if (config.validate) {
391
- try {
392
- config.validate(parsed);
393
- } catch (error) {
394
- const validationError = error instanceof Error ? error : new Error(String(error));
395
- throw new StructuredOutputError(
396
- `Validation failed: ${validationError.message}`,
397
- { rawOutput: content, cause: validationError },
398
- );
399
- }
400
- }
401
- }
402
- }