universal-llm-client 4.5.0 → 4.5.1

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 (174) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -0
  3. package/dist/ai-model.d.ts +0 -1
  4. package/dist/ai-model.js +0 -1
  5. package/dist/auditor.d.ts +0 -1
  6. package/dist/auditor.js +0 -1
  7. package/dist/client.d.ts +0 -1
  8. package/dist/client.js +0 -1
  9. package/dist/gemma-channel.d.ts +0 -1
  10. package/dist/gemma-channel.js +0 -1
  11. package/dist/gemma-diffusion.d.ts +0 -1
  12. package/dist/gemma-diffusion.js +0 -1
  13. package/dist/http.d.ts +0 -1
  14. package/dist/http.js +0 -1
  15. package/dist/index.d.ts +0 -1
  16. package/dist/index.js +0 -1
  17. package/dist/interfaces.d.ts +0 -1
  18. package/dist/interfaces.js +0 -1
  19. package/dist/mcp.d.ts +0 -1
  20. package/dist/mcp.js +0 -1
  21. package/dist/providers/anthropic.d.ts +0 -1
  22. package/dist/providers/anthropic.js +0 -1
  23. package/dist/providers/google.d.ts +0 -1
  24. package/dist/providers/google.js +0 -1
  25. package/dist/providers/index.d.ts +0 -1
  26. package/dist/providers/index.js +0 -1
  27. package/dist/providers/ollama.d.ts +0 -1
  28. package/dist/providers/ollama.js +0 -1
  29. package/dist/providers/openai.d.ts +2 -1
  30. package/dist/providers/openai.js +303 -74
  31. package/dist/router.d.ts +0 -1
  32. package/dist/router.js +0 -1
  33. package/dist/stream-decoder.d.ts +0 -1
  34. package/dist/stream-decoder.js +0 -1
  35. package/dist/structured-output.d.ts +0 -1
  36. package/dist/structured-output.js +0 -1
  37. package/dist/thinking.d.ts +0 -1
  38. package/dist/thinking.js +0 -1
  39. package/dist/tools.d.ts +0 -1
  40. package/dist/tools.js +0 -1
  41. package/dist/zod-adapter.d.ts +0 -1
  42. package/dist/zod-adapter.js +0 -1
  43. package/package.json +1 -2
  44. package/dist/ai-model.d.ts.map +0 -1
  45. package/dist/ai-model.js.map +0 -1
  46. package/dist/auditor.d.ts.map +0 -1
  47. package/dist/auditor.js.map +0 -1
  48. package/dist/client.d.ts.map +0 -1
  49. package/dist/client.js.map +0 -1
  50. package/dist/gemma-channel.d.ts.map +0 -1
  51. package/dist/gemma-channel.js.map +0 -1
  52. package/dist/gemma-diffusion.d.ts.map +0 -1
  53. package/dist/gemma-diffusion.js.map +0 -1
  54. package/dist/http.d.ts.map +0 -1
  55. package/dist/http.js.map +0 -1
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js.map +0 -1
  58. package/dist/interfaces.d.ts.map +0 -1
  59. package/dist/interfaces.js.map +0 -1
  60. package/dist/mcp.d.ts.map +0 -1
  61. package/dist/mcp.js.map +0 -1
  62. package/dist/providers/anthropic.d.ts.map +0 -1
  63. package/dist/providers/anthropic.js.map +0 -1
  64. package/dist/providers/google.d.ts.map +0 -1
  65. package/dist/providers/google.js.map +0 -1
  66. package/dist/providers/index.d.ts.map +0 -1
  67. package/dist/providers/index.js.map +0 -1
  68. package/dist/providers/ollama.d.ts.map +0 -1
  69. package/dist/providers/ollama.js.map +0 -1
  70. package/dist/providers/openai.d.ts.map +0 -1
  71. package/dist/providers/openai.js.map +0 -1
  72. package/dist/router.d.ts.map +0 -1
  73. package/dist/router.js.map +0 -1
  74. package/dist/stream-decoder.d.ts.map +0 -1
  75. package/dist/stream-decoder.js.map +0 -1
  76. package/dist/structured-output.d.ts.map +0 -1
  77. package/dist/structured-output.js.map +0 -1
  78. package/dist/thinking.d.ts.map +0 -1
  79. package/dist/thinking.js.map +0 -1
  80. package/dist/tools.d.ts.map +0 -1
  81. package/dist/tools.js.map +0 -1
  82. package/dist/zod-adapter.d.ts.map +0 -1
  83. package/dist/zod-adapter.js.map +0 -1
  84. package/src/ai-model.ts +0 -400
  85. package/src/auditor.ts +0 -213
  86. package/src/client.ts +0 -402
  87. package/src/debug/debug-google-streaming.ts +0 -97
  88. package/src/debug/debug-tool-execution.ts +0 -86
  89. package/src/debug/test-lmstudio-tools.ts +0 -155
  90. package/src/demos/README.md +0 -47
  91. package/src/demos/basic/universal-llm-examples.ts +0 -161
  92. package/src/demos/diffusion-gemma/.env +0 -29
  93. package/src/demos/diffusion-gemma/.env.example +0 -27
  94. package/src/demos/diffusion-gemma/CLAUDE.md +0 -95
  95. package/src/demos/diffusion-gemma/README.md +0 -59
  96. package/src/demos/diffusion-gemma/canvas.ts +0 -1606
  97. package/src/demos/diffusion-gemma/docker-compose.yml +0 -29
  98. package/src/demos/diffusion-gemma/probe-stream.ts +0 -51
  99. package/src/demos/diffusion-gemma/probe-tools.ts +0 -55
  100. package/src/demos/diffusion-gemma/server.ts +0 -1205
  101. package/src/demos/diffusion-gemma/start-vllm.sh +0 -98
  102. package/src/demos/mcp/astrid-memory-demo.ts +0 -295
  103. package/src/demos/mcp/astrid-persona-memory.ts +0 -357
  104. package/src/demos/mcp/mcp-mongodb-demo.ts +0 -275
  105. package/src/demos/mcp/simple-astrid-memory.ts +0 -148
  106. package/src/demos/mcp/simple-mcp-demo.ts +0 -68
  107. package/src/demos/mcp/working-mcp-demo.ts +0 -62
  108. package/src/demos/model-alias-demo.ts +0 -0
  109. package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +0 -267
  110. package/src/demos/tools/astrid-memory-demo.ts +0 -270
  111. package/src/demos/tools/astrid-production-memory-clean.ts +0 -785
  112. package/src/demos/tools/astrid-production-memory.ts +0 -558
  113. package/src/demos/tools/basic-translation-test.ts +0 -66
  114. package/src/demos/tools/chromadb-similarity-tuning.ts +0 -390
  115. package/src/demos/tools/clean-multilingual-conversation.ts +0 -209
  116. package/src/demos/tools/clean-translation-test.ts +0 -119
  117. package/src/demos/tools/clean-universal-multilingual-test.ts +0 -131
  118. package/src/demos/tools/complete-rag-demo.ts +0 -369
  119. package/src/demos/tools/complete-tool-demo.ts +0 -132
  120. package/src/demos/tools/demo-tool-calling.ts +0 -124
  121. package/src/demos/tools/dynamic-language-switching-test.ts +0 -251
  122. package/src/demos/tools/hybrid-thinking-test.ts +0 -154
  123. package/src/demos/tools/memory-integration-test.ts +0 -420
  124. package/src/demos/tools/multilingual-memory-system.ts +0 -802
  125. package/src/demos/tools/ondemand-translation-demo.ts +0 -655
  126. package/src/demos/tools/production-tool-demo.ts +0 -245
  127. package/src/demos/tools/revolutionary-multilingual-test.ts +0 -151
  128. package/src/demos/tools/rigorous-language-analysis.ts +0 -218
  129. package/src/demos/tools/test-universal-memory-system.ts +0 -126
  130. package/src/demos/tools/translation-integration-guide.ts +0 -346
  131. package/src/demos/tools/universal-memory-system.ts +0 -560
  132. package/src/gemma-channel.ts +0 -47
  133. package/src/gemma-diffusion.ts +0 -167
  134. package/src/http.ts +0 -261
  135. package/src/index.ts +0 -180
  136. package/src/interfaces.ts +0 -843
  137. package/src/mcp.ts +0 -345
  138. package/src/providers/anthropic.ts +0 -796
  139. package/src/providers/google.ts +0 -840
  140. package/src/providers/index.ts +0 -8
  141. package/src/providers/ollama.ts +0 -503
  142. package/src/providers/openai.ts +0 -587
  143. package/src/router.ts +0 -785
  144. package/src/stream-decoder.ts +0 -535
  145. package/src/structured-output.ts +0 -759
  146. package/src/test-scripts/test-advanced-tools.ts +0 -310
  147. package/src/test-scripts/test-google-deep-research.ts +0 -33
  148. package/src/test-scripts/test-google-streaming-enhanced.ts +0 -147
  149. package/src/test-scripts/test-google-streaming.ts +0 -63
  150. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +0 -189
  151. package/src/test-scripts/test-google-thinking.ts +0 -46
  152. package/src/test-scripts/test-mcp-config.ts +0 -28
  153. package/src/test-scripts/test-mcp-connection.ts +0 -29
  154. package/src/test-scripts/test-system-message-positions.ts +0 -163
  155. package/src/test-scripts/test-system-prompt-improvement-demo.ts +0 -83
  156. package/src/test-scripts/test-tool-calling.ts +0 -231
  157. package/src/test-scripts/test-vllm-qwen36.ts +0 -256
  158. package/src/tests/ai-model.test.ts +0 -1614
  159. package/src/tests/auditor.test.ts +0 -224
  160. package/src/tests/gemma-diffusion.test.ts +0 -115
  161. package/src/tests/http.test.ts +0 -200
  162. package/src/tests/interfaces.test.ts +0 -117
  163. package/src/tests/providers/anthropic.test.ts +0 -118
  164. package/src/tests/providers/google.test.ts +0 -841
  165. package/src/tests/providers/ollama.test.ts +0 -1034
  166. package/src/tests/providers/openai.test.ts +0 -1511
  167. package/src/tests/router.test.ts +0 -254
  168. package/src/tests/stream-decoder.test.ts +0 -263
  169. package/src/tests/structured-output.test.ts +0 -1450
  170. package/src/tests/thinking.test.ts +0 -65
  171. package/src/tests/tools.test.ts +0 -175
  172. package/src/thinking.ts +0 -73
  173. package/src/tools.ts +0 -246
  174. package/src/zod-adapter.ts +0 -72
@@ -1,254 +0,0 @@
1
- /**
2
- * Tests for router.ts — Failover Engine
3
- */
4
- import { describe, it, expect, mock, beforeEach } from 'bun:test';
5
- import { Router, type ProviderEntry } from '../router.js';
6
- import { BaseLLMClient } from '../client.js';
7
- import { BufferedAuditor } from '../auditor.js';
8
- import type {
9
- LLMChatMessage,
10
- LLMChatResponse,
11
- ChatOptions,
12
- DecodedEvent,
13
- } from '../interfaces.js';
14
-
15
- // ============================================================================
16
- // Mock Client
17
- // ============================================================================
18
-
19
- class MockClient extends BaseLLMClient {
20
- public chatFn: (messages: LLMChatMessage[]) => Promise<LLMChatResponse>;
21
- public embedFn: (text: string) => Promise<number[]>;
22
- public modelsFn: () => Promise<string[]>;
23
-
24
- constructor(id: string, opts?: {
25
- chatFn?: (messages: LLMChatMessage[]) => Promise<LLMChatResponse>;
26
- embedFn?: (text: string) => Promise<number[]>;
27
- }) {
28
- super({
29
- model: `mock-${id}`,
30
- url: `http://mock-${id}`,
31
- apiType: 'openai' as never,
32
- });
33
- this.chatFn = opts?.chatFn ?? (async () => ({
34
- message: { role: 'assistant' as const, content: `Response from ${id}` },
35
- provider: id,
36
- }));
37
- this.embedFn = opts?.embedFn ?? (async () => [1, 2, 3]);
38
- this.modelsFn = async () => [`mock-${id}`];
39
- }
40
-
41
- async chat(messages: LLMChatMessage[]): Promise<LLMChatResponse> {
42
- return this.chatFn(messages);
43
- }
44
-
45
- async *chatStream(): AsyncGenerator<DecodedEvent, LLMChatResponse | void, unknown> {
46
- yield { type: 'text', content: 'streamed' };
47
- return { message: { role: 'assistant', content: 'streamed' }, provider: 'mock' };
48
- }
49
-
50
- async getModels(): Promise<string[]> {
51
- return this.modelsFn();
52
- }
53
-
54
- async embed(text: string): Promise<number[]> {
55
- return this.embedFn(text);
56
- }
57
- }
58
-
59
- // ============================================================================
60
- // Tests
61
- // ============================================================================
62
-
63
- describe('Router', () => {
64
- let router: Router;
65
- let auditor: BufferedAuditor;
66
-
67
- beforeEach(() => {
68
- auditor = new BufferedAuditor();
69
- router = new Router({ auditor, retriesPerProvider: 1, maxFailures: 2, cooldownMs: 100 });
70
- });
71
-
72
- describe('provider management', () => {
73
- it('adds providers and sorts by priority', () => {
74
- const clientA = new MockClient('a');
75
- const clientB = new MockClient('b');
76
-
77
- router.addProvider({ id: 'a', client: clientA, priority: 2 });
78
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
79
-
80
- const status = router.getStatus();
81
- expect(status).toHaveLength(2);
82
- // b has lower priority number = tried first
83
- expect(status[0]!.id).toBe('b');
84
- expect(status[1]!.id).toBe('a');
85
- });
86
-
87
- it('removes providers', () => {
88
- router.addProvider({ id: 'a', client: new MockClient('a'), priority: 0 });
89
- router.addProvider({ id: 'b', client: new MockClient('b'), priority: 1 });
90
- router.removeProvider('a');
91
-
92
- expect(router.getStatus()).toHaveLength(1);
93
- expect(router.getStatus()[0]!.id).toBe('b');
94
- });
95
- });
96
-
97
- describe('execution with failover', () => {
98
- it('uses the highest-priority provider', async () => {
99
- const clientA = new MockClient('a');
100
- const clientB = new MockClient('b');
101
-
102
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
103
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
104
-
105
- const result = await router.chat([{ role: 'user', content: 'test' }]);
106
- expect(result.provider).toBe('a');
107
- });
108
-
109
- it('fails over to next provider on error', async () => {
110
- const clientA = new MockClient('a', {
111
- chatFn: async () => { throw new Error('A failed'); },
112
- });
113
- const clientB = new MockClient('b');
114
-
115
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
116
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
117
-
118
- const result = await router.chat([{ role: 'user', content: 'test' }]);
119
- expect(result.provider).toBe('b');
120
-
121
- // Check audit events
122
- const events = auditor.getEvents();
123
- const failoverEvents = events.filter(e => e.type === 'failover');
124
- expect(failoverEvents.length).toBeGreaterThan(0);
125
- });
126
-
127
- it('retries within a provider before failover', async () => {
128
- let attempts = 0;
129
- const clientA = new MockClient('a', {
130
- chatFn: async () => {
131
- attempts++;
132
- if (attempts === 1) throw new Error('Transient failure');
133
- return {
134
- message: { role: 'assistant' as const, content: 'recovered' },
135
- provider: 'a',
136
- };
137
- },
138
- });
139
-
140
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
141
- const result = await router.chat([{ role: 'user', content: 'test' }]);
142
-
143
- expect(result.provider).toBe('a');
144
- expect(attempts).toBe(2); // first attempt + 1 retry
145
- });
146
-
147
- it('throws when all providers fail', async () => {
148
- const clientA = new MockClient('a', {
149
- chatFn: async () => { throw new Error('A failed'); },
150
- });
151
- const clientB = new MockClient('b', {
152
- chatFn: async () => { throw new Error('B failed'); },
153
- });
154
-
155
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
156
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
157
-
158
- expect(router.chat([{ role: 'user', content: 'test' }])).rejects.toThrow();
159
- });
160
-
161
- it('throws when no providers configured', async () => {
162
- expect(router.chat([{ role: 'user', content: 'test' }])).rejects.toThrow(
163
- 'No available LLM providers'
164
- );
165
- });
166
- });
167
-
168
- describe('health tracking', () => {
169
- it('marks unhealthy after max failures', async () => {
170
- const failing = new MockClient('fail', {
171
- chatFn: async () => { throw new Error('always fails'); },
172
- });
173
- const backup = new MockClient('backup');
174
-
175
- router.addProvider({ id: 'fail', client: failing, priority: 0 });
176
- router.addProvider({ id: 'backup', client: backup, priority: 1 });
177
-
178
- // Each chat call records 1 failure for 'fail', maxFailures is 2
179
- await router.chat([{ role: 'user', content: 'test 1' }]);
180
- await router.chat([{ role: 'user', content: 'test 2' }]);
181
-
182
- const status = router.getStatus();
183
- const failStatus = status.find(s => s.id === 'fail');
184
- expect(failStatus!.healthy).toBe(false);
185
- });
186
-
187
- it('recovers after cooldown expires', async () => {
188
- let callCount = 0;
189
- const failing = new MockClient('fail', {
190
- chatFn: async () => {
191
- callCount++;
192
- if (callCount <= 4) throw new Error('failing');
193
- return {
194
- message: { role: 'assistant' as const, content: 'recovered' },
195
- provider: 'fail',
196
- };
197
- },
198
- });
199
- const backup = new MockClient('backup');
200
-
201
- router.addProvider({ id: 'fail', client: failing, priority: 0 });
202
- router.addProvider({ id: 'backup', client: backup, priority: 1 });
203
-
204
- // First call: fail → backup
205
- await router.chat([{ role: 'user', content: '1' }]);
206
-
207
- // Wait for cooldown (100ms in test config)
208
- await new Promise(r => setTimeout(r, 150));
209
-
210
- // After cooldown, fail should be tried again
211
- const status = router.getStatus();
212
- const failStatus = status.find(s => s.id === 'fail');
213
- expect(failStatus!.healthy).toBe(true);
214
- });
215
- });
216
-
217
- describe('tool registration', () => {
218
- it('broadcasts tool registration to all providers', () => {
219
- const clientA = new MockClient('a');
220
- const clientB = new MockClient('b');
221
-
222
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
223
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
224
-
225
- router.registerTool(
226
- 'test_tool',
227
- 'A test tool',
228
- { type: 'object', properties: {} },
229
- async () => 'result',
230
- );
231
-
232
- // Both clients should have the tool registered
233
- const defsA = clientA.getToolDefinitions();
234
- const defsB = clientB.getToolDefinitions();
235
- expect(defsA).toHaveLength(1);
236
- expect(defsB).toHaveLength(1);
237
- expect(defsA[0]!.function.name).toBe('test_tool');
238
- });
239
- });
240
-
241
- describe('model aggregation', () => {
242
- it('aggregates models from all providers', async () => {
243
- const clientA = new MockClient('a');
244
- const clientB = new MockClient('b');
245
-
246
- router.addProvider({ id: 'a', client: clientA, priority: 0 });
247
- router.addProvider({ id: 'b', client: clientB, priority: 1 });
248
-
249
- const models = await router.getModels();
250
- expect(models).toContain('mock-a');
251
- expect(models).toContain('mock-b');
252
- });
253
- });
254
- });
@@ -1,263 +0,0 @@
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
- });