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.
- package/dist/ai-model.d.ts +20 -22
- package/dist/ai-model.d.ts.map +1 -1
- package/dist/ai-model.js +26 -23
- package/dist/ai-model.js.map +1 -1
- package/dist/client.d.ts +5 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -9
- package/dist/client.js.map +1 -1
- package/dist/http.d.ts +2 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +1 -0
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +49 -11
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js +14 -0
- package/dist/interfaces.js.map +1 -1
- package/dist/providers/anthropic.d.ts +56 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +524 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/google.d.ts +5 -0
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +64 -8
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +38 -11
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +9 -7
- package/dist/providers/openai.js.map +1 -1
- package/dist/router.d.ts +13 -33
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +33 -57
- package/dist/router.js.map +1 -1
- package/dist/stream-decoder.d.ts +29 -2
- package/dist/stream-decoder.d.ts.map +1 -1
- package/dist/stream-decoder.js +39 -11
- package/dist/stream-decoder.js.map +1 -1
- package/dist/structured-output.d.ts +107 -181
- package/dist/structured-output.d.ts.map +1 -1
- package/dist/structured-output.js +137 -192
- package/dist/structured-output.js.map +1 -1
- package/dist/zod-adapter.d.ts +44 -0
- package/dist/zod-adapter.d.ts.map +1 -0
- package/dist/zod-adapter.js +61 -0
- package/dist/zod-adapter.js.map +1 -0
- package/package.json +9 -1
- package/src/ai-model.ts +350 -0
- package/src/auditor.ts +213 -0
- package/src/client.ts +402 -0
- package/src/debug/debug-google-streaming.ts +97 -0
- package/src/debug/debug-tool-execution.ts +86 -0
- package/src/debug/test-lmstudio-tools.ts +155 -0
- package/src/demos/README.md +47 -0
- package/src/demos/basic/universal-llm-examples.ts +161 -0
- package/src/demos/mcp/astrid-memory-demo.ts +295 -0
- package/src/demos/mcp/astrid-persona-memory.ts +357 -0
- package/src/demos/mcp/mcp-mongodb-demo.ts +275 -0
- package/src/demos/mcp/simple-astrid-memory.ts +148 -0
- package/src/demos/mcp/simple-mcp-demo.ts +68 -0
- package/src/demos/mcp/working-mcp-demo.ts +62 -0
- package/src/demos/model-alias-demo.ts +0 -0
- package/src/demos/tools/RAG_MEMORY_INTEGRATION.md +267 -0
- package/src/demos/tools/astrid-memory-demo.ts +270 -0
- package/src/demos/tools/astrid-production-memory-clean.ts +785 -0
- package/src/demos/tools/astrid-production-memory.ts +558 -0
- package/src/demos/tools/basic-translation-test.ts +66 -0
- package/src/demos/tools/chromadb-similarity-tuning.ts +390 -0
- package/src/demos/tools/clean-multilingual-conversation.ts +209 -0
- package/src/demos/tools/clean-translation-test.ts +119 -0
- package/src/demos/tools/clean-universal-multilingual-test.ts +131 -0
- package/src/demos/tools/complete-rag-demo.ts +369 -0
- package/src/demos/tools/complete-tool-demo.ts +132 -0
- package/src/demos/tools/demo-tool-calling.ts +124 -0
- package/src/demos/tools/dynamic-language-switching-test.ts +251 -0
- package/src/demos/tools/hybrid-thinking-test.ts +154 -0
- package/src/demos/tools/memory-integration-test.ts +420 -0
- package/src/demos/tools/multilingual-memory-system.ts +802 -0
- package/src/demos/tools/ondemand-translation-demo.ts +655 -0
- package/src/demos/tools/production-tool-demo.ts +245 -0
- package/src/demos/tools/revolutionary-multilingual-test.ts +151 -0
- package/src/demos/tools/rigorous-language-analysis.ts +218 -0
- package/src/demos/tools/test-universal-memory-system.ts +126 -0
- package/src/demos/tools/translation-integration-guide.ts +346 -0
- package/src/demos/tools/universal-memory-system.ts +560 -0
- package/src/http.ts +247 -0
- package/src/index.ts +161 -0
- package/src/interfaces.ts +657 -0
- package/src/mcp.ts +345 -0
- package/src/providers/anthropic.ts +762 -0
- package/src/providers/google.ts +620 -0
- package/src/providers/index.ts +8 -0
- package/src/providers/ollama.ts +469 -0
- package/src/providers/openai.ts +392 -0
- package/src/router.ts +780 -0
- package/src/stream-decoder.ts +361 -0
- package/src/structured-output.ts +759 -0
- package/src/test-scripts/test-advanced-tools.ts +310 -0
- package/src/test-scripts/test-google-streaming-enhanced.ts +147 -0
- package/src/test-scripts/test-google-streaming.ts +63 -0
- package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -0
- package/src/test-scripts/test-mcp-config.ts +28 -0
- package/src/test-scripts/test-mcp-connection.ts +29 -0
- package/src/test-scripts/test-system-message-positions.ts +163 -0
- package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -0
- package/src/test-scripts/test-tool-calling.ts +231 -0
- package/src/tests/ai-model.test.ts +1614 -0
- package/src/tests/auditor.test.ts +224 -0
- package/src/tests/http.test.ts +200 -0
- package/src/tests/interfaces.test.ts +117 -0
- package/src/tests/providers/google.test.ts +660 -0
- package/src/tests/providers/ollama.test.ts +954 -0
- package/src/tests/providers/openai.test.ts +1122 -0
- package/src/tests/router.test.ts +254 -0
- package/src/tests/stream-decoder.test.ts +179 -0
- package/src/tests/structured-output.test.ts +1450 -0
- package/src/tests/tools.test.ts +175 -0
- package/src/tools.ts +246 -0
- package/src/zod-adapter.ts +72 -0
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Decoded Event Types
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/** Clean, typed events emitted by a stream decoder */
|
|
17
|
+
export type DecodedEvent =
|
|
18
|
+
| { type: 'text'; content: string }
|
|
19
|
+
| { type: 'thinking'; content: string }
|
|
20
|
+
| { type: 'progress'; content: string }
|
|
21
|
+
| { type: 'tool_call'; calls: LLMToolCall[] };
|
|
22
|
+
|
|
23
|
+
/** Callback invoked by the decoder as events become available */
|
|
24
|
+
export type DecoderCallback = (event: DecodedEvent) => void;
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Decoder Interface
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Transform raw LLM tokens into clean typed events.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* const decoder = createDecoder('standard-chat', callback);
|
|
35
|
+
* for (const token of stream) decoder.push(token);
|
|
36
|
+
* decoder.flush();
|
|
37
|
+
* const clean = decoder.getCleanContent();
|
|
38
|
+
*/
|
|
39
|
+
export interface StreamDecoder {
|
|
40
|
+
/** Feed a raw token from the LLM stream */
|
|
41
|
+
push(token: string): void;
|
|
42
|
+
/** Signal end of stream — flush any buffered state */
|
|
43
|
+
flush(): void;
|
|
44
|
+
/** Get the accumulated clean text (all structural tags stripped) */
|
|
45
|
+
getCleanContent(): string;
|
|
46
|
+
/** Get accumulated reasoning/thinking content (if any) */
|
|
47
|
+
getReasoning(): string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Decoder Types
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export type DecoderType = 'passthrough' | 'standard-chat' | 'interleaved-reasoning';
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Passthrough Decoder
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bare-bones decoder for raw text completions.
|
|
62
|
+
* No parsing, no tag awareness. All tokens → text events.
|
|
63
|
+
*/
|
|
64
|
+
export class PassthroughDecoder implements StreamDecoder {
|
|
65
|
+
private content = '';
|
|
66
|
+
private readonly callback: DecoderCallback;
|
|
67
|
+
|
|
68
|
+
constructor(callback: DecoderCallback) {
|
|
69
|
+
this.callback = callback;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
push(token: string): void {
|
|
73
|
+
this.content += token;
|
|
74
|
+
this.callback({ type: 'text', content: token });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
flush(): void {
|
|
78
|
+
// Nothing to flush — all tokens emitted immediately
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getCleanContent(): string {
|
|
82
|
+
return this.content;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getReasoning(): string | undefined {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Standard Chat Decoder
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Decoder for standard LLM chat patterns — text streaming with native
|
|
96
|
+
* reasoning and structured API tool calls. No text-level tag parsing.
|
|
97
|
+
*
|
|
98
|
+
* Streamed tokens are clean text → emitted as `text` events.
|
|
99
|
+
* Native reasoning tokens → accepted via `pushReasoning()`.
|
|
100
|
+
* Structured tool calls → accepted via `pushToolCalls()`.
|
|
101
|
+
*/
|
|
102
|
+
export class StandardChatDecoder implements StreamDecoder {
|
|
103
|
+
private content = '';
|
|
104
|
+
private reasoning = '';
|
|
105
|
+
private readonly callback: DecoderCallback;
|
|
106
|
+
|
|
107
|
+
constructor(callback: DecoderCallback) {
|
|
108
|
+
this.callback = callback;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
push(token: string): void {
|
|
112
|
+
this.content += token;
|
|
113
|
+
this.callback({ type: 'text', content: token });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Feed native reasoning tokens from the provider */
|
|
117
|
+
pushReasoning(content: string): void {
|
|
118
|
+
this.reasoning += content;
|
|
119
|
+
this.callback({ type: 'thinking', content });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Feed structured tool calls from the provider API response */
|
|
123
|
+
pushToolCalls(calls: LLMToolCall[]): void {
|
|
124
|
+
this.callback({ type: 'tool_call', calls });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
flush(): void {
|
|
128
|
+
// Nothing to flush — all events emitted as they arrive
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getCleanContent(): string {
|
|
132
|
+
return this.content;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getReasoning(): string | undefined {
|
|
136
|
+
return this.reasoning || undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// Interleaved Reasoning Decoder
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Decoder for models that emit interleaved reasoning tags in text.
|
|
146
|
+
* Parses <think>...</think> and <progress>...</progress> tags from the
|
|
147
|
+
* raw token stream and emits typed events for each.
|
|
148
|
+
*
|
|
149
|
+
* Handles streaming where tags may be split across chunks.
|
|
150
|
+
*/
|
|
151
|
+
export class InterleavedReasoningDecoder implements StreamDecoder {
|
|
152
|
+
private buffer = '';
|
|
153
|
+
private content = '';
|
|
154
|
+
private reasoning = '';
|
|
155
|
+
private readonly callback: DecoderCallback;
|
|
156
|
+
private inThink = false;
|
|
157
|
+
private inProgress = false;
|
|
158
|
+
|
|
159
|
+
constructor(callback: DecoderCallback) {
|
|
160
|
+
this.callback = callback;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
push(token: string): void {
|
|
164
|
+
this.buffer += token;
|
|
165
|
+
this.processBuffer();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
flush(): void {
|
|
169
|
+
// Emit any remaining buffer content as text
|
|
170
|
+
if (this.buffer.length > 0) {
|
|
171
|
+
if (this.inThink) {
|
|
172
|
+
this.reasoning += this.buffer;
|
|
173
|
+
this.callback({ type: 'thinking', content: this.buffer });
|
|
174
|
+
} else if (this.inProgress) {
|
|
175
|
+
this.callback({ type: 'progress', content: this.buffer });
|
|
176
|
+
} else {
|
|
177
|
+
this.content += this.buffer;
|
|
178
|
+
this.callback({ type: 'text', content: this.buffer });
|
|
179
|
+
}
|
|
180
|
+
this.buffer = '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getCleanContent(): string {
|
|
185
|
+
return this.content;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getReasoning(): string | undefined {
|
|
189
|
+
return this.reasoning || undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private processBuffer(): void {
|
|
193
|
+
let safety = 0;
|
|
194
|
+
while (this.buffer.length > 0 && safety++ < 200) {
|
|
195
|
+
if (this.inThink) {
|
|
196
|
+
const closeIdx = this.buffer.indexOf('</think>');
|
|
197
|
+
if (closeIdx === -1) {
|
|
198
|
+
// Might have partial closing tag at end
|
|
199
|
+
if (this.buffer.endsWith('<') || this.buffer.endsWith('</') ||
|
|
200
|
+
this.buffer.endsWith('</t') || this.buffer.endsWith('</th') ||
|
|
201
|
+
this.buffer.endsWith('</thi') || this.buffer.endsWith('</thin') ||
|
|
202
|
+
this.buffer.endsWith('</think')) {
|
|
203
|
+
return; // Wait for more data
|
|
204
|
+
}
|
|
205
|
+
this.reasoning += this.buffer;
|
|
206
|
+
this.callback({ type: 'thinking', content: this.buffer });
|
|
207
|
+
this.buffer = '';
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const thinkContent = this.buffer.slice(0, closeIdx);
|
|
211
|
+
if (thinkContent) {
|
|
212
|
+
this.reasoning += thinkContent;
|
|
213
|
+
this.callback({ type: 'thinking', content: thinkContent });
|
|
214
|
+
}
|
|
215
|
+
this.buffer = this.buffer.slice(closeIdx + 8); // '</think>'.length
|
|
216
|
+
this.inThink = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (this.inProgress) {
|
|
221
|
+
const closeIdx = this.buffer.indexOf('</progress>');
|
|
222
|
+
if (closeIdx === -1) {
|
|
223
|
+
if (this.couldBePartialTag(this.buffer, '</progress>')) return;
|
|
224
|
+
this.callback({ type: 'progress', content: this.buffer });
|
|
225
|
+
this.buffer = '';
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const progressContent = this.buffer.slice(0, closeIdx);
|
|
229
|
+
if (progressContent) {
|
|
230
|
+
this.callback({ type: 'progress', content: progressContent });
|
|
231
|
+
}
|
|
232
|
+
this.buffer = this.buffer.slice(closeIdx + 11); // '</progress>'.length
|
|
233
|
+
this.inProgress = false;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Look for opening tags
|
|
238
|
+
const thinkIdx = this.buffer.indexOf('<think>');
|
|
239
|
+
const progressIdx = this.buffer.indexOf('<progress>');
|
|
240
|
+
|
|
241
|
+
// Find earliest tag
|
|
242
|
+
const nextTag = this.findEarliest(thinkIdx, progressIdx);
|
|
243
|
+
|
|
244
|
+
if (nextTag === -1) {
|
|
245
|
+
// No complete opening tags — check for partial tag at end
|
|
246
|
+
const lastAngle = this.buffer.lastIndexOf('<');
|
|
247
|
+
if (lastAngle >= 0 && lastAngle > this.buffer.length - 12) {
|
|
248
|
+
// Potential partial tag — emit text before it, keep the rest
|
|
249
|
+
const textBefore = this.buffer.slice(0, lastAngle);
|
|
250
|
+
if (textBefore) {
|
|
251
|
+
this.content += textBefore;
|
|
252
|
+
this.callback({ type: 'text', content: textBefore });
|
|
253
|
+
}
|
|
254
|
+
this.buffer = this.buffer.slice(lastAngle);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// No partial tags — emit all as text
|
|
258
|
+
this.content += this.buffer;
|
|
259
|
+
this.callback({ type: 'text', content: this.buffer });
|
|
260
|
+
this.buffer = '';
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Emit text before the tag
|
|
265
|
+
const textBefore = this.buffer.slice(0, nextTag);
|
|
266
|
+
if (textBefore) {
|
|
267
|
+
this.content += textBefore;
|
|
268
|
+
this.callback({ type: 'text', content: textBefore });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (nextTag === thinkIdx) {
|
|
272
|
+
this.buffer = this.buffer.slice(nextTag + 7); // '<think>'.length
|
|
273
|
+
this.inThink = true;
|
|
274
|
+
} else {
|
|
275
|
+
this.buffer = this.buffer.slice(nextTag + 10); // '<progress>'.length
|
|
276
|
+
this.inProgress = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private findEarliest(a: number, b: number): number {
|
|
282
|
+
if (a === -1) return b;
|
|
283
|
+
if (b === -1) return a;
|
|
284
|
+
return Math.min(a, b);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private couldBePartialTag(buffer: string, tag: string): boolean {
|
|
288
|
+
for (let i = 1; i < tag.length; i++) {
|
|
289
|
+
if (buffer.endsWith(tag.slice(0, i))) return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// Pluggable Decoder Registry
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
export interface DecoderOptions {
|
|
300
|
+
/** Known tool names for text-based tool call recovery */
|
|
301
|
+
knownToolNames?: Set<string>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Factory function that creates a StreamDecoder instance.
|
|
306
|
+
* External code registers these via `registerDecoder()`.
|
|
307
|
+
*/
|
|
308
|
+
export type DecoderFactory = (callback: DecoderCallback, options?: DecoderOptions) => StreamDecoder;
|
|
309
|
+
|
|
310
|
+
/** Internal registry of decoder factories, keyed by decoder type name */
|
|
311
|
+
const decoderRegistry = new Map<string, DecoderFactory>();
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Register a custom stream decoder type.
|
|
315
|
+
* Once registered, it can be used via `createDecoder(name, ...)` or
|
|
316
|
+
* by passing `decoderType: name` in ChatOptions.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* import { registerDecoder } from 'universal-llm-client';
|
|
321
|
+
*
|
|
322
|
+
* registerDecoder('my-decoder', (callback, options) => {
|
|
323
|
+
* return new MyCustomDecoder(callback, options);
|
|
324
|
+
* });
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
export function registerDecoder(type: string, factory: DecoderFactory): void {
|
|
328
|
+
decoderRegistry.set(type, factory);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get all registered decoder type names.
|
|
333
|
+
*/
|
|
334
|
+
export function getRegisteredDecoders(): string[] {
|
|
335
|
+
return Array.from(decoderRegistry.keys());
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Pre-register built-in decoders
|
|
339
|
+
registerDecoder('passthrough', (cb) => new PassthroughDecoder(cb));
|
|
340
|
+
registerDecoder('standard-chat', (cb) => new StandardChatDecoder(cb));
|
|
341
|
+
registerDecoder('interleaved-reasoning', (cb) => new InterleavedReasoningDecoder(cb));
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Create a stream decoder by type name.
|
|
345
|
+
* Looks up the decoder in the registry (built-in + custom).
|
|
346
|
+
*
|
|
347
|
+
* @throws Error if the decoder type is not registered
|
|
348
|
+
*/
|
|
349
|
+
export function createDecoder(
|
|
350
|
+
type: DecoderType | string,
|
|
351
|
+
callback: DecoderCallback,
|
|
352
|
+
options?: DecoderOptions,
|
|
353
|
+
): StreamDecoder {
|
|
354
|
+
const factory = decoderRegistry.get(type);
|
|
355
|
+
if (!factory) {
|
|
356
|
+
const available = Array.from(decoderRegistry.keys()).join(', ');
|
|
357
|
+
throw new Error(`Unknown decoder type: "${type}". Available: ${available}`);
|
|
358
|
+
}
|
|
359
|
+
return factory(callback, options);
|
|
360
|
+
}
|
|
361
|
+
|