universal-llm-client 4.2.0 → 4.5.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 (108) hide show
  1. package/CHANGELOG.md +142 -103
  2. package/LICENSE +21 -21
  3. package/README.md +640 -591
  4. package/dist/ai-model.d.ts +12 -1
  5. package/dist/ai-model.d.ts.map +1 -1
  6. package/dist/ai-model.js +36 -1
  7. package/dist/ai-model.js.map +1 -1
  8. package/dist/gemma-channel.d.ts +14 -0
  9. package/dist/gemma-channel.d.ts.map +1 -0
  10. package/dist/gemma-channel.js +38 -0
  11. package/dist/gemma-channel.js.map +1 -0
  12. package/dist/gemma-diffusion.d.ts +49 -0
  13. package/dist/gemma-diffusion.d.ts.map +1 -0
  14. package/dist/gemma-diffusion.js +147 -0
  15. package/dist/gemma-diffusion.js.map +1 -0
  16. package/dist/http.d.ts +4 -0
  17. package/dist/http.d.ts.map +1 -1
  18. package/dist/http.js +14 -1
  19. package/dist/http.js.map +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/interfaces.d.ts +183 -7
  25. package/dist/interfaces.d.ts.map +1 -1
  26. package/dist/interfaces.js.map +1 -1
  27. package/dist/providers/anthropic.d.ts.map +1 -1
  28. package/dist/providers/anthropic.js +28 -3
  29. package/dist/providers/anthropic.js.map +1 -1
  30. package/dist/providers/google.d.ts +22 -1
  31. package/dist/providers/google.d.ts.map +1 -1
  32. package/dist/providers/google.js +225 -13
  33. package/dist/providers/google.js.map +1 -1
  34. package/dist/providers/ollama.d.ts +2 -0
  35. package/dist/providers/ollama.d.ts.map +1 -1
  36. package/dist/providers/ollama.js +59 -30
  37. package/dist/providers/ollama.js.map +1 -1
  38. package/dist/providers/openai.d.ts +14 -0
  39. package/dist/providers/openai.d.ts.map +1 -1
  40. package/dist/providers/openai.js +200 -22
  41. package/dist/providers/openai.js.map +1 -1
  42. package/dist/router.d.ts +2 -0
  43. package/dist/router.d.ts.map +1 -1
  44. package/dist/router.js +4 -0
  45. package/dist/router.js.map +1 -1
  46. package/dist/stream-decoder.d.ts +12 -0
  47. package/dist/stream-decoder.d.ts.map +1 -1
  48. package/dist/stream-decoder.js +182 -5
  49. package/dist/stream-decoder.js.map +1 -1
  50. package/dist/thinking.d.ts +36 -0
  51. package/dist/thinking.d.ts.map +1 -0
  52. package/dist/thinking.js +52 -0
  53. package/dist/thinking.js.map +1 -0
  54. package/package.json +118 -116
  55. package/src/ai-model.ts +400 -350
  56. package/src/auditor.ts +213 -213
  57. package/src/client.ts +402 -402
  58. package/src/debug/debug-google-streaming.ts +1 -1
  59. package/src/demos/basic/universal-llm-examples.ts +3 -3
  60. package/src/demos/diffusion-gemma/.env +29 -0
  61. package/src/demos/diffusion-gemma/.env.example +27 -0
  62. package/src/demos/diffusion-gemma/CLAUDE.md +95 -0
  63. package/src/demos/diffusion-gemma/README.md +59 -0
  64. package/src/demos/diffusion-gemma/canvas.ts +1606 -0
  65. package/src/demos/diffusion-gemma/docker-compose.yml +29 -0
  66. package/src/demos/diffusion-gemma/probe-stream.ts +51 -0
  67. package/src/demos/diffusion-gemma/probe-tools.ts +55 -0
  68. package/src/demos/diffusion-gemma/server.ts +1205 -0
  69. package/src/demos/diffusion-gemma/start-vllm.sh +98 -0
  70. package/src/gemma-channel.ts +47 -0
  71. package/src/gemma-diffusion.ts +167 -0
  72. package/src/http.ts +261 -247
  73. package/src/index.ts +180 -161
  74. package/src/interfaces.ts +843 -657
  75. package/src/mcp.ts +345 -345
  76. package/src/providers/anthropic.ts +796 -762
  77. package/src/providers/google.ts +840 -620
  78. package/src/providers/index.ts +8 -8
  79. package/src/providers/ollama.ts +503 -469
  80. package/src/providers/openai.ts +587 -392
  81. package/src/router.ts +785 -780
  82. package/src/stream-decoder.ts +535 -361
  83. package/src/structured-output.ts +759 -759
  84. package/src/test-scripts/test-google-deep-research.ts +33 -0
  85. package/src/test-scripts/test-google-streaming-enhanced.ts +147 -147
  86. package/src/test-scripts/test-google-streaming.ts +1 -1
  87. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -189
  88. package/src/test-scripts/test-google-thinking.ts +46 -0
  89. package/src/test-scripts/test-system-message-positions.ts +163 -163
  90. package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -83
  91. package/src/test-scripts/test-vllm-qwen36.ts +256 -0
  92. package/src/tests/ai-model.test.ts +1614 -1614
  93. package/src/tests/auditor.test.ts +224 -224
  94. package/src/tests/gemma-diffusion.test.ts +115 -0
  95. package/src/tests/http.test.ts +200 -200
  96. package/src/tests/interfaces.test.ts +117 -117
  97. package/src/tests/providers/anthropic.test.ts +118 -0
  98. package/src/tests/providers/google.test.ts +841 -660
  99. package/src/tests/providers/ollama.test.ts +1034 -954
  100. package/src/tests/providers/openai.test.ts +1511 -1122
  101. package/src/tests/router.test.ts +254 -254
  102. package/src/tests/stream-decoder.test.ts +263 -179
  103. package/src/tests/structured-output.test.ts +1450 -1450
  104. package/src/tests/thinking.test.ts +65 -0
  105. package/src/tests/tools.test.ts +175 -175
  106. package/src/thinking.ts +73 -0
  107. package/src/tools.ts +246 -246
  108. package/src/zod-adapter.ts +72 -72
package/src/http.ts CHANGED
@@ -1,247 +1,261 @@
1
- /**
2
- * Universal LLM Client v3 — HTTP Utilities
3
- *
4
- * Zero-dependency HTTP layer using native fetch.
5
- * Works on Node 22+, Bun, Deno, and browsers.
6
- */
7
-
8
- import type { LLMClientOptions } from './interfaces.js';
9
-
10
- // ============================================================================
11
- // Types
12
- // ============================================================================
13
-
14
- export interface HttpRequestOptions {
15
- method?: 'GET' | 'POST';
16
- headers?: Record<string, string>;
17
- body?: unknown;
18
- timeout?: number;
19
- signal?: AbortSignal;
20
- }
21
-
22
- export interface HttpResponse<T = unknown> {
23
- ok: boolean;
24
- status: number;
25
- data: T;
26
- /** Response headers (when available) */
27
- headers?: Headers;
28
- }
29
-
30
- // ============================================================================
31
- // HTTP Request
32
- // ============================================================================
33
-
34
- /**
35
- * Make an HTTP request with timeout and error handling.
36
- * Uses native fetch (available in all target runtimes).
37
- */
38
- export async function httpRequest<T = unknown>(
39
- url: string,
40
- options: HttpRequestOptions = {},
41
- ): Promise<HttpResponse<T>> {
42
- const { method = 'GET', headers = {}, body, timeout = 30000, signal } = options;
43
-
44
- const controller = new AbortController();
45
- const timeoutId = setTimeout(() => controller.abort(), timeout);
46
-
47
- // Combine external signal with timeout
48
- const combinedSignal = signal
49
- ? AbortSignal.any([signal, controller.signal])
50
- : controller.signal;
51
-
52
- try {
53
- const response = await fetch(url, {
54
- method,
55
- headers: {
56
- 'Content-Type': 'application/json',
57
- ...headers,
58
- },
59
- body: body ? JSON.stringify(body) : undefined,
60
- signal: combinedSignal,
61
- });
62
-
63
- clearTimeout(timeoutId);
64
-
65
- if (!response.ok) {
66
- const errorText = await response.text().catch(() => 'Unknown error');
67
- throw new Error(`HTTP ${response.status}: ${errorText}`);
68
- }
69
-
70
- const data = (await response.json()) as T;
71
-
72
- return {
73
- ok: response.ok,
74
- status: response.status,
75
- data,
76
- headers: response.headers,
77
- };
78
- } catch (error) {
79
- clearTimeout(timeoutId);
80
-
81
- if (error instanceof Error && error.name === 'AbortError') {
82
- throw new Error(`Request timeout after ${timeout}ms: ${url}`);
83
- }
84
- throw error;
85
- }
86
- }
87
-
88
- // ============================================================================
89
- // Streaming HTTP
90
- // ============================================================================
91
-
92
- /**
93
- * Make a streaming HTTP request.
94
- * Yields raw string chunks as they arrive.
95
- */
96
- export async function* httpStream(
97
- url: string,
98
- options: HttpRequestOptions = {},
99
- ): AsyncGenerator<string, void, unknown> {
100
- const { method = 'POST', headers = {}, body, timeout = 120000, signal } = options;
101
-
102
- const controller = new AbortController();
103
- const timeoutId = setTimeout(() => controller.abort(), timeout);
104
-
105
- const combinedSignal = signal
106
- ? AbortSignal.any([signal, controller.signal])
107
- : controller.signal;
108
-
109
- try {
110
- const response = await fetch(url, {
111
- method,
112
- headers: {
113
- 'Content-Type': 'application/json',
114
- ...headers,
115
- },
116
- body: body ? JSON.stringify(body) : undefined,
117
- signal: combinedSignal,
118
- });
119
-
120
- clearTimeout(timeoutId);
121
-
122
- if (!response.ok) {
123
- const errorText = await response.text().catch(() => 'Unknown error');
124
- throw new Error(`HTTP ${response.status}: ${errorText}`);
125
- }
126
-
127
- if (!response.body) {
128
- throw new Error('No response body for streaming');
129
- }
130
-
131
- const reader = response.body.getReader();
132
- const decoder = new TextDecoder();
133
-
134
- try {
135
- while (true) {
136
- const { done, value } = await reader.read();
137
- if (done) break;
138
- yield decoder.decode(value, { stream: true });
139
- }
140
- } finally {
141
- reader.releaseLock();
142
- }
143
- } catch (error) {
144
- clearTimeout(timeoutId);
145
-
146
- if (error instanceof Error && error.name === 'AbortError') {
147
- throw new Error(`Stream timeout after ${timeout}ms: ${url}`);
148
- }
149
- throw error;
150
- }
151
- }
152
-
153
- // ============================================================================
154
- // Protocol Parsers
155
- // ============================================================================
156
-
157
- /**
158
- * Parse NDJSON (newline-delimited JSON) stream.
159
- * Used by Ollama's streaming API.
160
- */
161
- export async function* parseNDJSON<T = unknown>(
162
- stream: AsyncGenerator<string>,
163
- ): AsyncGenerator<T, void, unknown> {
164
- let buffer = '';
165
-
166
- for await (const chunk of stream) {
167
- buffer += chunk;
168
- const lines = buffer.split('\n');
169
- buffer = lines.pop() ?? '';
170
-
171
- for (const line of lines) {
172
- const trimmed = line.trim();
173
- if (!trimmed) continue;
174
-
175
- try {
176
- yield JSON.parse(trimmed) as T;
177
- } catch {
178
- // Skip invalid JSON lines
179
- }
180
- }
181
- }
182
-
183
- // Handle remaining buffer
184
- if (buffer.trim()) {
185
- try {
186
- yield JSON.parse(buffer) as T;
187
- } catch {
188
- // Skip invalid JSON
189
- }
190
- }
191
- }
192
-
193
- /**
194
- * Parse Server-Sent Events stream.
195
- * Used by OpenAI-compatible APIs and LlamaCpp/vLLM.
196
- */
197
- export async function* parseSSE(
198
- stream: AsyncGenerator<string>,
199
- ): AsyncGenerator<{ event?: string; data: string }, void, unknown> {
200
- let buffer = '';
201
-
202
- for await (const chunk of stream) {
203
- buffer += chunk;
204
-
205
- // Split by double newlines (SSE event delimiter)
206
- const events = buffer.split('\n\n');
207
- buffer = events.pop() ?? '';
208
-
209
- for (const event of events) {
210
- const lines = event.split('\n');
211
- let eventType: string | undefined;
212
- const dataLines: string[] = [];
213
-
214
- for (const line of lines) {
215
- if (line.startsWith('event:')) {
216
- eventType = line.slice(6).trim();
217
- } else if (line.startsWith('data:')) {
218
- dataLines.push(line.slice(5).trim());
219
- }
220
- }
221
-
222
- const data = dataLines.join('\n');
223
- if (data && data !== '[DONE]') {
224
- yield { event: eventType, data };
225
- }
226
- }
227
- }
228
- }
229
-
230
- // ============================================================================
231
- // Header Utilities
232
- // ============================================================================
233
-
234
- /**
235
- * Build standard headers for LLM API requests.
236
- */
237
- export function buildHeaders(options: LLMClientOptions): Record<string, string> {
238
- const headers: Record<string, string> = {
239
- 'Content-Type': 'application/json',
240
- };
241
-
242
- if (options.apiKey) {
243
- headers['Authorization'] = `Bearer ${options.apiKey}`;
244
- }
245
-
246
- return headers;
247
- }
1
+ /**
2
+ * Universal LLM Client v3 — HTTP Utilities
3
+ *
4
+ * Zero-dependency HTTP layer using native fetch.
5
+ * Works on Node 22+, Bun, Deno, and browsers.
6
+ */
7
+
8
+ import type { LLMClientOptions } from './interfaces.js';
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface HttpRequestOptions {
15
+ method?: 'GET' | 'POST';
16
+ headers?: Record<string, string>;
17
+ body?: unknown;
18
+ timeout?: number;
19
+ signal?: AbortSignal;
20
+ }
21
+
22
+ export interface HttpResponse<T = unknown> {
23
+ ok: boolean;
24
+ status: number;
25
+ data: T;
26
+ /** Response headers (when available) */
27
+ headers?: Headers;
28
+ }
29
+
30
+ // ============================================================================
31
+ // HTTP Request
32
+ // ============================================================================
33
+
34
+ /**
35
+ * Make an HTTP request with timeout and error handling.
36
+ * Uses native fetch (available in all target runtimes).
37
+ */
38
+ export async function httpRequest<T = unknown>(
39
+ url: string,
40
+ options: HttpRequestOptions = {},
41
+ ): Promise<HttpResponse<T>> {
42
+ const { method = 'GET', headers = {}, body, timeout = 30000, signal } = options;
43
+
44
+ const controller = new AbortController();
45
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
46
+
47
+ // Combine external signal with timeout
48
+ const combinedSignal = signal
49
+ ? AbortSignal.any([signal, controller.signal])
50
+ : controller.signal;
51
+
52
+ try {
53
+ const response = await fetch(url, {
54
+ method,
55
+ headers: {
56
+ 'Content-Type': 'application/json',
57
+ ...headers,
58
+ },
59
+ body: body ? JSON.stringify(body) : undefined,
60
+ signal: combinedSignal,
61
+ });
62
+
63
+ clearTimeout(timeoutId);
64
+
65
+ if (!response.ok) {
66
+ const errorText = await response.text().catch(() => 'Unknown error');
67
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
68
+ }
69
+
70
+ const data = (await response.json()) as T;
71
+
72
+ return {
73
+ ok: response.ok,
74
+ status: response.status,
75
+ data,
76
+ headers: response.headers,
77
+ };
78
+ } catch (error) {
79
+ clearTimeout(timeoutId);
80
+
81
+ if (error instanceof Error && error.name === 'AbortError') {
82
+ throw new Error(`Request timeout after ${timeout}ms: ${url}`);
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ // ============================================================================
89
+ // Streaming HTTP
90
+ // ============================================================================
91
+
92
+ /**
93
+ * Make a streaming HTTP request.
94
+ * Yields raw string chunks as they arrive.
95
+ */
96
+ export async function* httpStream(
97
+ url: string,
98
+ options: HttpRequestOptions = {},
99
+ ): AsyncGenerator<string, void, unknown> {
100
+ const { method = 'POST', headers = {}, body, timeout = 120000, signal } = options;
101
+
102
+ const controller = new AbortController();
103
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
104
+
105
+ const combinedSignal = signal
106
+ ? AbortSignal.any([signal, controller.signal])
107
+ : controller.signal;
108
+
109
+ try {
110
+ const response = await fetch(url, {
111
+ method,
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ ...headers,
115
+ },
116
+ body: body ? JSON.stringify(body) : undefined,
117
+ signal: combinedSignal,
118
+ });
119
+
120
+ clearTimeout(timeoutId);
121
+
122
+ if (!response.ok) {
123
+ const errorText = await response.text().catch(() => 'Unknown error');
124
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
125
+ }
126
+
127
+ if (!response.body) {
128
+ throw new Error('No response body for streaming');
129
+ }
130
+
131
+ const reader = response.body.getReader();
132
+ const decoder = new TextDecoder();
133
+
134
+ try {
135
+ while (true) {
136
+ const { done, value } = await reader.read();
137
+ if (done) break;
138
+ yield decoder.decode(value, { stream: true });
139
+ }
140
+ } finally {
141
+ reader.releaseLock();
142
+ }
143
+ } catch (error) {
144
+ clearTimeout(timeoutId);
145
+
146
+ if (error instanceof Error && error.name === 'AbortError') {
147
+ throw new Error(`Stream timeout after ${timeout}ms: ${url}`);
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ // ============================================================================
154
+ // Protocol Parsers
155
+ // ============================================================================
156
+
157
+ /**
158
+ * Parse NDJSON (newline-delimited JSON) stream.
159
+ * Used by Ollama's streaming API.
160
+ */
161
+ export async function* parseNDJSON<T = unknown>(
162
+ stream: AsyncGenerator<string>,
163
+ ): AsyncGenerator<T, void, unknown> {
164
+ let buffer = '';
165
+
166
+ for await (const chunk of stream) {
167
+ buffer += chunk;
168
+ const lines = buffer.split('\n');
169
+ buffer = lines.pop() ?? '';
170
+
171
+ for (const line of lines) {
172
+ const trimmed = line.trim();
173
+ if (!trimmed) continue;
174
+
175
+ try {
176
+ yield JSON.parse(trimmed) as T;
177
+ } catch {
178
+ // Skip invalid JSON lines
179
+ }
180
+ }
181
+ }
182
+
183
+ // Handle remaining buffer
184
+ if (buffer.trim()) {
185
+ try {
186
+ yield JSON.parse(buffer) as T;
187
+ } catch {
188
+ // Skip invalid JSON
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Parse Server-Sent Events stream.
195
+ * Used by OpenAI-compatible APIs and LlamaCpp/vLLM.
196
+ */
197
+ export async function* parseSSE(
198
+ stream: AsyncGenerator<string>,
199
+ ): AsyncGenerator<{ event?: string; data: string }, void, unknown> {
200
+ let buffer = '';
201
+
202
+ for await (const chunk of stream) {
203
+ buffer += chunk;
204
+
205
+ // Split by double newlines (SSE event delimiter)
206
+ const events = buffer.split('\n\n');
207
+ buffer = events.pop() ?? '';
208
+
209
+ for (const event of events) {
210
+ const lines = event.split('\n');
211
+ let eventType: string | undefined;
212
+ const dataLines: string[] = [];
213
+
214
+ for (const line of lines) {
215
+ if (line.startsWith('event:')) {
216
+ eventType = line.slice(6).trim();
217
+ } else if (line.startsWith('data:')) {
218
+ dataLines.push(line.slice(5).trim());
219
+ }
220
+ }
221
+
222
+ const data = dataLines.join('\n');
223
+ if (data && data !== '[DONE]') {
224
+ yield { event: eventType, data };
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ // ============================================================================
231
+ // Header Utilities
232
+ // ============================================================================
233
+
234
+ /**
235
+ * Build standard headers for LLM API requests.
236
+ * Merges any provider-specific extraHeaders (from ProviderConfig) on top.
237
+ * Provider clients can still fully override (e.g. Anthropic uses x-api-key).
238
+ *
239
+ * Respects authHeader / authPrefix from config for Azure-style or gateway auth.
240
+ */
241
+ export function buildHeaders(options: LLMClientOptions): Record<string, string> {
242
+ const headers: Record<string, string> = {
243
+ 'Content-Type': 'application/json',
244
+ };
245
+
246
+ if (options.apiKey) {
247
+ const headerName = options.authHeader || 'Authorization';
248
+ // Sensible default prefix: Bearer for Authorization, nothing for api-key / x-api-key etc.
249
+ const defaultPrefix = headerName.toLowerCase() === 'authorization' ? 'Bearer ' : '';
250
+ const prefix = options.authPrefix !== undefined ? options.authPrefix : defaultPrefix;
251
+ headers[headerName] = `${prefix}${options.apiKey}`.trim();
252
+ }
253
+
254
+ // Merge provider-specific extras (e.g. Azure 'api-key', custom gateway headers).
255
+ // Later entries win on conflicts, allowing complete override of auth.
256
+ if (options.extraHeaders) {
257
+ Object.assign(headers, options.extraHeaders);
258
+ }
259
+
260
+ return headers;
261
+ }