universal-llm-client 4.2.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -103
- package/LICENSE +21 -21
- package/README.md +640 -591
- package/dist/ai-model.d.ts +12 -1
- package/dist/ai-model.d.ts.map +1 -1
- package/dist/ai-model.js +36 -1
- package/dist/ai-model.js.map +1 -1
- package/dist/gemma-channel.d.ts +14 -0
- package/dist/gemma-channel.d.ts.map +1 -0
- package/dist/gemma-channel.js +38 -0
- package/dist/gemma-channel.js.map +1 -0
- package/dist/gemma-diffusion.d.ts +49 -0
- package/dist/gemma-diffusion.d.ts.map +1 -0
- package/dist/gemma-diffusion.js +147 -0
- package/dist/gemma-diffusion.js.map +1 -0
- package/dist/http.d.ts +4 -0
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +14 -1
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +183 -7
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +28 -3
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/google.d.ts +22 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +225 -13
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/ollama.d.ts +2 -0
- package/dist/providers/ollama.d.ts.map +1 -1
- package/dist/providers/ollama.js +59 -30
- package/dist/providers/ollama.js.map +1 -1
- package/dist/providers/openai.d.ts +14 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +200 -22
- package/dist/providers/openai.js.map +1 -1
- package/dist/router.d.ts +2 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -0
- package/dist/router.js.map +1 -1
- package/dist/stream-decoder.d.ts +12 -0
- package/dist/stream-decoder.d.ts.map +1 -1
- package/dist/stream-decoder.js +182 -5
- package/dist/stream-decoder.js.map +1 -1
- package/dist/thinking.d.ts +36 -0
- package/dist/thinking.d.ts.map +1 -0
- package/dist/thinking.js +52 -0
- package/dist/thinking.js.map +1 -0
- package/package.json +118 -116
- package/src/ai-model.ts +400 -350
- package/src/auditor.ts +213 -213
- package/src/client.ts +402 -402
- package/src/debug/debug-google-streaming.ts +1 -1
- package/src/demos/basic/universal-llm-examples.ts +3 -3
- package/src/demos/diffusion-gemma/.env +29 -0
- package/src/demos/diffusion-gemma/.env.example +27 -0
- package/src/demos/diffusion-gemma/CLAUDE.md +95 -0
- package/src/demos/diffusion-gemma/README.md +59 -0
- package/src/demos/diffusion-gemma/canvas.ts +1606 -0
- package/src/demos/diffusion-gemma/docker-compose.yml +29 -0
- package/src/demos/diffusion-gemma/probe-stream.ts +51 -0
- package/src/demos/diffusion-gemma/probe-tools.ts +55 -0
- package/src/demos/diffusion-gemma/server.ts +1205 -0
- package/src/demos/diffusion-gemma/start-vllm.sh +98 -0
- package/src/gemma-channel.ts +47 -0
- package/src/gemma-diffusion.ts +167 -0
- package/src/http.ts +261 -247
- package/src/index.ts +180 -161
- package/src/interfaces.ts +843 -657
- package/src/mcp.ts +345 -345
- package/src/providers/anthropic.ts +796 -762
- package/src/providers/google.ts +840 -620
- package/src/providers/index.ts +8 -8
- package/src/providers/ollama.ts +503 -469
- package/src/providers/openai.ts +587 -392
- package/src/router.ts +785 -780
- package/src/stream-decoder.ts +535 -361
- package/src/structured-output.ts +759 -759
- package/src/test-scripts/test-google-deep-research.ts +33 -0
- package/src/test-scripts/test-google-streaming-enhanced.ts +147 -147
- package/src/test-scripts/test-google-streaming.ts +1 -1
- package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -189
- package/src/test-scripts/test-google-thinking.ts +46 -0
- package/src/test-scripts/test-system-message-positions.ts +163 -163
- package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -83
- package/src/test-scripts/test-vllm-qwen36.ts +256 -0
- package/src/tests/ai-model.test.ts +1614 -1614
- package/src/tests/auditor.test.ts +224 -224
- package/src/tests/gemma-diffusion.test.ts +115 -0
- package/src/tests/http.test.ts +200 -200
- package/src/tests/interfaces.test.ts +117 -117
- package/src/tests/providers/anthropic.test.ts +118 -0
- package/src/tests/providers/google.test.ts +841 -660
- package/src/tests/providers/ollama.test.ts +1034 -954
- package/src/tests/providers/openai.test.ts +1511 -1122
- package/src/tests/router.test.ts +254 -254
- package/src/tests/stream-decoder.test.ts +263 -179
- package/src/tests/structured-output.test.ts +1450 -1450
- package/src/tests/thinking.test.ts +65 -0
- package/src/tests/tools.test.ts +175 -175
- package/src/thinking.ts +73 -0
- package/src/tools.ts +246 -246
- package/src/zod-adapter.ts +72 -72
|
@@ -1,179 +1,263 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for stream-decoder.ts — Pluggable reasoning strategies
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from 'bun:test';
|
|
5
|
-
import {
|
|
6
|
-
PassthroughDecoder,
|
|
7
|
-
StandardChatDecoder,
|
|
8
|
-
InterleavedReasoningDecoder,
|
|
9
|
-
createDecoder,
|
|
10
|
-
type DecodedEvent,
|
|
11
|
-
} from '../stream-decoder.js';
|
|
12
|
-
|
|
13
|
-
describe('PassthroughDecoder', () => {
|
|
14
|
-
it('emits all tokens as text events', () => {
|
|
15
|
-
const events: DecodedEvent[] = [];
|
|
16
|
-
const decoder = new PassthroughDecoder(e => events.push(e));
|
|
17
|
-
|
|
18
|
-
decoder.push('Hello');
|
|
19
|
-
decoder.push(' world');
|
|
20
|
-
decoder.flush();
|
|
21
|
-
|
|
22
|
-
expect(events).toHaveLength(2);
|
|
23
|
-
expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
|
|
24
|
-
expect(events[1]).toEqual({ type: 'text', content: ' world' });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('returns clean content', () => {
|
|
28
|
-
const decoder = new PassthroughDecoder(() => {});
|
|
29
|
-
decoder.push('Hello ');
|
|
30
|
-
decoder.push('world');
|
|
31
|
-
decoder.flush();
|
|
32
|
-
expect(decoder.getCleanContent()).toBe('Hello world');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('returns no reasoning', () => {
|
|
36
|
-
const decoder = new PassthroughDecoder(() => {});
|
|
37
|
-
decoder.push('test');
|
|
38
|
-
expect(decoder.getReasoning()).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('StandardChatDecoder', () => {
|
|
43
|
-
it('emits text events', () => {
|
|
44
|
-
const events: DecodedEvent[] = [];
|
|
45
|
-
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
46
|
-
|
|
47
|
-
decoder.push('Hello');
|
|
48
|
-
expect(events).toHaveLength(1);
|
|
49
|
-
expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('emits reasoning events', () => {
|
|
53
|
-
const events: DecodedEvent[] = [];
|
|
54
|
-
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
55
|
-
|
|
56
|
-
decoder.pushReasoning('Thinking...');
|
|
57
|
-
expect(events).toHaveLength(1);
|
|
58
|
-
expect(events[0]).toEqual({ type: 'thinking', content: 'Thinking...' });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('emits tool call events', () => {
|
|
62
|
-
const events: DecodedEvent[] = [];
|
|
63
|
-
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
64
|
-
|
|
65
|
-
const calls = [{
|
|
66
|
-
id: 'call_1',
|
|
67
|
-
type: 'function' as const,
|
|
68
|
-
function: { name: 'get_time', arguments: '{}' },
|
|
69
|
-
}];
|
|
70
|
-
decoder.pushToolCalls(calls);
|
|
71
|
-
|
|
72
|
-
expect(events).toHaveLength(1);
|
|
73
|
-
expect(events[0]!.type).toBe('tool_call');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('tracks both content and reasoning', () => {
|
|
77
|
-
const decoder = new StandardChatDecoder(() => {});
|
|
78
|
-
decoder.push('Text');
|
|
79
|
-
decoder.pushReasoning('Reason');
|
|
80
|
-
decoder.flush();
|
|
81
|
-
|
|
82
|
-
expect(decoder.getCleanContent()).toBe('Text');
|
|
83
|
-
expect(decoder.getReasoning()).toBe('Reason');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
expect(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
decoder.
|
|
109
|
-
|
|
110
|
-
decoder.
|
|
111
|
-
decoder.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
decoder.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const decoder =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Tests for stream-decoder.ts — Pluggable reasoning strategies
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'bun:test';
|
|
5
|
+
import {
|
|
6
|
+
PassthroughDecoder,
|
|
7
|
+
StandardChatDecoder,
|
|
8
|
+
InterleavedReasoningDecoder,
|
|
9
|
+
createDecoder,
|
|
10
|
+
type DecodedEvent,
|
|
11
|
+
} from '../stream-decoder.js';
|
|
12
|
+
|
|
13
|
+
describe('PassthroughDecoder', () => {
|
|
14
|
+
it('emits all tokens as text events', () => {
|
|
15
|
+
const events: DecodedEvent[] = [];
|
|
16
|
+
const decoder = new PassthroughDecoder(e => events.push(e));
|
|
17
|
+
|
|
18
|
+
decoder.push('Hello');
|
|
19
|
+
decoder.push(' world');
|
|
20
|
+
decoder.flush();
|
|
21
|
+
|
|
22
|
+
expect(events).toHaveLength(2);
|
|
23
|
+
expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
|
|
24
|
+
expect(events[1]).toEqual({ type: 'text', content: ' world' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns clean content', () => {
|
|
28
|
+
const decoder = new PassthroughDecoder(() => {});
|
|
29
|
+
decoder.push('Hello ');
|
|
30
|
+
decoder.push('world');
|
|
31
|
+
decoder.flush();
|
|
32
|
+
expect(decoder.getCleanContent()).toBe('Hello world');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns no reasoning', () => {
|
|
36
|
+
const decoder = new PassthroughDecoder(() => {});
|
|
37
|
+
decoder.push('test');
|
|
38
|
+
expect(decoder.getReasoning()).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('StandardChatDecoder', () => {
|
|
43
|
+
it('emits text events', () => {
|
|
44
|
+
const events: DecodedEvent[] = [];
|
|
45
|
+
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
46
|
+
|
|
47
|
+
decoder.push('Hello');
|
|
48
|
+
expect(events).toHaveLength(1);
|
|
49
|
+
expect(events[0]).toEqual({ type: 'text', content: 'Hello' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('emits reasoning events', () => {
|
|
53
|
+
const events: DecodedEvent[] = [];
|
|
54
|
+
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
55
|
+
|
|
56
|
+
decoder.pushReasoning('Thinking...');
|
|
57
|
+
expect(events).toHaveLength(1);
|
|
58
|
+
expect(events[0]).toEqual({ type: 'thinking', content: 'Thinking...' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('emits tool call events', () => {
|
|
62
|
+
const events: DecodedEvent[] = [];
|
|
63
|
+
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
64
|
+
|
|
65
|
+
const calls = [{
|
|
66
|
+
id: 'call_1',
|
|
67
|
+
type: 'function' as const,
|
|
68
|
+
function: { name: 'get_time', arguments: '{}' },
|
|
69
|
+
}];
|
|
70
|
+
decoder.pushToolCalls(calls);
|
|
71
|
+
|
|
72
|
+
expect(events).toHaveLength(1);
|
|
73
|
+
expect(events[0]!.type).toBe('tool_call');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('tracks both content and reasoning', () => {
|
|
77
|
+
const decoder = new StandardChatDecoder(() => {});
|
|
78
|
+
decoder.push('Text');
|
|
79
|
+
decoder.pushReasoning('Reason');
|
|
80
|
+
decoder.flush();
|
|
81
|
+
|
|
82
|
+
expect(decoder.getCleanContent()).toBe('Text');
|
|
83
|
+
expect(decoder.getReasoning()).toBe('Reason');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('parses Gemma thought channel into reasoning', () => {
|
|
87
|
+
const events: DecodedEvent[] = [];
|
|
88
|
+
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
89
|
+
|
|
90
|
+
decoder.push('<|channel>thought\nNeed Portuguese.<channel|>Olá!');
|
|
91
|
+
decoder.flush();
|
|
92
|
+
|
|
93
|
+
expect(events.filter(e => e.type === 'thinking')).toEqual([
|
|
94
|
+
{ type: 'thinking', content: 'Need Portuguese.' },
|
|
95
|
+
]);
|
|
96
|
+
expect(events.filter(e => e.type === 'text').map(e => e.content).join('')).toBe('Olá!');
|
|
97
|
+
expect(decoder.getCleanContent()).toBe('Olá!');
|
|
98
|
+
expect(decoder.getReasoning()).toBe('Need Portuguese.');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('parses Gemma thought channel split across chunks', () => {
|
|
102
|
+
const decoder = new StandardChatDecoder(() => {});
|
|
103
|
+
|
|
104
|
+
decoder.push('<|chan');
|
|
105
|
+
decoder.push('nel>thought\nNeed');
|
|
106
|
+
decoder.push(' Portuguese.<chan');
|
|
107
|
+
decoder.push('nel|>Olá!');
|
|
108
|
+
decoder.flush();
|
|
109
|
+
|
|
110
|
+
expect(decoder.getCleanContent()).toBe('Olá!');
|
|
111
|
+
expect(decoder.getReasoning()).toBe('Need Portuguese.');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('strips compact empty Gemma thought marker', () => {
|
|
115
|
+
const decoder = new StandardChatDecoder(() => {});
|
|
116
|
+
|
|
117
|
+
decoder.push('<|thought\n|>Olá!');
|
|
118
|
+
decoder.flush();
|
|
119
|
+
|
|
120
|
+
expect(decoder.getCleanContent()).toBe('Olá!');
|
|
121
|
+
expect(decoder.getReasoning()).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('strips tool_call and tool_response tags', () => {
|
|
125
|
+
const events: DecodedEvent[] = [];
|
|
126
|
+
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
127
|
+
|
|
128
|
+
decoder.push('<tool_call|><|tool_response>');
|
|
129
|
+
decoder.flush();
|
|
130
|
+
|
|
131
|
+
expect(events.filter(e => e.type === 'tool_call')).toHaveLength(0);
|
|
132
|
+
expect(decoder.getCleanContent()).toBe('');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('parses tool_call containing JSON content', () => {
|
|
136
|
+
const events: DecodedEvent[] = [];
|
|
137
|
+
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
138
|
+
|
|
139
|
+
decoder.push("<tool_call|>{'name': 'get_weather', 'arguments': {'city': 'Tokyo'}}<|tool_response>");
|
|
140
|
+
decoder.flush();
|
|
141
|
+
|
|
142
|
+
const toolCalls = events.filter(e => e.type === 'tool_call');
|
|
143
|
+
expect(toolCalls).toHaveLength(1);
|
|
144
|
+
expect(toolCalls[0]).toEqual({
|
|
145
|
+
type: 'tool_call',
|
|
146
|
+
calls: [
|
|
147
|
+
{
|
|
148
|
+
id: expect.any(String),
|
|
149
|
+
type: 'function',
|
|
150
|
+
function: {
|
|
151
|
+
name: 'get_weather',
|
|
152
|
+
arguments: JSON.stringify({ city: 'Tokyo' }),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
expect(decoder.getCleanContent()).toBe('');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('strips stray tool_response tags', () => {
|
|
161
|
+
const events: DecodedEvent[] = [];
|
|
162
|
+
const decoder = new StandardChatDecoder(e => events.push(e));
|
|
163
|
+
|
|
164
|
+
decoder.push('Hello<|tool_response> World');
|
|
165
|
+
decoder.flush();
|
|
166
|
+
|
|
167
|
+
expect(decoder.getCleanContent()).toBe('Hello World');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('InterleavedReasoningDecoder', () => {
|
|
172
|
+
it('extracts think tags from text', () => {
|
|
173
|
+
const events: DecodedEvent[] = [];
|
|
174
|
+
const decoder = new InterleavedReasoningDecoder(e => events.push(e));
|
|
175
|
+
|
|
176
|
+
decoder.push('<think>I should analyze this</think>The answer is 42');
|
|
177
|
+
decoder.flush();
|
|
178
|
+
|
|
179
|
+
const thinkEvents = events.filter(e => e.type === 'thinking');
|
|
180
|
+
const textEvents = events.filter(e => e.type === 'text');
|
|
181
|
+
|
|
182
|
+
expect(thinkEvents.length).toBeGreaterThan(0);
|
|
183
|
+
expect(textEvents.length).toBeGreaterThan(0);
|
|
184
|
+
expect(decoder.getReasoning()).toBe('I should analyze this');
|
|
185
|
+
expect(decoder.getCleanContent()).toBe('The answer is 42');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('handles think tags split across chunks', () => {
|
|
189
|
+
const events: DecodedEvent[] = [];
|
|
190
|
+
const decoder = new InterleavedReasoningDecoder(e => events.push(e));
|
|
191
|
+
|
|
192
|
+
decoder.push('<thi');
|
|
193
|
+
decoder.push('nk>Split reasoning</th');
|
|
194
|
+
decoder.push('ink>Done');
|
|
195
|
+
decoder.flush();
|
|
196
|
+
|
|
197
|
+
expect(decoder.getReasoning()).toBe('Split reasoning');
|
|
198
|
+
expect(decoder.getCleanContent()).toBe('Done');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('handles progress tags', () => {
|
|
202
|
+
const events: DecodedEvent[] = [];
|
|
203
|
+
const decoder = new InterleavedReasoningDecoder(e => events.push(e));
|
|
204
|
+
|
|
205
|
+
decoder.push('<progress>Loading data</progress>Complete');
|
|
206
|
+
decoder.flush();
|
|
207
|
+
|
|
208
|
+
const progressEvents = events.filter(e => e.type === 'progress');
|
|
209
|
+
expect(progressEvents.length).toBeGreaterThan(0);
|
|
210
|
+
expect(decoder.getCleanContent()).toBe('Complete');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('handles plain text with no tags', () => {
|
|
214
|
+
const events: DecodedEvent[] = [];
|
|
215
|
+
const decoder = new InterleavedReasoningDecoder(e => events.push(e));
|
|
216
|
+
|
|
217
|
+
decoder.push('Just plain text');
|
|
218
|
+
decoder.flush();
|
|
219
|
+
|
|
220
|
+
expect(decoder.getCleanContent()).toBe('Just plain text');
|
|
221
|
+
expect(decoder.getReasoning()).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('handles empty reasoning', () => {
|
|
225
|
+
const decoder = new InterleavedReasoningDecoder(() => {});
|
|
226
|
+
decoder.push('<think></think>Content');
|
|
227
|
+
decoder.flush();
|
|
228
|
+
|
|
229
|
+
expect(decoder.getCleanContent()).toBe('Content');
|
|
230
|
+
expect(decoder.getReasoning()).toBeUndefined();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('handles multiple think blocks', () => {
|
|
234
|
+
const decoder = new InterleavedReasoningDecoder(() => {});
|
|
235
|
+
|
|
236
|
+
decoder.push('<think>First thought</think>Text<think>Second thought</think>More text');
|
|
237
|
+
decoder.flush();
|
|
238
|
+
|
|
239
|
+
expect(decoder.getReasoning()).toBe('First thoughtSecond thought');
|
|
240
|
+
expect(decoder.getCleanContent()).toBe('TextMore text');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('createDecoder', () => {
|
|
245
|
+
it('creates passthrough decoder', () => {
|
|
246
|
+
const decoder = createDecoder('passthrough', () => {});
|
|
247
|
+
expect(decoder).toBeInstanceOf(PassthroughDecoder);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('creates standard-chat decoder', () => {
|
|
251
|
+
const decoder = createDecoder('standard-chat', () => {});
|
|
252
|
+
expect(decoder).toBeInstanceOf(StandardChatDecoder);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('creates interleaved-reasoning decoder', () => {
|
|
256
|
+
const decoder = createDecoder('interleaved-reasoning', () => {});
|
|
257
|
+
expect(decoder).toBeInstanceOf(InterleavedReasoningDecoder);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('throws for unknown type', () => {
|
|
261
|
+
expect(() => createDecoder('unknown' as never, () => {})).toThrow('Unknown decoder type');
|
|
262
|
+
});
|
|
263
|
+
});
|