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,224 +0,0 @@
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,115 +0,0 @@
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
- });
@@ -1,200 +0,0 @@
1
- /**
2
- * Tests for http.ts — Universal HTTP utilities
3
- */
4
- import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test';
5
- import { httpRequest, parseNDJSON, parseSSE, buildHeaders } from '../http.js';
6
- import { AIModelApiType } from '../interfaces.js';
7
-
8
- describe('httpRequest', () => {
9
- const originalFetch = globalThis.fetch;
10
-
11
- afterEach(() => {
12
- globalThis.fetch = originalFetch;
13
- });
14
-
15
- it('makes a GET request and parses JSON', async () => {
16
- globalThis.fetch = mock(async () =>
17
- new Response(JSON.stringify({ ok: true }), {
18
- status: 200,
19
- headers: { 'Content-Type': 'application/json' },
20
- })
21
- ) as typeof fetch;
22
-
23
- const result = await httpRequest<{ ok: boolean }>('http://test.com/api');
24
- expect(result.ok).toBe(true);
25
- expect(result.status).toBe(200);
26
- expect(result.data.ok).toBe(true);
27
- });
28
-
29
- it('makes a POST request with body', async () => {
30
- let capturedBody: string | undefined;
31
- globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
32
- capturedBody = init?.body as string;
33
- return new Response(JSON.stringify({ received: true }), {
34
- status: 200,
35
- headers: { 'Content-Type': 'application/json' },
36
- });
37
- }) as typeof fetch;
38
-
39
- await httpRequest('http://test.com/api', {
40
- method: 'POST',
41
- body: { message: 'hello' },
42
- });
43
-
44
- expect(capturedBody).toBe(JSON.stringify({ message: 'hello' }));
45
- });
46
-
47
- it('throws on non-OK response', async () => {
48
- globalThis.fetch = mock(async () =>
49
- new Response('Unauthorized', { status: 401 })
50
- ) as typeof fetch;
51
-
52
- expect(httpRequest('http://test.com/api')).rejects.toThrow('HTTP 401');
53
- });
54
-
55
- it('includes custom headers', async () => {
56
- let capturedHeaders: HeadersInit | undefined;
57
- globalThis.fetch = mock(async (_url: string | URL | Request, init?: RequestInit) => {
58
- capturedHeaders = init?.headers;
59
- return new Response(JSON.stringify({}), { status: 200 });
60
- }) as typeof fetch;
61
-
62
- await httpRequest('http://test.com/api', {
63
- headers: { 'X-Custom': 'value' },
64
- });
65
-
66
- expect(capturedHeaders).toHaveProperty('X-Custom', 'value');
67
- });
68
- });
69
-
70
- describe('parseNDJSON', () => {
71
- it('parses newline-delimited JSON', async () => {
72
- async function* source(): AsyncGenerator<string> {
73
- yield '{"a":1}\n{"b":2}\n';
74
- }
75
-
76
- const results: Record<string, number>[] = [];
77
- for await (const item of parseNDJSON<Record<string, number>>(source())) {
78
- results.push(item);
79
- }
80
-
81
- expect(results).toHaveLength(2);
82
- expect(results[0]).toEqual({ a: 1 });
83
- expect(results[1]).toEqual({ b: 2 });
84
- });
85
-
86
- it('handles chunks split across JSON boundaries', async () => {
87
- async function* source(): AsyncGenerator<string> {
88
- yield '{"a":';
89
- yield '1}\n{"b":2';
90
- yield '}\n';
91
- }
92
-
93
- const results: unknown[] = [];
94
- for await (const item of parseNDJSON(source())) {
95
- results.push(item);
96
- }
97
-
98
- expect(results).toHaveLength(2);
99
- });
100
-
101
- it('skips empty lines', async () => {
102
- async function* source(): AsyncGenerator<string> {
103
- yield '{"a":1}\n\n\n{"b":2}\n';
104
- }
105
-
106
- const results: unknown[] = [];
107
- for await (const item of parseNDJSON(source())) {
108
- results.push(item);
109
- }
110
-
111
- expect(results).toHaveLength(2);
112
- });
113
-
114
- it('handles remaining buffer content', async () => {
115
- async function* source(): AsyncGenerator<string> {
116
- yield '{"a":1}';
117
- }
118
-
119
- const results: unknown[] = [];
120
- for await (const item of parseNDJSON(source())) {
121
- results.push(item);
122
- }
123
-
124
- expect(results).toHaveLength(1);
125
- });
126
- });
127
-
128
- describe('parseSSE', () => {
129
- it('parses server-sent events', async () => {
130
- async function* source(): AsyncGenerator<string> {
131
- yield 'data: {"content":"hello"}\n\ndata: {"content":"world"}\n\n';
132
- }
133
-
134
- const results: { event?: string; data: string }[] = [];
135
- for await (const event of parseSSE(source())) {
136
- results.push(event);
137
- }
138
-
139
- expect(results).toHaveLength(2);
140
- expect(results[0]!.data).toBe('{"content":"hello"}');
141
- expect(results[1]!.data).toBe('{"content":"world"}');
142
- });
143
-
144
- it('skips [DONE] events', async () => {
145
- async function* source(): AsyncGenerator<string> {
146
- yield 'data: {"content":"hello"}\n\ndata: [DONE]\n\n';
147
- }
148
-
149
- const results: unknown[] = [];
150
- for await (const event of parseSSE(source())) {
151
- results.push(event);
152
- }
153
-
154
- expect(results).toHaveLength(1);
155
- });
156
-
157
- it('extracts event type', async () => {
158
- async function* source(): AsyncGenerator<string> {
159
- yield 'event: custom\ndata: {"test":true}\n\n';
160
- }
161
-
162
- const results: { event?: string; data: string }[] = [];
163
- for await (const event of parseSSE(source())) {
164
- results.push(event);
165
- }
166
-
167
- expect(results).toHaveLength(1);
168
- expect(results[0]!.event).toBe('custom');
169
- });
170
- });
171
-
172
- describe('buildHeaders', () => {
173
- it('returns Content-Type header', () => {
174
- const headers = buildHeaders({
175
- model: 'test',
176
- url: 'http://test.com',
177
- apiType: AIModelApiType.Ollama,
178
- });
179
- expect(headers['Content-Type']).toBe('application/json');
180
- });
181
-
182
- it('adds Authorization header when apiKey is set', () => {
183
- const headers = buildHeaders({
184
- model: 'test',
185
- url: 'http://test.com',
186
- apiType: AIModelApiType.OpenAI,
187
- apiKey: 'sk-test',
188
- });
189
- expect(headers['Authorization']).toBe('Bearer sk-test');
190
- });
191
-
192
- it('omits Authorization when no apiKey', () => {
193
- const headers = buildHeaders({
194
- model: 'test',
195
- url: 'http://test.com',
196
- apiType: AIModelApiType.Ollama,
197
- });
198
- expect(headers['Authorization']).toBeUndefined();
199
- });
200
- });
@@ -1,117 +0,0 @@
1
- /**
2
- * Tests for interfaces.ts — Helper functions and type utilities
3
- */
4
- import { describe, it, expect } from 'bun:test';
5
- import {
6
- textContent,
7
- imageContent,
8
- multimodalMessage,
9
- extractTextContent,
10
- hasImages,
11
- AIModelApiType,
12
- AIModelType,
13
- } from '../interfaces.js';
14
-
15
- describe('Helper Functions', () => {
16
- describe('textContent', () => {
17
- it('creates a text content part', () => {
18
- const result = textContent('Hello');
19
- expect(result).toEqual({ type: 'text', text: 'Hello' });
20
- });
21
- });
22
-
23
- describe('imageContent', () => {
24
- it('creates an image content part from base64', () => {
25
- const result = imageContent('abc123');
26
- expect(result.type).toBe('image_url');
27
- expect(result.image_url.url).toBe('data:image/jpeg;base64,abc123');
28
- });
29
-
30
- it('creates an image content part from URL', () => {
31
- const result = imageContent('https://example.com/image.jpg');
32
- expect(result.image_url.url).toBe('https://example.com/image.jpg');
33
- });
34
-
35
- it('creates an image content part from data URI', () => {
36
- const result = imageContent('data:image/png;base64,abc');
37
- expect(result.image_url.url).toBe('data:image/png;base64,abc');
38
- });
39
-
40
- it('respects custom mimeType', () => {
41
- const result = imageContent('abc123', 'image/png');
42
- expect(result.image_url.url).toBe('data:image/png;base64,abc123');
43
- });
44
-
45
- it('includes detail parameter', () => {
46
- const result = imageContent('https://example.com/img.jpg', 'image/jpeg', 'low');
47
- expect(result.image_url.detail).toBe('low');
48
- });
49
- });
50
-
51
- describe('multimodalMessage', () => {
52
- it('creates a user message with text and images', () => {
53
- const msg = multimodalMessage('Describe this', ['base64img']);
54
- expect(msg.role).toBe('user');
55
- expect(Array.isArray(msg.content)).toBe(true);
56
- const parts = msg.content as Array<{ type: string }>;
57
- expect(parts).toHaveLength(2);
58
- expect(parts[0]!.type).toBe('text');
59
- expect(parts[1]!.type).toBe('image_url');
60
- });
61
-
62
- it('handles multiple images', () => {
63
- const msg = multimodalMessage('Compare', ['img1', 'img2', 'img3']);
64
- const parts = msg.content as Array<{ type: string }>;
65
- expect(parts).toHaveLength(4); // 1 text + 3 images
66
- });
67
- });
68
-
69
- describe('extractTextContent', () => {
70
- it('extracts text from string content', () => {
71
- expect(extractTextContent('Hello')).toBe('Hello');
72
- });
73
-
74
- it('extracts text from content parts array', () => {
75
- const content = [
76
- textContent('Hello'),
77
- imageContent('img'),
78
- textContent(' World'),
79
- ];
80
- expect(extractTextContent(content)).toBe('Hello World');
81
- });
82
-
83
- it('returns empty string from image-only content', () => {
84
- const content = [imageContent('img')];
85
- expect(extractTextContent(content)).toBe('');
86
- });
87
- });
88
-
89
- describe('hasImages', () => {
90
- it('returns false for string content', () => {
91
- expect(hasImages('Hello')).toBe(false);
92
- });
93
-
94
- it('returns false for text-only content parts', () => {
95
- expect(hasImages([textContent('Hello')])).toBe(false);
96
- });
97
-
98
- it('returns true for content with images', () => {
99
- expect(hasImages([textContent('Hello'), imageContent('img')])).toBe(true);
100
- });
101
- });
102
- });
103
-
104
- describe('Enums', () => {
105
- it('AIModelApiType has expected values', () => {
106
- expect(AIModelApiType.Ollama).toBe('ollama');
107
- expect(AIModelApiType.OpenAI).toBe('openai');
108
- expect(AIModelApiType.Google).toBe('google');
109
- expect(AIModelApiType.Vertex).toBe('vertex');
110
- expect(AIModelApiType.LlamaCpp).toBe('llamacpp');
111
- });
112
-
113
- it('AIModelType has expected values', () => {
114
- expect(AIModelType.Chat).toBe('chat');
115
- expect(AIModelType.Embedding).toBe('embedding');
116
- });
117
- });