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.
Files changed (108) hide show
  1. package/CHANGELOG.md +142 -103
  2. package/LICENSE +21 -21
  3. package/README.md +640 -591
  4. package/dist/ai-model.d.ts +12 -1
  5. package/dist/ai-model.d.ts.map +1 -1
  6. package/dist/ai-model.js +36 -1
  7. package/dist/ai-model.js.map +1 -1
  8. package/dist/gemma-channel.d.ts +14 -0
  9. package/dist/gemma-channel.d.ts.map +1 -0
  10. package/dist/gemma-channel.js +38 -0
  11. package/dist/gemma-channel.js.map +1 -0
  12. package/dist/gemma-diffusion.d.ts +49 -0
  13. package/dist/gemma-diffusion.d.ts.map +1 -0
  14. package/dist/gemma-diffusion.js +147 -0
  15. package/dist/gemma-diffusion.js.map +1 -0
  16. package/dist/http.d.ts +4 -0
  17. package/dist/http.d.ts.map +1 -1
  18. package/dist/http.js +14 -1
  19. package/dist/http.js.map +1 -1
  20. package/dist/index.d.ts +2 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/interfaces.d.ts +183 -7
  25. package/dist/interfaces.d.ts.map +1 -1
  26. package/dist/interfaces.js.map +1 -1
  27. package/dist/providers/anthropic.d.ts.map +1 -1
  28. package/dist/providers/anthropic.js +28 -3
  29. package/dist/providers/anthropic.js.map +1 -1
  30. package/dist/providers/google.d.ts +22 -1
  31. package/dist/providers/google.d.ts.map +1 -1
  32. package/dist/providers/google.js +225 -13
  33. package/dist/providers/google.js.map +1 -1
  34. package/dist/providers/ollama.d.ts +2 -0
  35. package/dist/providers/ollama.d.ts.map +1 -1
  36. package/dist/providers/ollama.js +59 -30
  37. package/dist/providers/ollama.js.map +1 -1
  38. package/dist/providers/openai.d.ts +14 -0
  39. package/dist/providers/openai.d.ts.map +1 -1
  40. package/dist/providers/openai.js +200 -22
  41. package/dist/providers/openai.js.map +1 -1
  42. package/dist/router.d.ts +2 -0
  43. package/dist/router.d.ts.map +1 -1
  44. package/dist/router.js +4 -0
  45. package/dist/router.js.map +1 -1
  46. package/dist/stream-decoder.d.ts +12 -0
  47. package/dist/stream-decoder.d.ts.map +1 -1
  48. package/dist/stream-decoder.js +182 -5
  49. package/dist/stream-decoder.js.map +1 -1
  50. package/dist/thinking.d.ts +36 -0
  51. package/dist/thinking.d.ts.map +1 -0
  52. package/dist/thinking.js +52 -0
  53. package/dist/thinking.js.map +1 -0
  54. package/package.json +118 -116
  55. package/src/ai-model.ts +400 -350
  56. package/src/auditor.ts +213 -213
  57. package/src/client.ts +402 -402
  58. package/src/debug/debug-google-streaming.ts +1 -1
  59. package/src/demos/basic/universal-llm-examples.ts +3 -3
  60. package/src/demos/diffusion-gemma/.env +29 -0
  61. package/src/demos/diffusion-gemma/.env.example +27 -0
  62. package/src/demos/diffusion-gemma/CLAUDE.md +95 -0
  63. package/src/demos/diffusion-gemma/README.md +59 -0
  64. package/src/demos/diffusion-gemma/canvas.ts +1606 -0
  65. package/src/demos/diffusion-gemma/docker-compose.yml +29 -0
  66. package/src/demos/diffusion-gemma/probe-stream.ts +51 -0
  67. package/src/demos/diffusion-gemma/probe-tools.ts +55 -0
  68. package/src/demos/diffusion-gemma/server.ts +1205 -0
  69. package/src/demos/diffusion-gemma/start-vllm.sh +98 -0
  70. package/src/gemma-channel.ts +47 -0
  71. package/src/gemma-diffusion.ts +167 -0
  72. package/src/http.ts +261 -247
  73. package/src/index.ts +180 -161
  74. package/src/interfaces.ts +843 -657
  75. package/src/mcp.ts +345 -345
  76. package/src/providers/anthropic.ts +796 -762
  77. package/src/providers/google.ts +840 -620
  78. package/src/providers/index.ts +8 -8
  79. package/src/providers/ollama.ts +503 -469
  80. package/src/providers/openai.ts +587 -392
  81. package/src/router.ts +785 -780
  82. package/src/stream-decoder.ts +535 -361
  83. package/src/structured-output.ts +759 -759
  84. package/src/test-scripts/test-google-deep-research.ts +33 -0
  85. package/src/test-scripts/test-google-streaming-enhanced.ts +147 -147
  86. package/src/test-scripts/test-google-streaming.ts +1 -1
  87. package/src/test-scripts/test-google-system-prompt-comprehensive.ts +189 -189
  88. package/src/test-scripts/test-google-thinking.ts +46 -0
  89. package/src/test-scripts/test-system-message-positions.ts +163 -163
  90. package/src/test-scripts/test-system-prompt-improvement-demo.ts +83 -83
  91. package/src/test-scripts/test-vllm-qwen36.ts +256 -0
  92. package/src/tests/ai-model.test.ts +1614 -1614
  93. package/src/tests/auditor.test.ts +224 -224
  94. package/src/tests/gemma-diffusion.test.ts +115 -0
  95. package/src/tests/http.test.ts +200 -200
  96. package/src/tests/interfaces.test.ts +117 -117
  97. package/src/tests/providers/anthropic.test.ts +118 -0
  98. package/src/tests/providers/google.test.ts +841 -660
  99. package/src/tests/providers/ollama.test.ts +1034 -954
  100. package/src/tests/providers/openai.test.ts +1511 -1122
  101. package/src/tests/router.test.ts +254 -254
  102. package/src/tests/stream-decoder.test.ts +263 -179
  103. package/src/tests/structured-output.test.ts +1450 -1450
  104. package/src/tests/thinking.test.ts +65 -0
  105. package/src/tests/tools.test.ts +175 -175
  106. package/src/thinking.ts +73 -0
  107. package/src/tools.ts +246 -246
  108. package/src/zod-adapter.ts +72 -72
@@ -1,224 +1,224 @@
1
- /**
2
- * Tests for auditor.ts — Observability implementations
3
- */
4
- import { describe, it, expect } from 'bun:test';
5
- import { NoopAuditor, ConsoleAuditor, BufferedAuditor, type AuditEvent } from '../auditor.js';
6
-
7
- describe('NoopAuditor', () => {
8
- it('accepts events without error', () => {
9
- const auditor = new NoopAuditor();
10
- expect(() => {
11
- auditor.record({
12
- timestamp: Date.now(),
13
- type: 'request',
14
- provider: 'test',
15
- });
16
- }).not.toThrow();
17
- });
18
-
19
- it('accepts structured output events without error', () => {
20
- const auditor = new NoopAuditor();
21
- expect(() => {
22
- auditor.record({
23
- timestamp: Date.now(),
24
- type: 'structured_request',
25
- provider: 'test',
26
- schemaName: 'User',
27
- });
28
- }).not.toThrow();
29
- });
30
- });
31
-
32
- describe('ConsoleAuditor', () => {
33
- it('creates with default prefix', () => {
34
- const auditor = new ConsoleAuditor();
35
- expect(auditor).toBeDefined();
36
- });
37
-
38
- it('creates with custom prefix', () => {
39
- const auditor = new ConsoleAuditor('[TEST]');
40
- expect(auditor).toBeDefined();
41
- });
42
-
43
- it('records all event types without error', () => {
44
- const auditor = new ConsoleAuditor('[TEST]');
45
- const types: AuditEvent['type'][] = [
46
- 'request', 'response', 'stream_start', 'stream_end',
47
- 'tool_call', 'tool_result', 'error', 'retry', 'failover',
48
- 'structured_request', 'structured_response', 'structured_validation_error',
49
- ];
50
-
51
- for (const type of types) {
52
- expect(() => {
53
- auditor.record({
54
- timestamp: Date.now(),
55
- type,
56
- provider: 'test',
57
- model: 'test-model',
58
- duration: 100,
59
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
60
- error: type === 'error' || type === 'structured_validation_error' ? 'test error' : undefined,
61
- metadata: type === 'failover' ? { nextProvider: 'backup' } : undefined,
62
- schemaName: type.startsWith('structured') ? 'User' : undefined,
63
- rawOutput: type === 'structured_validation_error' ? '{"name": "invalid"}' : undefined,
64
- });
65
- }).not.toThrow();
66
- }
67
- });
68
-
69
- it('logs structured_request with schema name', () => {
70
- const auditor = new ConsoleAuditor('[TEST]');
71
- // Should not throw when logging structured request
72
- expect(() => {
73
- auditor.record({
74
- timestamp: Date.now(),
75
- type: 'structured_request',
76
- provider: 'ollama',
77
- schemaName: 'User',
78
- });
79
- }).not.toThrow();
80
- });
81
-
82
- it('logs structured_response with duration and schema name', () => {
83
- const auditor = new ConsoleAuditor('[TEST]');
84
- expect(() => {
85
- auditor.record({
86
- timestamp: Date.now(),
87
- type: 'structured_response',
88
- provider: 'google',
89
- model: 'gemini-2.0-flash',
90
- duration: 150,
91
- schemaName: 'User',
92
- usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
93
- });
94
- }).not.toThrow();
95
- });
96
-
97
- it('logs structured_validation_error with schema name and error', () => {
98
- const auditor = new ConsoleAuditor('[TEST]');
99
- expect(() => {
100
- auditor.record({
101
- timestamp: Date.now(),
102
- type: 'structured_validation_error',
103
- provider: 'openai',
104
- schemaName: 'User',
105
- error: 'Validation failed: name is required',
106
- rawOutput: '{"age": 30}',
107
- });
108
- }).not.toThrow();
109
- });
110
- });
111
-
112
- describe('BufferedAuditor', () => {
113
- it('buffers events', () => {
114
- const auditor = new BufferedAuditor();
115
- const event: AuditEvent = {
116
- timestamp: Date.now(),
117
- type: 'request',
118
- provider: 'test',
119
- };
120
-
121
- auditor.record(event);
122
- auditor.record(event);
123
- auditor.record(event);
124
-
125
- expect(auditor.getEvents()).toHaveLength(3);
126
- });
127
-
128
- it('buffers structured output events', () => {
129
- const auditor = new BufferedAuditor();
130
-
131
- auditor.record({
132
- timestamp: Date.now(),
133
- type: 'structured_request',
134
- provider: 'ollama',
135
- schemaName: 'User',
136
- });
137
- auditor.record({
138
- timestamp: Date.now(),
139
- type: 'structured_response',
140
- provider: 'ollama',
141
- schemaName: 'User',
142
- duration: 100,
143
- });
144
-
145
- const events = auditor.getEvents();
146
- expect(events).toHaveLength(2);
147
- expect(events[0]?.type).toBe('structured_request');
148
- expect(events[0]?.schemaName).toBe('User');
149
- expect(events[1]?.type).toBe('structured_response');
150
- });
151
-
152
- it('buffers structured_validation_error events', () => {
153
- const auditor = new BufferedAuditor();
154
-
155
- auditor.record({
156
- timestamp: Date.now(),
157
- type: 'structured_validation_error',
158
- provider: 'openai',
159
- schemaName: 'User',
160
- error: 'Validation failed',
161
- rawOutput: '{"invalid": true}',
162
- });
163
-
164
- const events = auditor.getEvents();
165
- expect(events).toHaveLength(1);
166
- expect(events[0]?.type).toBe('structured_validation_error');
167
- expect(events[0]?.schemaName).toBe('User');
168
- expect(events[0]?.error).toBe('Validation failed');
169
- expect(events[0]?.rawOutput).toBe('{"invalid": true}');
170
- });
171
-
172
- it('flushes events to callback', async () => {
173
- const flushed: AuditEvent[][] = [];
174
- const auditor = new BufferedAuditor({
175
- onFlush: async (events) => {
176
- flushed.push([...events]);
177
- },
178
- });
179
-
180
- auditor.record({ timestamp: 1, type: 'request' });
181
- auditor.record({ timestamp: 2, type: 'response' });
182
-
183
- await auditor.flush();
184
-
185
- expect(flushed).toHaveLength(1);
186
- expect(flushed[0]).toHaveLength(2);
187
- expect(auditor.getEvents()).toHaveLength(0);
188
- });
189
-
190
- it('auto-flushes when buffer is full', async () => {
191
- let flushCount = 0;
192
- const auditor = new BufferedAuditor({
193
- maxBufferSize: 3,
194
- onFlush: async () => { flushCount++; },
195
- });
196
-
197
- auditor.record({ timestamp: 1, type: 'request' });
198
- auditor.record({ timestamp: 2, type: 'request' });
199
- auditor.record({ timestamp: 3, type: 'request' }); // triggers auto-flush
200
-
201
- // Give auto-flush time to complete
202
- await new Promise(r => setTimeout(r, 50));
203
- expect(flushCount).toBe(1);
204
- });
205
-
206
- it('clears without flushing', () => {
207
- const auditor = new BufferedAuditor();
208
- auditor.record({ timestamp: 1, type: 'request' });
209
- auditor.record({ timestamp: 2, type: 'response' });
210
-
211
- auditor.clear();
212
- expect(auditor.getEvents()).toHaveLength(0);
213
- });
214
-
215
- it('flush with no events is a no-op', async () => {
216
- let flushed = false;
217
- const auditor = new BufferedAuditor({
218
- onFlush: async () => { flushed = true; },
219
- });
220
-
221
- await auditor.flush();
222
- expect(flushed).toBe(false);
223
- });
224
- });
1
+ /**
2
+ * Tests for auditor.ts — Observability implementations
3
+ */
4
+ import { describe, it, expect } from 'bun:test';
5
+ import { NoopAuditor, ConsoleAuditor, BufferedAuditor, type AuditEvent } from '../auditor.js';
6
+
7
+ describe('NoopAuditor', () => {
8
+ it('accepts events without error', () => {
9
+ const auditor = new NoopAuditor();
10
+ expect(() => {
11
+ auditor.record({
12
+ timestamp: Date.now(),
13
+ type: 'request',
14
+ provider: 'test',
15
+ });
16
+ }).not.toThrow();
17
+ });
18
+
19
+ it('accepts structured output events without error', () => {
20
+ const auditor = new NoopAuditor();
21
+ expect(() => {
22
+ auditor.record({
23
+ timestamp: Date.now(),
24
+ type: 'structured_request',
25
+ provider: 'test',
26
+ schemaName: 'User',
27
+ });
28
+ }).not.toThrow();
29
+ });
30
+ });
31
+
32
+ describe('ConsoleAuditor', () => {
33
+ it('creates with default prefix', () => {
34
+ const auditor = new ConsoleAuditor();
35
+ expect(auditor).toBeDefined();
36
+ });
37
+
38
+ it('creates with custom prefix', () => {
39
+ const auditor = new ConsoleAuditor('[TEST]');
40
+ expect(auditor).toBeDefined();
41
+ });
42
+
43
+ it('records all event types without error', () => {
44
+ const auditor = new ConsoleAuditor('[TEST]');
45
+ const types: AuditEvent['type'][] = [
46
+ 'request', 'response', 'stream_start', 'stream_end',
47
+ 'tool_call', 'tool_result', 'error', 'retry', 'failover',
48
+ 'structured_request', 'structured_response', 'structured_validation_error',
49
+ ];
50
+
51
+ for (const type of types) {
52
+ expect(() => {
53
+ auditor.record({
54
+ timestamp: Date.now(),
55
+ type,
56
+ provider: 'test',
57
+ model: 'test-model',
58
+ duration: 100,
59
+ usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
60
+ error: type === 'error' || type === 'structured_validation_error' ? 'test error' : undefined,
61
+ metadata: type === 'failover' ? { nextProvider: 'backup' } : undefined,
62
+ schemaName: type.startsWith('structured') ? 'User' : undefined,
63
+ rawOutput: type === 'structured_validation_error' ? '{"name": "invalid"}' : undefined,
64
+ });
65
+ }).not.toThrow();
66
+ }
67
+ });
68
+
69
+ it('logs structured_request with schema name', () => {
70
+ const auditor = new ConsoleAuditor('[TEST]');
71
+ // Should not throw when logging structured request
72
+ expect(() => {
73
+ auditor.record({
74
+ timestamp: Date.now(),
75
+ type: 'structured_request',
76
+ provider: 'ollama',
77
+ schemaName: 'User',
78
+ });
79
+ }).not.toThrow();
80
+ });
81
+
82
+ it('logs structured_response with duration and schema name', () => {
83
+ const auditor = new ConsoleAuditor('[TEST]');
84
+ expect(() => {
85
+ auditor.record({
86
+ timestamp: Date.now(),
87
+ type: 'structured_response',
88
+ provider: 'google',
89
+ model: 'gemini-2.0-flash',
90
+ duration: 150,
91
+ schemaName: 'User',
92
+ usage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
93
+ });
94
+ }).not.toThrow();
95
+ });
96
+
97
+ it('logs structured_validation_error with schema name and error', () => {
98
+ const auditor = new ConsoleAuditor('[TEST]');
99
+ expect(() => {
100
+ auditor.record({
101
+ timestamp: Date.now(),
102
+ type: 'structured_validation_error',
103
+ provider: 'openai',
104
+ schemaName: 'User',
105
+ error: 'Validation failed: name is required',
106
+ rawOutput: '{"age": 30}',
107
+ });
108
+ }).not.toThrow();
109
+ });
110
+ });
111
+
112
+ describe('BufferedAuditor', () => {
113
+ it('buffers events', () => {
114
+ const auditor = new BufferedAuditor();
115
+ const event: AuditEvent = {
116
+ timestamp: Date.now(),
117
+ type: 'request',
118
+ provider: 'test',
119
+ };
120
+
121
+ auditor.record(event);
122
+ auditor.record(event);
123
+ auditor.record(event);
124
+
125
+ expect(auditor.getEvents()).toHaveLength(3);
126
+ });
127
+
128
+ it('buffers structured output events', () => {
129
+ const auditor = new BufferedAuditor();
130
+
131
+ auditor.record({
132
+ timestamp: Date.now(),
133
+ type: 'structured_request',
134
+ provider: 'ollama',
135
+ schemaName: 'User',
136
+ });
137
+ auditor.record({
138
+ timestamp: Date.now(),
139
+ type: 'structured_response',
140
+ provider: 'ollama',
141
+ schemaName: 'User',
142
+ duration: 100,
143
+ });
144
+
145
+ const events = auditor.getEvents();
146
+ expect(events).toHaveLength(2);
147
+ expect(events[0]?.type).toBe('structured_request');
148
+ expect(events[0]?.schemaName).toBe('User');
149
+ expect(events[1]?.type).toBe('structured_response');
150
+ });
151
+
152
+ it('buffers structured_validation_error events', () => {
153
+ const auditor = new BufferedAuditor();
154
+
155
+ auditor.record({
156
+ timestamp: Date.now(),
157
+ type: 'structured_validation_error',
158
+ provider: 'openai',
159
+ schemaName: 'User',
160
+ error: 'Validation failed',
161
+ rawOutput: '{"invalid": true}',
162
+ });
163
+
164
+ const events = auditor.getEvents();
165
+ expect(events).toHaveLength(1);
166
+ expect(events[0]?.type).toBe('structured_validation_error');
167
+ expect(events[0]?.schemaName).toBe('User');
168
+ expect(events[0]?.error).toBe('Validation failed');
169
+ expect(events[0]?.rawOutput).toBe('{"invalid": true}');
170
+ });
171
+
172
+ it('flushes events to callback', async () => {
173
+ const flushed: AuditEvent[][] = [];
174
+ const auditor = new BufferedAuditor({
175
+ onFlush: async (events) => {
176
+ flushed.push([...events]);
177
+ },
178
+ });
179
+
180
+ auditor.record({ timestamp: 1, type: 'request' });
181
+ auditor.record({ timestamp: 2, type: 'response' });
182
+
183
+ await auditor.flush();
184
+
185
+ expect(flushed).toHaveLength(1);
186
+ expect(flushed[0]).toHaveLength(2);
187
+ expect(auditor.getEvents()).toHaveLength(0);
188
+ });
189
+
190
+ it('auto-flushes when buffer is full', async () => {
191
+ let flushCount = 0;
192
+ const auditor = new BufferedAuditor({
193
+ maxBufferSize: 3,
194
+ onFlush: async () => { flushCount++; },
195
+ });
196
+
197
+ auditor.record({ timestamp: 1, type: 'request' });
198
+ auditor.record({ timestamp: 2, type: 'request' });
199
+ auditor.record({ timestamp: 3, type: 'request' }); // triggers auto-flush
200
+
201
+ // Give auto-flush time to complete
202
+ await new Promise(r => setTimeout(r, 50));
203
+ expect(flushCount).toBe(1);
204
+ });
205
+
206
+ it('clears without flushing', () => {
207
+ const auditor = new BufferedAuditor();
208
+ auditor.record({ timestamp: 1, type: 'request' });
209
+ auditor.record({ timestamp: 2, type: 'response' });
210
+
211
+ auditor.clear();
212
+ expect(auditor.getEvents()).toHaveLength(0);
213
+ });
214
+
215
+ it('flush with no events is a no-op', async () => {
216
+ let flushed = false;
217
+ const auditor = new BufferedAuditor({
218
+ onFlush: async () => { flushed = true; },
219
+ });
220
+
221
+ await auditor.flush();
222
+ expect(flushed).toBe(false);
223
+ });
224
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Tests for the DiffusionGemma native-protocol adapter.
3
+ * Wire-format samples are taken verbatim from live vLLM responses
4
+ * (skip_special_tokens: false) captured June 2026.
5
+ */
6
+
7
+ import { describe, expect, it } from 'bun:test';
8
+ import {
9
+ gemmaArgsToJson,
10
+ isGemmaDiffusionModel,
11
+ parseGemmaDiffusionOutput,
12
+ } from '../gemma-diffusion.js';
13
+
14
+ describe('isGemmaDiffusionModel', () => {
15
+ it('detects the served model id', () => {
16
+ expect(isGemmaDiffusionModel('RedHatAI/diffusiongemma-26B-A4B-it-NVFP4')).toBe(true);
17
+ expect(isGemmaDiffusionModel('nvidia/DiffusionGemma-26B')).toBe(true);
18
+ expect(isGemmaDiffusionModel('diffusion_gemma-mini')).toBe(true);
19
+ });
20
+
21
+ it('ignores other models', () => {
22
+ expect(isGemmaDiffusionModel('gemma-4-27b-it')).toBe(false);
23
+ expect(isGemmaDiffusionModel('gpt-4o')).toBe(false);
24
+ expect(isGemmaDiffusionModel('stable-diffusion-xl')).toBe(false);
25
+ });
26
+ });
27
+
28
+ describe('gemmaArgsToJson', () => {
29
+ it('converts quoted strings and bare keys', () => {
30
+ const json = gemmaArgsToJson('city:<|"|>Paris<|"|>,unit:<|"|>celsius<|"|>');
31
+ expect(JSON.parse(json)).toEqual({ city: 'Paris', unit: 'celsius' });
32
+ });
33
+
34
+ it('handles numbers, booleans and null', () => {
35
+ const json = gemmaArgsToJson('count:3,ratio:-2.5e2,on:true,off:false,nothing:null');
36
+ expect(JSON.parse(json)).toEqual({ count: 3, ratio: -250, on: true, off: false, nothing: null });
37
+ });
38
+
39
+ it('preserves JSON-hostile characters inside quoted strings', () => {
40
+ const json = gemmaArgsToJson('q:<|"|>a, b: {c} [d] "e"<|"|>');
41
+ expect(JSON.parse(json)).toEqual({ q: 'a, b: {c} [d] "e"' });
42
+ });
43
+
44
+ it('handles nested objects and arrays', () => {
45
+ const json = gemmaArgsToJson('filter:{city:<|"|>Rio<|"|>,limit:2},tags:[<|"|>a<|"|>,<|"|>b<|"|>,1]');
46
+ expect(JSON.parse(json)).toEqual({ filter: { city: 'Rio', limit: 2 }, tags: ['a', 'b', 1] });
47
+ });
48
+
49
+ it('treats unquoted bare words as strings (model deviation)', () => {
50
+ const json = gemmaArgsToJson('unit:celsius,city:Paris');
51
+ expect(JSON.parse(json)).toEqual({ unit: 'celsius', city: 'Paris' });
52
+ });
53
+
54
+ it('handles empty arguments', () => {
55
+ expect(JSON.parse(gemmaArgsToJson(''))).toEqual({});
56
+ });
57
+ });
58
+
59
+ describe('parseGemmaDiffusionOutput', () => {
60
+ it('parses the live tool-call wire format', () => {
61
+ const raw =
62
+ '<|channel>thought\nThe user is asking for the weather. I should use the `get_weather` tool.<channel|>' +
63
+ '<|tool_call>call:get_weather{city:<|"|>Paris<|"|>,unit:<|"|>celsius<|"|>}<tool_call|>';
64
+ const parsed = parseGemmaDiffusionOutput(raw);
65
+ expect(parsed.reasoning).toContain('get_weather');
66
+ expect(parsed.content).toBe('');
67
+ expect(parsed.toolCalls).toHaveLength(1);
68
+ expect(parsed.toolCalls[0]!.name).toBe('get_weather');
69
+ expect(JSON.parse(parsed.toolCalls[0]!.argumentsJson)).toEqual({ city: 'Paris', unit: 'celsius' });
70
+ });
71
+
72
+ it('separates reasoning from answer (the live 42 sample)', () => {
73
+ const raw =
74
+ '<|channel>thought\n* Question: What is 17 + 25?\n * 30 + 12 = 42\n\n * The answer is 42.<channel|>42';
75
+ const parsed = parseGemmaDiffusionOutput(raw);
76
+ expect(parsed.content).toBe('42');
77
+ expect(parsed.reasoning).toContain('30 + 12 = 42');
78
+ expect(parsed.toolCalls).toHaveLength(0);
79
+ });
80
+
81
+ it('handles answer text after a tool result turn (no thought)', () => {
82
+ const parsed = parseGemmaDiffusionOutput('The current weather in Paris is 18°C and partly cloudy.');
83
+ expect(parsed.content).toBe('The current weather in Paris is 18°C and partly cloudy.');
84
+ expect(parsed.reasoning).toBe('');
85
+ expect(parsed.toolCalls).toHaveLength(0);
86
+ });
87
+
88
+ it('captures a dangling unterminated thought channel (max_tokens cutoff)', () => {
89
+ const parsed = parseGemmaDiffusionOutput('<|channel>thought\nStill reasoning about');
90
+ expect(parsed.content).toBe('');
91
+ expect(parsed.reasoning).toBe('Still reasoning about');
92
+ });
93
+
94
+ it('parses multiple tool calls in one turn', () => {
95
+ const raw =
96
+ '<|tool_call>call:get_weather{city:<|"|>Paris<|"|>}<tool_call|>' +
97
+ '<|tool_call>call:get_weather{city:<|"|>London<|"|>}<tool_call|>';
98
+ const parsed = parseGemmaDiffusionOutput(raw);
99
+ expect(parsed.toolCalls.map(t => JSON.parse(t.argumentsJson)['city'])).toEqual(['Paris', 'London']);
100
+ });
101
+
102
+ it('strips residual control tokens from text', () => {
103
+ const parsed = parseGemmaDiffusionOutput('Hello there.<turn|>');
104
+ expect(parsed.content).toBe('Hello there.');
105
+ });
106
+
107
+ it('strips stray unbalanced channel markers (live run 1 sample)', () => {
108
+ const raw =
109
+ '<|channel>thought\nThe current weather is 18C.<channel|>' +
110
+ '<channel|>The weather in Paris is currently 18°C and partly cloudy.';
111
+ const parsed = parseGemmaDiffusionOutput(raw);
112
+ expect(parsed.content).toBe('The weather in Paris is currently 18°C and partly cloudy.');
113
+ expect(parsed.reasoning).toBe('The current weather is 18C.');
114
+ });
115
+ });