universal-llm-client 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/ai-model.d.ts +20 -22
  2. package/dist/ai-model.d.ts.map +1 -1
  3. package/dist/ai-model.js +26 -23
  4. package/dist/ai-model.js.map +1 -1
  5. package/dist/client.d.ts +5 -5
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +17 -9
  8. package/dist/client.js.map +1 -1
  9. package/dist/http.d.ts +2 -0
  10. package/dist/http.d.ts.map +1 -1
  11. package/dist/http.js +1 -0
  12. package/dist/http.js.map +1 -1
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -4
  16. package/dist/index.js.map +1 -1
  17. package/dist/interfaces.d.ts +49 -11
  18. package/dist/interfaces.d.ts.map +1 -1
  19. package/dist/interfaces.js +14 -0
  20. package/dist/interfaces.js.map +1 -1
  21. package/dist/providers/anthropic.d.ts +56 -0
  22. package/dist/providers/anthropic.d.ts.map +1 -0
  23. package/dist/providers/anthropic.js +524 -0
  24. package/dist/providers/anthropic.js.map +1 -0
  25. package/dist/providers/google.d.ts +5 -0
  26. package/dist/providers/google.d.ts.map +1 -1
  27. package/dist/providers/google.js +64 -8
  28. package/dist/providers/google.js.map +1 -1
  29. package/dist/providers/index.d.ts +1 -0
  30. package/dist/providers/index.d.ts.map +1 -1
  31. package/dist/providers/index.js +1 -0
  32. package/dist/providers/index.js.map +1 -1
  33. package/dist/providers/ollama.d.ts.map +1 -1
  34. package/dist/providers/ollama.js +38 -11
  35. package/dist/providers/ollama.js.map +1 -1
  36. package/dist/providers/openai.d.ts.map +1 -1
  37. package/dist/providers/openai.js +9 -7
  38. package/dist/providers/openai.js.map +1 -1
  39. package/dist/router.d.ts +13 -33
  40. package/dist/router.d.ts.map +1 -1
  41. package/dist/router.js +33 -57
  42. package/dist/router.js.map +1 -1
  43. package/dist/stream-decoder.d.ts +29 -2
  44. package/dist/stream-decoder.d.ts.map +1 -1
  45. package/dist/stream-decoder.js +39 -11
  46. package/dist/stream-decoder.js.map +1 -1
  47. package/dist/structured-output.d.ts +107 -181
  48. package/dist/structured-output.d.ts.map +1 -1
  49. package/dist/structured-output.js +137 -192
  50. package/dist/structured-output.js.map +1 -1
  51. package/dist/zod-adapter.d.ts +44 -0
  52. package/dist/zod-adapter.d.ts.map +1 -0
  53. package/dist/zod-adapter.js +61 -0
  54. package/dist/zod-adapter.js.map +1 -0
  55. package/package.json +9 -1
  56. package/src/ai-model.ts +350 -0
  57. package/src/auditor.ts +213 -0
  58. package/src/client.ts +402 -0
  59. package/src/debug/debug-google-streaming.ts +97 -0
  60. package/src/debug/debug-tool-execution.ts +86 -0
  61. package/src/debug/test-lmstudio-tools.ts +155 -0
  62. package/src/demos/README.md +47 -0
  63. package/src/demos/basic/universal-llm-examples.ts +161 -0
  64. package/src/demos/mcp/astrid-memory-demo.ts +295 -0
  65. package/src/demos/mcp/astrid-persona-memory.ts +357 -0
  66. package/src/demos/mcp/mcp-mongodb-demo.ts +275 -0
  67. package/src/demos/mcp/simple-astrid-memory.ts +148 -0
  68. package/src/demos/mcp/simple-mcp-demo.ts +68 -0
  69. package/src/demos/mcp/working-mcp-demo.ts +62 -0
  70. package/src/demos/model-alias-demo.ts +0 -0
  71. package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +267 -0
  72. package/src/demos/tools/astrid-memory-demo.ts +270 -0
  73. package/src/demos/tools/astrid-production-memory-clean.ts +785 -0
  74. package/src/demos/tools/astrid-production-memory.ts +558 -0
  75. package/src/demos/tools/basic-translation-test.ts +66 -0
  76. package/src/demos/tools/chromadb-similarity-tuning.ts +390 -0
  77. package/src/demos/tools/clean-multilingual-conversation.ts +209 -0
  78. package/src/demos/tools/clean-translation-test.ts +119 -0
  79. package/src/demos/tools/clean-universal-multilingual-test.ts +131 -0
  80. package/src/demos/tools/complete-rag-demo.ts +369 -0
  81. package/src/demos/tools/complete-tool-demo.ts +132 -0
  82. package/src/demos/tools/demo-tool-calling.ts +124 -0
  83. package/src/demos/tools/dynamic-language-switching-test.ts +251 -0
  84. package/src/demos/tools/hybrid-thinking-test.ts +154 -0
  85. package/src/demos/tools/memory-integration-test.ts +420 -0
  86. package/src/demos/tools/multilingual-memory-system.ts +802 -0
  87. package/src/demos/tools/ondemand-translation-demo.ts +655 -0
  88. package/src/demos/tools/production-tool-demo.ts +245 -0
  89. package/src/demos/tools/revolutionary-multilingual-test.ts +151 -0
  90. package/src/demos/tools/rigorous-language-analysis.ts +218 -0
  91. package/src/demos/tools/test-universal-memory-system.ts +126 -0
  92. package/src/demos/tools/translation-integration-guide.ts +346 -0
  93. package/src/demos/tools/universal-memory-system.ts +560 -0
  94. package/src/http.ts +247 -0
  95. package/src/index.ts +161 -0
  96. package/src/interfaces.ts +657 -0
  97. package/src/mcp.ts +345 -0
  98. package/src/providers/anthropic.ts +762 -0
  99. package/src/providers/google.ts +620 -0
  100. package/src/providers/index.ts +8 -0
  101. package/src/providers/ollama.ts +469 -0
  102. package/src/providers/openai.ts +392 -0
  103. package/src/router.ts +780 -0
  104. package/src/stream-decoder.ts +361 -0
  105. package/src/structured-output.ts +759 -0
  106. package/src/test-scripts/test-advanced-tools.ts +310 -0
  107. package/src/test-scripts/test-google-streaming-enhanced.ts +147 -0
  108. package/src/test-scripts/test-google-streaming.ts +63 -0
  109. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -0
  110. package/src/test-scripts/test-mcp-config.ts +28 -0
  111. package/src/test-scripts/test-mcp-connection.ts +29 -0
  112. package/src/test-scripts/test-system-message-positions.ts +163 -0
  113. package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -0
  114. package/src/test-scripts/test-tool-calling.ts +231 -0
  115. package/src/tests/ai-model.test.ts +1614 -0
  116. package/src/tests/auditor.test.ts +224 -0
  117. package/src/tests/http.test.ts +200 -0
  118. package/src/tests/interfaces.test.ts +117 -0
  119. package/src/tests/providers/google.test.ts +660 -0
  120. package/src/tests/providers/ollama.test.ts +954 -0
  121. package/src/tests/providers/openai.test.ts +1122 -0
  122. package/src/tests/router.test.ts +254 -0
  123. package/src/tests/stream-decoder.test.ts +179 -0
  124. package/src/tests/structured-output.test.ts +1450 -0
  125. package/src/tests/tools.test.ts +175 -0
  126. package/src/tools.ts +246 -0
  127. package/src/zod-adapter.ts +72 -0
@@ -0,0 +1,254 @@
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
+ });
@@ -0,0 +1,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
+
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
+ });