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,535 +0,0 @@
1
- /**
2
- * Universal LLM Client v3 — Stream Decoder
3
- *
4
- * Pluggable interface for decoding raw LLM token streams into typed events.
5
- * Consumers select their strategy per-call: passthrough for raw speed,
6
- * standard-chat for structured tool calls, or interleaved-reasoning
7
- * for models that emit <think>/<progress> tags.
8
- */
9
-
10
- import type { LLMToolCall } from './interfaces.js';
11
- import { GEMMA_THOUGHT_OPENERS, normalizeGemmaThought } from './gemma-channel.js';
12
-
13
- // ============================================================================
14
- // Decoded Event Types
15
- // ============================================================================
16
-
17
- /** Clean, typed events emitted by a stream decoder */
18
- export type DecodedEvent =
19
- | { type: 'text'; content: string }
20
- | { type: 'thinking'; content: string }
21
- | { type: 'progress'; content: string }
22
- | { type: 'tool_call'; calls: LLMToolCall[] };
23
-
24
- /** Callback invoked by the decoder as events become available */
25
- export type DecoderCallback = (event: DecodedEvent) => void;
26
-
27
- // ============================================================================
28
- // Decoder Interface
29
- // ============================================================================
30
-
31
- /**
32
- * Transform raw LLM tokens into clean typed events.
33
- *
34
- * Usage:
35
- * const decoder = createDecoder('standard-chat', callback);
36
- * for (const token of stream) decoder.push(token);
37
- * decoder.flush();
38
- * const clean = decoder.getCleanContent();
39
- */
40
- export interface StreamDecoder {
41
- /** Feed a raw token from the LLM stream */
42
- push(token: string): void;
43
- /** Signal end of stream — flush any buffered state */
44
- flush(): void;
45
- /** Get the accumulated clean text (all structural tags stripped) */
46
- getCleanContent(): string;
47
- /** Get accumulated reasoning/thinking content (if any) */
48
- getReasoning(): string | undefined;
49
- }
50
-
51
- // ============================================================================
52
- // Decoder Types
53
- // ============================================================================
54
-
55
- export type DecoderType = 'passthrough' | 'standard-chat' | 'interleaved-reasoning';
56
-
57
- // ============================================================================
58
- // Passthrough Decoder
59
- // ============================================================================
60
-
61
- /**
62
- * Bare-bones decoder for raw text completions.
63
- * No parsing, no tag awareness. All tokens → text events.
64
- */
65
- export class PassthroughDecoder implements StreamDecoder {
66
- private content = '';
67
- private readonly callback: DecoderCallback;
68
-
69
- constructor(callback: DecoderCallback) {
70
- this.callback = callback;
71
- }
72
-
73
- push(token: string): void {
74
- this.content += token;
75
- this.callback({ type: 'text', content: token });
76
- }
77
-
78
- flush(): void {
79
- // Nothing to flush — all tokens emitted immediately
80
- }
81
-
82
- getCleanContent(): string {
83
- return this.content;
84
- }
85
-
86
- getReasoning(): string | undefined {
87
- return undefined;
88
- }
89
- }
90
-
91
- // ============================================================================
92
- // Standard Chat Decoder
93
- // ============================================================================
94
-
95
- /**
96
- * Decoder for standard LLM chat patterns — text streaming with native
97
- * reasoning and structured API tool calls. No text-level tag parsing.
98
- *
99
- * Streamed tokens are clean text → emitted as `text` events.
100
- * Native reasoning tokens → accepted via `pushReasoning()`.
101
- * Structured tool calls → accepted via `pushToolCalls()`.
102
- */
103
- export class StandardChatDecoder implements StreamDecoder {
104
- private content = '';
105
- private reasoning = '';
106
- private readonly callback: DecoderCallback;
107
- private tagBuffer = '';
108
- private inProgressTag = false;
109
- private progressBody = '';
110
- private inGemmaThought = false;
111
- private gemmaThoughtBody = '';
112
- private gemmaThoughtClose = '';
113
- private inToolCallTag = false;
114
- private toolCallBody = '';
115
- private toolCallClose = '';
116
-
117
- constructor(callback: DecoderCallback) {
118
- this.callback = callback;
119
- }
120
-
121
- push(token: string): void {
122
- let pos = 0;
123
-
124
- while (pos < token.length) {
125
- if (this.inGemmaThought) {
126
- this.gemmaThoughtBody += token.slice(pos);
127
- const closeIdx = this.gemmaThoughtBody.indexOf(this.gemmaThoughtClose);
128
- if (closeIdx !== -1) {
129
- const body = this.gemmaThoughtBody.slice(0, closeIdx);
130
- const remainder = this.gemmaThoughtBody.slice(closeIdx + this.gemmaThoughtClose.length);
131
- this.emitReasoning(normalizeGemmaThought(body));
132
- this.inGemmaThought = false;
133
- this.gemmaThoughtBody = '';
134
- this.gemmaThoughtClose = '';
135
- if (remainder) this.push(remainder);
136
- }
137
- return;
138
- }
139
-
140
- if (this.inToolCallTag) {
141
- this.toolCallBody += token.slice(pos);
142
- const closeIdx = this.toolCallBody.indexOf(this.toolCallClose);
143
- if (closeIdx !== -1) {
144
- const body = this.toolCallBody.slice(0, closeIdx);
145
- const remainder = this.toolCallBody.slice(closeIdx + this.toolCallClose.length);
146
-
147
- if (body.trim()) {
148
- try {
149
- const normalizedJson = body.trim()
150
- .replace(/'/g, '"')
151
- .replace(/True/g, 'true')
152
- .replace(/False/g, 'false')
153
- .replace(/None/g, 'null');
154
- const parsed = JSON.parse(normalizedJson);
155
- const calls = Array.isArray(parsed) ? parsed : [parsed];
156
- const validatedCalls: LLMToolCall[] = [];
157
- for (const call of calls) {
158
- if (call && typeof call === 'object' && call.name) {
159
- validatedCalls.push({
160
- id: call.id || `recovered_${Date.now()}_${Math.random().toString(36).slice(2)}`,
161
- type: 'function',
162
- function: {
163
- name: call.name,
164
- arguments: typeof call.arguments === 'string'
165
- ? call.arguments
166
- : JSON.stringify(call.arguments ?? {}),
167
- }
168
- });
169
- }
170
- }
171
- if (validatedCalls.length > 0) {
172
- this.callback({ type: 'tool_call', calls: validatedCalls });
173
- }
174
- } catch {
175
- // ignore
176
- }
177
- }
178
-
179
- this.inToolCallTag = false;
180
- this.toolCallBody = '';
181
- this.toolCallClose = '';
182
- if (remainder) this.push(remainder);
183
- }
184
- return;
185
- }
186
-
187
- if (this.inProgressTag) {
188
- this.progressBody += token.slice(pos);
189
- const closeIdx = this.progressBody.indexOf('</progress>');
190
- if (closeIdx !== -1) {
191
- const body = this.progressBody.slice(0, closeIdx);
192
- const remainder = this.progressBody.slice(closeIdx + '</progress>'.length);
193
- if (body) {
194
- this.callback({ type: 'progress', content: body });
195
- }
196
- this.inProgressTag = false;
197
- this.progressBody = '';
198
- if (remainder) this.push(remainder);
199
- }
200
- return;
201
- }
202
-
203
- if (this.tagBuffer.length > 0) {
204
- const ch = token[pos]!;
205
- pos++;
206
- this.tagBuffer += ch;
207
- if (this.matchesStructuralOpenerPrefix(this.tagBuffer)) {
208
- if (this.tagBuffer === '<progress>') {
209
- this.inProgressTag = true;
210
- this.progressBody = '';
211
- this.tagBuffer = '';
212
- } else if (this.tagBuffer === '<tool_call|>') {
213
- this.inToolCallTag = true;
214
- this.toolCallBody = '';
215
- this.toolCallClose = '<|tool_response>';
216
- this.tagBuffer = '';
217
- } else if (this.tagBuffer === '<|tool_response>') {
218
- this.tagBuffer = '';
219
- } else if (this.tagBuffer === '<|channel>thought') {
220
- this.inGemmaThought = true;
221
- this.gemmaThoughtBody = '';
222
- this.gemmaThoughtClose = '<channel|>';
223
- this.tagBuffer = '';
224
- } else if (this.tagBuffer === '<|thought') {
225
- this.inGemmaThought = true;
226
- this.gemmaThoughtBody = '';
227
- this.gemmaThoughtClose = '|>';
228
- this.tagBuffer = '';
229
- }
230
- } else {
231
- this.emitText(this.tagBuffer);
232
- this.tagBuffer = '';
233
- }
234
- continue;
235
- }
236
-
237
- const ltIdx = token.indexOf('<', pos);
238
- if (ltIdx === -1) {
239
- this.emitText(token.slice(pos));
240
- return;
241
- }
242
-
243
- if (ltIdx > pos) {
244
- this.emitText(token.slice(pos, ltIdx));
245
- }
246
- this.tagBuffer = '<';
247
- pos = ltIdx + 1;
248
- }
249
- }
250
-
251
- private emitText(text: string): void {
252
- if (!text) return;
253
- this.content += text;
254
- this.callback({ type: 'text', content: text });
255
- }
256
-
257
- private emitReasoning(content: string): void {
258
- if (!content) return;
259
- this.reasoning += content;
260
- this.callback({ type: 'thinking', content });
261
- }
262
-
263
- private matchesStructuralOpenerPrefix(candidate: string): boolean {
264
- if ('<progress>'.startsWith(candidate)) return true;
265
- if ('<tool_call|>'.startsWith(candidate)) return true;
266
- if ('<|tool_response>'.startsWith(candidate)) return true;
267
- return GEMMA_THOUGHT_OPENERS.some(opener => opener.startsWith(candidate));
268
- }
269
-
270
- /** Feed native reasoning tokens from the provider */
271
- pushReasoning(content: string): void {
272
- this.emitReasoning(content);
273
- }
274
-
275
- /** Feed structured tool calls from the provider API response */
276
- pushToolCalls(calls: LLMToolCall[]): void {
277
- this.callback({ type: 'tool_call', calls });
278
- }
279
-
280
- flush(): void {
281
- if (this.tagBuffer) {
282
- this.emitText(this.tagBuffer);
283
- this.tagBuffer = '';
284
- }
285
- if (this.inGemmaThought) {
286
- this.emitReasoning(normalizeGemmaThought(this.gemmaThoughtBody));
287
- this.inGemmaThought = false;
288
- this.gemmaThoughtBody = '';
289
- this.gemmaThoughtClose = '';
290
- }
291
- if (this.inProgressTag) {
292
- if (this.progressBody) {
293
- this.emitText('<progress>' + this.progressBody);
294
- }
295
- this.inProgressTag = false;
296
- this.progressBody = '';
297
- }
298
- if (this.inToolCallTag) {
299
- this.inToolCallTag = false;
300
- this.toolCallBody = '';
301
- this.toolCallClose = '';
302
- }
303
- }
304
-
305
- getCleanContent(): string {
306
- return this.content;
307
- }
308
-
309
- getReasoning(): string | undefined {
310
- return this.reasoning || undefined;
311
- }
312
- }
313
-
314
- // ============================================================================
315
- // Interleaved Reasoning Decoder
316
- // ============================================================================
317
-
318
- /**
319
- * Decoder for models that emit interleaved reasoning tags in text.
320
- * Parses <think>...</think> and <progress>...</progress> tags from the
321
- * raw token stream and emits typed events for each.
322
- *
323
- * Handles streaming where tags may be split across chunks.
324
- */
325
- export class InterleavedReasoningDecoder implements StreamDecoder {
326
- private buffer = '';
327
- private content = '';
328
- private reasoning = '';
329
- private readonly callback: DecoderCallback;
330
- private inThink = false;
331
- private inProgress = false;
332
-
333
- constructor(callback: DecoderCallback) {
334
- this.callback = callback;
335
- }
336
-
337
- push(token: string): void {
338
- this.buffer += token;
339
- this.processBuffer();
340
- }
341
-
342
- flush(): void {
343
- // Emit any remaining buffer content as text
344
- if (this.buffer.length > 0) {
345
- if (this.inThink) {
346
- this.reasoning += this.buffer;
347
- this.callback({ type: 'thinking', content: this.buffer });
348
- } else if (this.inProgress) {
349
- this.callback({ type: 'progress', content: this.buffer });
350
- } else {
351
- this.content += this.buffer;
352
- this.callback({ type: 'text', content: this.buffer });
353
- }
354
- this.buffer = '';
355
- }
356
- }
357
-
358
- getCleanContent(): string {
359
- return this.content;
360
- }
361
-
362
- getReasoning(): string | undefined {
363
- return this.reasoning || undefined;
364
- }
365
-
366
- private processBuffer(): void {
367
- let safety = 0;
368
- while (this.buffer.length > 0 && safety++ < 200) {
369
- if (this.inThink) {
370
- const closeIdx = this.buffer.indexOf('</think>');
371
- if (closeIdx === -1) {
372
- // Might have partial closing tag at end
373
- if (this.buffer.endsWith('<') || this.buffer.endsWith('</') ||
374
- this.buffer.endsWith('</t') || this.buffer.endsWith('</th') ||
375
- this.buffer.endsWith('</thi') || this.buffer.endsWith('</thin') ||
376
- this.buffer.endsWith('</think')) {
377
- return; // Wait for more data
378
- }
379
- this.reasoning += this.buffer;
380
- this.callback({ type: 'thinking', content: this.buffer });
381
- this.buffer = '';
382
- return;
383
- }
384
- const thinkContent = this.buffer.slice(0, closeIdx);
385
- if (thinkContent) {
386
- this.reasoning += thinkContent;
387
- this.callback({ type: 'thinking', content: thinkContent });
388
- }
389
- this.buffer = this.buffer.slice(closeIdx + 8); // '</think>'.length
390
- this.inThink = false;
391
- continue;
392
- }
393
-
394
- if (this.inProgress) {
395
- const closeIdx = this.buffer.indexOf('</progress>');
396
- if (closeIdx === -1) {
397
- if (this.couldBePartialTag(this.buffer, '</progress>')) return;
398
- this.callback({ type: 'progress', content: this.buffer });
399
- this.buffer = '';
400
- return;
401
- }
402
- const progressContent = this.buffer.slice(0, closeIdx);
403
- if (progressContent) {
404
- this.callback({ type: 'progress', content: progressContent });
405
- }
406
- this.buffer = this.buffer.slice(closeIdx + 11); // '</progress>'.length
407
- this.inProgress = false;
408
- continue;
409
- }
410
-
411
- // Look for opening tags
412
- const thinkIdx = this.buffer.indexOf('<think>');
413
- const progressIdx = this.buffer.indexOf('<progress>');
414
-
415
- // Find earliest tag
416
- const nextTag = this.findEarliest(thinkIdx, progressIdx);
417
-
418
- if (nextTag === -1) {
419
- // No complete opening tags — check for partial tag at end
420
- const lastAngle = this.buffer.lastIndexOf('<');
421
- if (lastAngle >= 0 && lastAngle > this.buffer.length - 12) {
422
- // Potential partial tag — emit text before it, keep the rest
423
- const textBefore = this.buffer.slice(0, lastAngle);
424
- if (textBefore) {
425
- this.content += textBefore;
426
- this.callback({ type: 'text', content: textBefore });
427
- }
428
- this.buffer = this.buffer.slice(lastAngle);
429
- return;
430
- }
431
- // No partial tags — emit all as text
432
- this.content += this.buffer;
433
- this.callback({ type: 'text', content: this.buffer });
434
- this.buffer = '';
435
- return;
436
- }
437
-
438
- // Emit text before the tag
439
- const textBefore = this.buffer.slice(0, nextTag);
440
- if (textBefore) {
441
- this.content += textBefore;
442
- this.callback({ type: 'text', content: textBefore });
443
- }
444
-
445
- if (nextTag === thinkIdx) {
446
- this.buffer = this.buffer.slice(nextTag + 7); // '<think>'.length
447
- this.inThink = true;
448
- } else {
449
- this.buffer = this.buffer.slice(nextTag + 10); // '<progress>'.length
450
- this.inProgress = true;
451
- }
452
- }
453
- }
454
-
455
- private findEarliest(a: number, b: number): number {
456
- if (a === -1) return b;
457
- if (b === -1) return a;
458
- return Math.min(a, b);
459
- }
460
-
461
- private couldBePartialTag(buffer: string, tag: string): boolean {
462
- for (let i = 1; i < tag.length; i++) {
463
- if (buffer.endsWith(tag.slice(0, i))) return true;
464
- }
465
- return false;
466
- }
467
- }
468
-
469
- // ============================================================================
470
- // Pluggable Decoder Registry
471
- // ============================================================================
472
-
473
- export interface DecoderOptions {
474
- /** Known tool names for text-based tool call recovery */
475
- knownToolNames?: Set<string>;
476
- }
477
-
478
- /**
479
- * Factory function that creates a StreamDecoder instance.
480
- * External code registers these via `registerDecoder()`.
481
- */
482
- export type DecoderFactory = (callback: DecoderCallback, options?: DecoderOptions) => StreamDecoder;
483
-
484
- /** Internal registry of decoder factories, keyed by decoder type name */
485
- const decoderRegistry = new Map<string, DecoderFactory>();
486
-
487
- /**
488
- * Register a custom stream decoder type.
489
- * Once registered, it can be used via `createDecoder(name, ...)` or
490
- * by passing `decoderType: name` in ChatOptions.
491
- *
492
- * @example
493
- * ```typescript
494
- * import { registerDecoder } from 'universal-llm-client';
495
- *
496
- * registerDecoder('my-decoder', (callback, options) => {
497
- * return new MyCustomDecoder(callback, options);
498
- * });
499
- * ```
500
- */
501
- export function registerDecoder(type: string, factory: DecoderFactory): void {
502
- decoderRegistry.set(type, factory);
503
- }
504
-
505
- /**
506
- * Get all registered decoder type names.
507
- */
508
- export function getRegisteredDecoders(): string[] {
509
- return Array.from(decoderRegistry.keys());
510
- }
511
-
512
- // Pre-register built-in decoders
513
- registerDecoder('passthrough', (cb) => new PassthroughDecoder(cb));
514
- registerDecoder('standard-chat', (cb) => new StandardChatDecoder(cb));
515
- registerDecoder('interleaved-reasoning', (cb) => new InterleavedReasoningDecoder(cb));
516
-
517
- /**
518
- * Create a stream decoder by type name.
519
- * Looks up the decoder in the registry (built-in + custom).
520
- *
521
- * @throws Error if the decoder type is not registered
522
- */
523
- export function createDecoder(
524
- type: DecoderType | string,
525
- callback: DecoderCallback,
526
- options?: DecoderOptions,
527
- ): StreamDecoder {
528
- const factory = decoderRegistry.get(type);
529
- if (!factory) {
530
- const available = Array.from(decoderRegistry.keys()).join(', ');
531
- throw new Error(`Unknown decoder type: "${type}". Available: ${available}`);
532
- }
533
- return factory(callback, options);
534
- }
535
-