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,200 +1,200 @@
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
+ /**
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 +1,117 @@
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
- });
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
+ });