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
@@ -1,179 +1,263 @@
1
- /**
2
- * Tests for stream-decoder.ts — Pluggable reasoning strategies
3
- */
4
- import { describe, it, expect } from 'bun:test';
5
- import {
6
- PassthroughDecoder,
7
- StandardChatDecoder,
8
- InterleavedReasoningDecoder,
9
- createDecoder,
10
- type DecodedEvent,
11
- } from '../stream-decoder.js';
12
-
13
- describe('PassthroughDecoder', () => {
14
- it('emits all tokens as text events', () => {
15
- const events: DecodedEvent[] = [];
16
- const decoder = new PassthroughDecoder(e => events.push(e));
17
-
18
- decoder.push('Hello');
19
- decoder.push(' world');
20
- decoder.flush();
21
-
22
- expect(events).toHaveLength(2);
23
- expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
24
- expect(events[1]).toEqual({ type: 'text', content: ' world' });
25
- });
26
-
27
- it('returns clean content', () => {
28
- const decoder = new PassthroughDecoder(() => {});
29
- decoder.push('Hello ');
30
- decoder.push('world');
31
- decoder.flush();
32
- expect(decoder.getCleanContent()).toBe('Hello world');
33
- });
34
-
35
- it('returns no reasoning', () => {
36
- const decoder = new PassthroughDecoder(() => {});
37
- decoder.push('test');
38
- expect(decoder.getReasoning()).toBeUndefined();
39
- });
40
- });
41
-
42
- describe('StandardChatDecoder', () => {
43
- it('emits text events', () => {
44
- const events: DecodedEvent[] = [];
45
- const decoder = new StandardChatDecoder(e => events.push(e));
46
-
47
- decoder.push('Hello');
48
- expect(events).toHaveLength(1);
49
- expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
50
- });
51
-
52
- it('emits reasoning events', () => {
53
- const events: DecodedEvent[] = [];
54
- const decoder = new StandardChatDecoder(e => events.push(e));
55
-
56
- decoder.pushReasoning('Thinking...');
57
- expect(events).toHaveLength(1);
58
- expect(events[0]).toEqual({ type: 'thinking', content: 'Thinking...' });
59
- });
60
-
61
- it('emits tool call events', () => {
62
- const events: DecodedEvent[] = [];
63
- const decoder = new StandardChatDecoder(e => events.push(e));
64
-
65
- const calls = [{
66
- id: 'call_1',
67
- type: 'function' as const,
68
- function: { name: 'get_time', arguments: '{}' },
69
- }];
70
- decoder.pushToolCalls(calls);
71
-
72
- expect(events).toHaveLength(1);
73
- expect(events[0]!.type).toBe('tool_call');
74
- });
75
-
76
- it('tracks both content and reasoning', () => {
77
- const decoder = new StandardChatDecoder(() => {});
78
- decoder.push('Text');
79
- decoder.pushReasoning('Reason');
80
- decoder.flush();
81
-
82
- expect(decoder.getCleanContent()).toBe('Text');
83
- expect(decoder.getReasoning()).toBe('Reason');
84
- });
85
- });
86
-
87
- describe('InterleavedReasoningDecoder', () => {
88
- it('extracts think tags from text', () => {
89
- const events: DecodedEvent[] = [];
90
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
91
-
92
- decoder.push('<think>I should analyze this</think>The answer is 42');
93
- decoder.flush();
94
-
95
- const thinkEvents = events.filter(e => e.type === 'thinking');
96
- const textEvents = events.filter(e => e.type === 'text');
97
-
98
- expect(thinkEvents.length).toBeGreaterThan(0);
99
- expect(textEvents.length).toBeGreaterThan(0);
100
- expect(decoder.getReasoning()).toBe('I should analyze this');
101
- expect(decoder.getCleanContent()).toBe('The answer is 42');
102
- });
103
-
104
- it('handles think tags split across chunks', () => {
105
- const events: DecodedEvent[] = [];
106
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
107
-
108
- decoder.push('<thi');
109
- decoder.push('nk>Split reasoning</th');
110
- decoder.push('ink>Done');
111
- decoder.flush();
112
-
113
- expect(decoder.getReasoning()).toBe('Split reasoning');
114
- expect(decoder.getCleanContent()).toBe('Done');
115
- });
116
-
117
- it('handles progress tags', () => {
118
- const events: DecodedEvent[] = [];
119
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
120
-
121
- decoder.push('<progress>Loading data</progress>Complete');
122
- decoder.flush();
123
-
124
- const progressEvents = events.filter(e => e.type === 'progress');
125
- expect(progressEvents.length).toBeGreaterThan(0);
126
- expect(decoder.getCleanContent()).toBe('Complete');
127
- });
128
-
129
- it('handles plain text with no tags', () => {
130
- const events: DecodedEvent[] = [];
131
- const decoder = new InterleavedReasoningDecoder(e => events.push(e));
132
-
133
- decoder.push('Just plain text');
134
- decoder.flush();
135
-
136
- expect(decoder.getCleanContent()).toBe('Just plain text');
137
- expect(decoder.getReasoning()).toBeUndefined();
138
- });
139
-
140
- it('handles empty reasoning', () => {
141
- const decoder = new InterleavedReasoningDecoder(() => {});
142
- decoder.push('<think></think>Content');
143
- decoder.flush();
144
-
145
- expect(decoder.getCleanContent()).toBe('Content');
146
- expect(decoder.getReasoning()).toBeUndefined();
147
- });
148
-
149
- it('handles multiple think blocks', () => {
150
- const decoder = new InterleavedReasoningDecoder(() => {});
151
-
152
- decoder.push('<think>First thought</think>Text<think>Second thought</think>More text');
153
- decoder.flush();
154
-
155
- expect(decoder.getReasoning()).toBe('First thoughtSecond thought');
156
- expect(decoder.getCleanContent()).toBe('TextMore text');
157
- });
158
- });
159
-
160
- describe('createDecoder', () => {
161
- it('creates passthrough decoder', () => {
162
- const decoder = createDecoder('passthrough', () => {});
163
- expect(decoder).toBeInstanceOf(PassthroughDecoder);
164
- });
165
-
166
- it('creates standard-chat decoder', () => {
167
- const decoder = createDecoder('standard-chat', () => {});
168
- expect(decoder).toBeInstanceOf(StandardChatDecoder);
169
- });
170
-
171
- it('creates interleaved-reasoning decoder', () => {
172
- const decoder = createDecoder('interleaved-reasoning', () => {});
173
- expect(decoder).toBeInstanceOf(InterleavedReasoningDecoder);
174
- });
175
-
176
- it('throws for unknown type', () => {
177
- expect(() => createDecoder('unknown' as never, () => {})).toThrow('Unknown decoder type');
178
- });
179
- });
1
+ /**
2
+ * Tests for stream-decoder.ts — Pluggable reasoning strategies
3
+ */
4
+ import { describe, it, expect } from 'bun:test';
5
+ import {
6
+ PassthroughDecoder,
7
+ StandardChatDecoder,
8
+ InterleavedReasoningDecoder,
9
+ createDecoder,
10
+ type DecodedEvent,
11
+ } from '../stream-decoder.js';
12
+
13
+ describe('PassthroughDecoder', () => {
14
+ it('emits all tokens as text events', () => {
15
+ const events: DecodedEvent[] = [];
16
+ const decoder = new PassthroughDecoder(e => events.push(e));
17
+
18
+ decoder.push('Hello');
19
+ decoder.push(' world');
20
+ decoder.flush();
21
+
22
+ expect(events).toHaveLength(2);
23
+ expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
24
+ expect(events[1]).toEqual({ type: 'text', content: ' world' });
25
+ });
26
+
27
+ it('returns clean content', () => {
28
+ const decoder = new PassthroughDecoder(() => {});
29
+ decoder.push('Hello ');
30
+ decoder.push('world');
31
+ decoder.flush();
32
+ expect(decoder.getCleanContent()).toBe('Hello world');
33
+ });
34
+
35
+ it('returns no reasoning', () => {
36
+ const decoder = new PassthroughDecoder(() => {});
37
+ decoder.push('test');
38
+ expect(decoder.getReasoning()).toBeUndefined();
39
+ });
40
+ });
41
+
42
+ describe('StandardChatDecoder', () => {
43
+ it('emits text events', () => {
44
+ const events: DecodedEvent[] = [];
45
+ const decoder = new StandardChatDecoder(e => events.push(e));
46
+
47
+ decoder.push('Hello');
48
+ expect(events).toHaveLength(1);
49
+ expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
50
+ });
51
+
52
+ it('emits reasoning events', () => {
53
+ const events: DecodedEvent[] = [];
54
+ const decoder = new StandardChatDecoder(e => events.push(e));
55
+
56
+ decoder.pushReasoning('Thinking...');
57
+ expect(events).toHaveLength(1);
58
+ expect(events[0]).toEqual({ type: 'thinking', content: 'Thinking...' });
59
+ });
60
+
61
+ it('emits tool call events', () => {
62
+ const events: DecodedEvent[] = [];
63
+ const decoder = new StandardChatDecoder(e => events.push(e));
64
+
65
+ const calls = [{
66
+ id: 'call_1',
67
+ type: 'function' as const,
68
+ function: { name: 'get_time', arguments: '{}' },
69
+ }];
70
+ decoder.pushToolCalls(calls);
71
+
72
+ expect(events).toHaveLength(1);
73
+ expect(events[0]!.type).toBe('tool_call');
74
+ });
75
+
76
+ it('tracks both content and reasoning', () => {
77
+ const decoder = new StandardChatDecoder(() => {});
78
+ decoder.push('Text');
79
+ decoder.pushReasoning('Reason');
80
+ decoder.flush();
81
+
82
+ expect(decoder.getCleanContent()).toBe('Text');
83
+ expect(decoder.getReasoning()).toBe('Reason');
84
+ });
85
+
86
+ it('parses Gemma thought channel into reasoning', () => {
87
+ const events: DecodedEvent[] = [];
88
+ const decoder = new StandardChatDecoder(e => events.push(e));
89
+
90
+ decoder.push('<|channel>thought\nNeed Portuguese.<channel|>Olá!');
91
+ decoder.flush();
92
+
93
+ expect(events.filter(e => e.type === 'thinking')).toEqual([
94
+ { type: 'thinking', content: 'Need Portuguese.' },
95
+ ]);
96
+ expect(events.filter(e => e.type === 'text').map(e => e.content).join('')).toBe('Olá!');
97
+ expect(decoder.getCleanContent()).toBe('Olá!');
98
+ expect(decoder.getReasoning()).toBe('Need Portuguese.');
99
+ });
100
+
101
+ it('parses Gemma thought channel split across chunks', () => {
102
+ const decoder = new StandardChatDecoder(() => {});
103
+
104
+ decoder.push('<|chan');
105
+ decoder.push('nel>thought\nNeed');
106
+ decoder.push(' Portuguese.<chan');
107
+ decoder.push('nel|>Olá!');
108
+ decoder.flush();
109
+
110
+ expect(decoder.getCleanContent()).toBe('Olá!');
111
+ expect(decoder.getReasoning()).toBe('Need Portuguese.');
112
+ });
113
+
114
+ it('strips compact empty Gemma thought marker', () => {
115
+ const decoder = new StandardChatDecoder(() => {});
116
+
117
+ decoder.push('<|thought\n|>Olá!');
118
+ decoder.flush();
119
+
120
+ expect(decoder.getCleanContent()).toBe('Olá!');
121
+ expect(decoder.getReasoning()).toBeUndefined();
122
+ });
123
+
124
+ it('strips tool_call and tool_response tags', () => {
125
+ const events: DecodedEvent[] = [];
126
+ const decoder = new StandardChatDecoder(e => events.push(e));
127
+
128
+ decoder.push('<tool_call|><|tool_response>');
129
+ decoder.flush();
130
+
131
+ expect(events.filter(e => e.type === 'tool_call')).toHaveLength(0);
132
+ expect(decoder.getCleanContent()).toBe('');
133
+ });
134
+
135
+ it('parses tool_call containing JSON content', () => {
136
+ const events: DecodedEvent[] = [];
137
+ const decoder = new StandardChatDecoder(e => events.push(e));
138
+
139
+ decoder.push("<tool_call|>{'name': 'get_weather', 'arguments': {'city': 'Tokyo'}}<|tool_response>");
140
+ decoder.flush();
141
+
142
+ const toolCalls = events.filter(e => e.type === 'tool_call');
143
+ expect(toolCalls).toHaveLength(1);
144
+ expect(toolCalls[0]).toEqual({
145
+ type: 'tool_call',
146
+ calls: [
147
+ {
148
+ id: expect.any(String),
149
+ type: 'function',
150
+ function: {
151
+ name: 'get_weather',
152
+ arguments: JSON.stringify({ city: 'Tokyo' }),
153
+ },
154
+ },
155
+ ],
156
+ });
157
+ expect(decoder.getCleanContent()).toBe('');
158
+ });
159
+
160
+ it('strips stray tool_response tags', () => {
161
+ const events: DecodedEvent[] = [];
162
+ const decoder = new StandardChatDecoder(e => events.push(e));
163
+
164
+ decoder.push('Hello<|tool_response> World');
165
+ decoder.flush();
166
+
167
+ expect(decoder.getCleanContent()).toBe('Hello World');
168
+ });
169
+ });
170
+
171
+ describe('InterleavedReasoningDecoder', () => {
172
+ it('extracts think tags from text', () => {
173
+ const events: DecodedEvent[] = [];
174
+ const decoder = new InterleavedReasoningDecoder(e => events.push(e));
175
+
176
+ decoder.push('<think>I should analyze this</think>The answer is 42');
177
+ decoder.flush();
178
+
179
+ const thinkEvents = events.filter(e => e.type === 'thinking');
180
+ const textEvents = events.filter(e => e.type === 'text');
181
+
182
+ expect(thinkEvents.length).toBeGreaterThan(0);
183
+ expect(textEvents.length).toBeGreaterThan(0);
184
+ expect(decoder.getReasoning()).toBe('I should analyze this');
185
+ expect(decoder.getCleanContent()).toBe('The answer is 42');
186
+ });
187
+
188
+ it('handles think tags split across chunks', () => {
189
+ const events: DecodedEvent[] = [];
190
+ const decoder = new InterleavedReasoningDecoder(e => events.push(e));
191
+
192
+ decoder.push('<thi');
193
+ decoder.push('nk>Split reasoning</th');
194
+ decoder.push('ink>Done');
195
+ decoder.flush();
196
+
197
+ expect(decoder.getReasoning()).toBe('Split reasoning');
198
+ expect(decoder.getCleanContent()).toBe('Done');
199
+ });
200
+
201
+ it('handles progress tags', () => {
202
+ const events: DecodedEvent[] = [];
203
+ const decoder = new InterleavedReasoningDecoder(e => events.push(e));
204
+
205
+ decoder.push('<progress>Loading data</progress>Complete');
206
+ decoder.flush();
207
+
208
+ const progressEvents = events.filter(e => e.type === 'progress');
209
+ expect(progressEvents.length).toBeGreaterThan(0);
210
+ expect(decoder.getCleanContent()).toBe('Complete');
211
+ });
212
+
213
+ it('handles plain text with no tags', () => {
214
+ const events: DecodedEvent[] = [];
215
+ const decoder = new InterleavedReasoningDecoder(e => events.push(e));
216
+
217
+ decoder.push('Just plain text');
218
+ decoder.flush();
219
+
220
+ expect(decoder.getCleanContent()).toBe('Just plain text');
221
+ expect(decoder.getReasoning()).toBeUndefined();
222
+ });
223
+
224
+ it('handles empty reasoning', () => {
225
+ const decoder = new InterleavedReasoningDecoder(() => {});
226
+ decoder.push('<think></think>Content');
227
+ decoder.flush();
228
+
229
+ expect(decoder.getCleanContent()).toBe('Content');
230
+ expect(decoder.getReasoning()).toBeUndefined();
231
+ });
232
+
233
+ it('handles multiple think blocks', () => {
234
+ const decoder = new InterleavedReasoningDecoder(() => {});
235
+
236
+ decoder.push('<think>First thought</think>Text<think>Second thought</think>More text');
237
+ decoder.flush();
238
+
239
+ expect(decoder.getReasoning()).toBe('First thoughtSecond thought');
240
+ expect(decoder.getCleanContent()).toBe('TextMore text');
241
+ });
242
+ });
243
+
244
+ describe('createDecoder', () => {
245
+ it('creates passthrough decoder', () => {
246
+ const decoder = createDecoder('passthrough', () => {});
247
+ expect(decoder).toBeInstanceOf(PassthroughDecoder);
248
+ });
249
+
250
+ it('creates standard-chat decoder', () => {
251
+ const decoder = createDecoder('standard-chat', () => {});
252
+ expect(decoder).toBeInstanceOf(StandardChatDecoder);
253
+ });
254
+
255
+ it('creates interleaved-reasoning decoder', () => {
256
+ const decoder = createDecoder('interleaved-reasoning', () => {});
257
+ expect(decoder).toBeInstanceOf(InterleavedReasoningDecoder);
258
+ });
259
+
260
+ it('throws for unknown type', () => {
261
+ expect(() => createDecoder('unknown' as never, () => {})).toThrow('Unknown decoder type');
262
+ });
263
+ });