@ank1015/providers 0.0.1 → 0.0.3
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/LICENSE +21 -0
- package/README.md +93 -383
- package/dist/agent/conversation.d.ts +97 -0
- package/dist/agent/conversation.d.ts.map +1 -0
- package/dist/agent/conversation.js +328 -0
- package/dist/agent/conversation.js.map +1 -0
- package/dist/agent/runner.d.ts +37 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +169 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/tools/calculate.d.ts +15 -0
- package/dist/agent/tools/calculate.d.ts.map +1 -0
- package/dist/agent/tools/calculate.js +23 -0
- package/dist/agent/tools/calculate.js.map +1 -0
- package/dist/agent/tools/get-current-time.d.ts +15 -0
- package/dist/agent/tools/get-current-time.d.ts.map +1 -0
- package/dist/agent/tools/get-current-time.js +38 -0
- package/dist/agent/tools/get-current-time.js.map +1 -0
- package/dist/agent/tools/index.d.ts +3 -0
- package/dist/agent/tools/index.d.ts.map +1 -0
- package/dist/agent/tools/index.js +3 -0
- package/dist/agent/tools/index.js.map +1 -0
- package/dist/agent/types.d.ts +53 -31
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js +1 -2
- package/dist/agent/utils.d.ts +14 -0
- package/dist/agent/utils.d.ts.map +1 -0
- package/dist/agent/utils.js +59 -0
- package/dist/agent/utils.js.map +1 -0
- package/dist/index.d.ts +16 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -28
- package/dist/index.js.map +1 -1
- package/dist/llm.d.ts +15 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +92 -0
- package/dist/llm.js.map +1 -0
- package/dist/models.d.ts +8 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +25 -112
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +72 -227
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +30 -32
- package/dist/models.js.map +1 -1
- package/dist/providers/google/complete.d.ts +3 -0
- package/dist/providers/google/complete.d.ts.map +1 -0
- package/dist/providers/google/complete.js +53 -0
- package/dist/providers/google/complete.js.map +1 -0
- package/dist/providers/google/index.d.ts +6 -0
- package/dist/providers/google/index.d.ts.map +1 -0
- package/dist/providers/google/index.js +6 -0
- package/dist/providers/google/index.js.map +1 -0
- package/dist/providers/google/stream.d.ts +3 -0
- package/dist/providers/google/stream.d.ts.map +1 -0
- package/dist/providers/{google.js → google/stream.js} +67 -231
- package/dist/providers/google/stream.js.map +1 -0
- package/dist/providers/google/types.d.ts +8 -0
- package/dist/providers/google/types.d.ts.map +1 -0
- package/dist/providers/google/types.js +2 -0
- package/dist/providers/google/types.js.map +1 -0
- package/dist/providers/google/utils.d.ts +30 -0
- package/dist/providers/google/utils.d.ts.map +1 -0
- package/dist/providers/google/utils.js +354 -0
- package/dist/providers/google/utils.js.map +1 -0
- package/dist/providers/openai/complete.d.ts +3 -0
- package/dist/providers/openai/complete.d.ts.map +1 -0
- package/dist/providers/openai/complete.js +57 -0
- package/dist/providers/openai/complete.js.map +1 -0
- package/dist/providers/openai/index.d.ts +4 -0
- package/dist/providers/openai/index.d.ts.map +1 -0
- package/dist/providers/openai/index.js +4 -0
- package/dist/providers/openai/index.js.map +1 -0
- package/dist/providers/openai/stream.d.ts +3 -0
- package/dist/providers/openai/stream.d.ts.map +1 -0
- package/dist/providers/{openai.js → openai/stream.js} +74 -152
- package/dist/providers/openai/stream.js.map +1 -0
- package/dist/providers/openai/types.d.ts +8 -0
- package/dist/providers/openai/types.d.ts.map +1 -0
- package/dist/providers/openai/types.js +2 -0
- package/dist/providers/openai/types.js.map +1 -0
- package/dist/providers/openai/utils.d.ts +13 -0
- package/dist/providers/openai/utils.d.ts.map +1 -0
- package/dist/providers/openai/utils.js +285 -0
- package/dist/providers/openai/utils.js.map +1 -0
- package/dist/types.d.ts +95 -87
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -9
- package/dist/types.js.map +1 -1
- package/dist/utils/event-stream.d.ts +2 -2
- package/dist/utils/event-stream.d.ts.map +1 -1
- package/dist/utils/event-stream.js +2 -7
- package/dist/utils/event-stream.js.map +1 -1
- package/dist/utils/json-parse.js +3 -6
- package/dist/utils/json-parse.js.map +1 -1
- package/dist/utils/overflow.d.ts +51 -0
- package/dist/utils/overflow.d.ts.map +1 -0
- package/dist/utils/overflow.js +106 -0
- package/dist/utils/overflow.js.map +1 -0
- package/dist/utils/sanitize-unicode.js +1 -4
- package/dist/utils/sanitize-unicode.js.map +1 -1
- package/dist/utils/uuid.d.ts +6 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/dist/utils/uuid.js +9 -0
- package/dist/utils/uuid.js.map +1 -0
- package/dist/utils/validation.d.ts +10 -3
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +20 -12
- package/dist/utils/validation.js.map +1 -1
- package/package.json +47 -8
- package/biome.json +0 -43
- package/dist/agent/agent-loop.d.ts +0 -5
- package/dist/agent/agent-loop.d.ts.map +0 -1
- package/dist/agent/agent-loop.js +0 -219
- package/dist/agent/agent-loop.js.map +0 -1
- package/dist/providers/convert.d.ts +0 -6
- package/dist/providers/convert.d.ts.map +0 -1
- package/dist/providers/convert.js +0 -207
- package/dist/providers/convert.js.map +0 -1
- package/dist/providers/google.d.ts +0 -26
- package/dist/providers/google.d.ts.map +0 -1
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/openai.d.ts +0 -17
- package/dist/providers/openai.d.ts.map +0 -1
- package/dist/providers/openai.js.map +0 -1
- package/dist/stream.d.ts +0 -4
- package/dist/stream.d.ts.map +0 -1
- package/dist/stream.js +0 -40
- package/dist/stream.js.map +0 -1
- package/dist/test-google-agent-loop.d.ts +0 -2
- package/dist/test-google-agent-loop.d.ts.map +0 -1
- package/dist/test-google-agent-loop.js +0 -186
- package/dist/test-google-agent-loop.js.map +0 -1
- package/dist/test-google.d.ts +0 -2
- package/dist/test-google.d.ts.map +0 -1
- package/dist/test-google.js +0 -41
- package/dist/test-google.js.map +0 -1
- package/src/agent/agent-loop.ts +0 -275
- package/src/agent/types.ts +0 -80
- package/src/index.ts +0 -72
- package/src/models.generated.ts +0 -314
- package/src/models.ts +0 -45
- package/src/providers/convert.ts +0 -222
- package/src/providers/google.ts +0 -496
- package/src/providers/openai.ts +0 -437
- package/src/stream.ts +0 -60
- package/src/types.ts +0 -198
- package/src/utils/event-stream.ts +0 -60
- package/src/utils/json-parse.ts +0 -28
- package/src/utils/sanitize-unicode.ts +0 -25
- package/src/utils/validation.ts +0 -69
- package/test/core/agent-loop.test.ts +0 -958
- package/test/core/stream.test.ts +0 -409
- package/test/data/red-circle.png +0 -0
- package/test/data/superintelligentwill.pdf +0 -0
- package/test/edge-cases/general.test.ts +0 -565
- package/test/integration/e2e.test.ts +0 -530
- package/test/models/cost.test.ts +0 -499
- package/test/models/registry.test.ts +0 -298
- package/test/providers/convert.test.ts +0 -846
- package/test/providers/google-schema.test.ts +0 -666
- package/test/providers/google-stream.test.ts +0 -369
- package/test/providers/openai-stream.test.ts +0 -251
- package/test/utils/event-stream.test.ts +0 -289
- package/test/utils/json-parse.test.ts +0 -344
- package/test/utils/sanitize-unicode.test.ts +0 -329
- package/test/utils/validation.test.ts +0 -614
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -9
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
-
import { Type } from '@sinclair/typebox';
|
|
3
|
-
import { stream } from '../../src/stream';
|
|
4
|
-
import { MODELS } from '../../src/models.generated';
|
|
5
|
-
import { Context } from '../../src/types';
|
|
6
|
-
import * as fs from 'fs';
|
|
7
|
-
import * as path from 'path';
|
|
8
|
-
|
|
9
|
-
// These tests will only run if API keys are set in environment
|
|
10
|
-
// Set ENABLE_E2E_TESTS=true to run these tests
|
|
11
|
-
// Set OPENAI_API_KEY and/or GEMINI_API_KEY to test specific providers
|
|
12
|
-
|
|
13
|
-
const E2E_ENABLED = process.env.ENABLE_E2E_TESTS === 'true';
|
|
14
|
-
const HAS_OPENAI_KEY = Boolean(process.env.OPENAI_API_KEY);
|
|
15
|
-
const HAS_GEMINI_KEY = Boolean(process.env.GEMINI_API_KEY);
|
|
16
|
-
|
|
17
|
-
const describeE2E = E2E_ENABLED ? describe : describe.skip;
|
|
18
|
-
|
|
19
|
-
describeE2E('E2E Integration Tests', () => {
|
|
20
|
-
beforeAll(() => {
|
|
21
|
-
if (!E2E_ENABLED) {
|
|
22
|
-
console.log('Skipping E2E tests (set ENABLE_E2E_TESTS=true to run)');
|
|
23
|
-
}
|
|
24
|
-
if (!HAS_OPENAI_KEY && !HAS_GEMINI_KEY) {
|
|
25
|
-
console.log('No API keys found. Set OPENAI_API_KEY or GEMINI_API_KEY to test.');
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const describeOpenAI = HAS_OPENAI_KEY ? describe : describe.skip;
|
|
30
|
-
const describeGoogle = HAS_GEMINI_KEY ? describe : describe.skip;
|
|
31
|
-
|
|
32
|
-
describeOpenAI('OpenAI Integration', () => {
|
|
33
|
-
it('should stream a simple text response', async () => {
|
|
34
|
-
const context: Context = {
|
|
35
|
-
systemPrompt: 'You are a helpful assistant. Be very concise.',
|
|
36
|
-
messages: [
|
|
37
|
-
{
|
|
38
|
-
role: 'user',
|
|
39
|
-
content: [{ type: 'text', content: 'Say "Hello Test" and nothing else.' }],
|
|
40
|
-
timestamp: Date.now(),
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const response = stream(MODELS.openai['gpt-5-mini'], context);
|
|
46
|
-
|
|
47
|
-
let textReceived = '';
|
|
48
|
-
let eventCount = 0;
|
|
49
|
-
|
|
50
|
-
for await (const event of response) {
|
|
51
|
-
eventCount++;
|
|
52
|
-
|
|
53
|
-
if (event.type === 'text_delta') {
|
|
54
|
-
textReceived += event.delta;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (event.type === 'done') {
|
|
58
|
-
expect(event.reason).toBe('stop');
|
|
59
|
-
expect(event.message.content.length).toBeGreaterThan(0);
|
|
60
|
-
expect(event.message.usage.totalTokens).toBeGreaterThan(0);
|
|
61
|
-
expect(event.message.usage.cost.total).toBeGreaterThan(0);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
expect(eventCount).toBeGreaterThan(0);
|
|
66
|
-
expect(textReceived.length).toBeGreaterThan(0);
|
|
67
|
-
}, 30000); // 30 second timeout
|
|
68
|
-
|
|
69
|
-
it('should handle tool calls', async () => {
|
|
70
|
-
const context: Context = {
|
|
71
|
-
systemPrompt: 'You are a helpful assistant. Use the calculator tool when asked to calculate.',
|
|
72
|
-
messages: [
|
|
73
|
-
{
|
|
74
|
-
role: 'user',
|
|
75
|
-
content: [{ type: 'text', content: 'Calculate 15 * 23' }],
|
|
76
|
-
timestamp: Date.now(),
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
tools: [
|
|
80
|
-
{
|
|
81
|
-
name: 'calculator',
|
|
82
|
-
description: 'Perform mathematical calculations',
|
|
83
|
-
parameters: Type.Object({
|
|
84
|
-
expression: Type.String({ description: 'Mathematical expression' }),
|
|
85
|
-
}),
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const response = stream(MODELS.openai['gpt-5-mini'], context);
|
|
91
|
-
|
|
92
|
-
let toolCallFound = false;
|
|
93
|
-
|
|
94
|
-
for await (const event of response) {
|
|
95
|
-
if (event.type === 'toolcall_end') {
|
|
96
|
-
toolCallFound = true;
|
|
97
|
-
expect(event.toolCall.name).toBe('calculator');
|
|
98
|
-
expect(event.toolCall.arguments).toBeDefined();
|
|
99
|
-
expect(typeof event.toolCall.arguments).toBe('object');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (event.type === 'done') {
|
|
103
|
-
expect(event.reason).toBe('toolUse');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
expect(toolCallFound).toBe(true);
|
|
108
|
-
}, 30000);
|
|
109
|
-
|
|
110
|
-
it('should handle reasoning mode (if supported)', async () => {
|
|
111
|
-
const context: Context = {
|
|
112
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
113
|
-
messages: [
|
|
114
|
-
{
|
|
115
|
-
role: 'user',
|
|
116
|
-
content: [{ type: 'text', content: 'Explain why the sky is blue in one sentence.' }],
|
|
117
|
-
timestamp: Date.now(),
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const response = stream(MODELS.openai['gpt-5-mini'], context, {
|
|
123
|
-
reasoning: {
|
|
124
|
-
effort: 'low',
|
|
125
|
-
summary: 'concise'
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
let hasThinking = false;
|
|
130
|
-
|
|
131
|
-
for await (const event of response) {
|
|
132
|
-
if (event.type === 'thinking_start' || event.type === 'thinking_delta') {
|
|
133
|
-
hasThinking = true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (event.type === 'done') {
|
|
137
|
-
expect(event.message.stopReason).toBe('stop');
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Reasoning mode may or may not produce thinking depending on the query
|
|
142
|
-
// Just verify the stream completes successfully
|
|
143
|
-
expect(true).toBe(true);
|
|
144
|
-
}, 30000);
|
|
145
|
-
|
|
146
|
-
it('should calculate costs correctly', async () => {
|
|
147
|
-
const context: Context = {
|
|
148
|
-
messages: [
|
|
149
|
-
{
|
|
150
|
-
role: 'user',
|
|
151
|
-
content: [{ type: 'text', content: 'Hello' }],
|
|
152
|
-
timestamp: Date.now(),
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const response = stream(MODELS.openai['gpt-5-mini'], context);
|
|
158
|
-
|
|
159
|
-
for await (const event of response) {
|
|
160
|
-
if (event.type === 'done') {
|
|
161
|
-
const { usage } = event.message;
|
|
162
|
-
|
|
163
|
-
expect(usage.input).toBeGreaterThan(0);
|
|
164
|
-
expect(usage.output).toBeGreaterThan(0);
|
|
165
|
-
expect(usage.totalTokens).toBe(usage.input + usage.output + usage.cacheRead + usage.cacheWrite);
|
|
166
|
-
|
|
167
|
-
expect(usage.cost.input).toBeGreaterThan(0);
|
|
168
|
-
expect(usage.cost.output).toBeGreaterThan(0);
|
|
169
|
-
expect(usage.cost.total).toBeGreaterThan(0);
|
|
170
|
-
|
|
171
|
-
// Total cost should equal sum of parts
|
|
172
|
-
const calculatedTotal =
|
|
173
|
-
usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
174
|
-
expect(usage.cost.total).toBeCloseTo(calculatedTotal, 10);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}, 30000);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
describeGoogle('Google Integration', () => {
|
|
181
|
-
it('should stream a simple text response', async () => {
|
|
182
|
-
const context: Context = {
|
|
183
|
-
systemPrompt: 'You are a helpful assistant. Be very concise.',
|
|
184
|
-
messages: [
|
|
185
|
-
{
|
|
186
|
-
role: 'user',
|
|
187
|
-
content: [{ type: 'text', content: 'Say "Hello Test" and nothing else.' }],
|
|
188
|
-
timestamp: Date.now(),
|
|
189
|
-
},
|
|
190
|
-
],
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const response = stream(MODELS.google['gemini-2.5-flash'], context);
|
|
194
|
-
|
|
195
|
-
let textReceived = '';
|
|
196
|
-
let eventCount = 0;
|
|
197
|
-
|
|
198
|
-
for await (const event of response) {
|
|
199
|
-
eventCount++;
|
|
200
|
-
|
|
201
|
-
if (event.type === 'text_delta') {
|
|
202
|
-
textReceived += event.delta;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (event.type === 'done') {
|
|
206
|
-
expect(event.reason).toBe('stop');
|
|
207
|
-
expect(event.message.content.length).toBeGreaterThan(0);
|
|
208
|
-
expect(event.message.usage.totalTokens).toBeGreaterThan(0);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
expect(eventCount).toBeGreaterThan(0);
|
|
213
|
-
expect(textReceived.length).toBeGreaterThan(0);
|
|
214
|
-
}, 30000);
|
|
215
|
-
|
|
216
|
-
it('should handle tool calls', async () => {
|
|
217
|
-
const context: Context = {
|
|
218
|
-
systemPrompt: 'You are a helpful assistant. Use the calculator tool when asked to calculate.',
|
|
219
|
-
messages: [
|
|
220
|
-
{
|
|
221
|
-
role: 'user',
|
|
222
|
-
content: [{ type: 'text', content: 'Calculate 25 * 17' }],
|
|
223
|
-
timestamp: Date.now(),
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
tools: [
|
|
227
|
-
{
|
|
228
|
-
name: 'calculator',
|
|
229
|
-
description: 'Perform mathematical calculations',
|
|
230
|
-
parameters: Type.Object({
|
|
231
|
-
expression: Type.String({ description: 'Mathematical expression' }),
|
|
232
|
-
}),
|
|
233
|
-
},
|
|
234
|
-
],
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const response = stream(MODELS.google['gemini-2.5-flash'], context);
|
|
238
|
-
|
|
239
|
-
let toolCallFound = false;
|
|
240
|
-
|
|
241
|
-
for await (const event of response) {
|
|
242
|
-
if (event.type === 'toolcall_end') {
|
|
243
|
-
toolCallFound = true;
|
|
244
|
-
expect(event.toolCall.name).toBe('calculator');
|
|
245
|
-
expect(event.toolCall.arguments).toBeDefined();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (event.type === 'done') {
|
|
249
|
-
expect(event.reason).toBe('toolUse');
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
expect(toolCallFound).toBe(true);
|
|
254
|
-
}, 30000);
|
|
255
|
-
|
|
256
|
-
it('should calculate costs correctly', async () => {
|
|
257
|
-
const context: Context = {
|
|
258
|
-
messages: [
|
|
259
|
-
{
|
|
260
|
-
role: 'user',
|
|
261
|
-
content: [{ type: 'text', content: 'Hello' }],
|
|
262
|
-
timestamp: Date.now(),
|
|
263
|
-
},
|
|
264
|
-
],
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
const response = stream(MODELS.google['gemini-2.5-flash'], context);
|
|
268
|
-
|
|
269
|
-
for await (const event of response) {
|
|
270
|
-
if (event.type === 'done') {
|
|
271
|
-
const { usage } = event.message;
|
|
272
|
-
|
|
273
|
-
expect(usage.input).toBeGreaterThan(0);
|
|
274
|
-
expect(usage.output).toBeGreaterThan(0);
|
|
275
|
-
expect(usage.totalTokens).toBeGreaterThan(0);
|
|
276
|
-
|
|
277
|
-
// Costs should be calculated
|
|
278
|
-
expect(usage.cost.total).toBeGreaterThanOrEqual(0);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}, 30000);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// Multimodal Tests (Image and File Inputs)
|
|
285
|
-
describeOpenAI('OpenAI Multimodal Integration', () => {
|
|
286
|
-
it('should handle image input (vision)', async () => {
|
|
287
|
-
const imagePath = path.join(__dirname, '../data/red-circle.png');
|
|
288
|
-
const imageBuffer = fs.readFileSync(imagePath);
|
|
289
|
-
const base64Image = imageBuffer.toString('base64');
|
|
290
|
-
|
|
291
|
-
const context: Context = {
|
|
292
|
-
systemPrompt: 'You are a helpful assistant that can analyze images.',
|
|
293
|
-
messages: [
|
|
294
|
-
{
|
|
295
|
-
role: 'user',
|
|
296
|
-
content: [
|
|
297
|
-
{ type: 'text', content: 'What color is the main shape in this image? Answer in one word.' },
|
|
298
|
-
{ type: 'image', data: base64Image, mimeType: 'image/png' },
|
|
299
|
-
],
|
|
300
|
-
timestamp: Date.now(),
|
|
301
|
-
},
|
|
302
|
-
],
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
const response = stream(MODELS.openai['gpt-5-mini'], context);
|
|
306
|
-
|
|
307
|
-
let textReceived = '';
|
|
308
|
-
let hasImage = false;
|
|
309
|
-
|
|
310
|
-
for await (const event of response) {
|
|
311
|
-
if (event.type === 'text_delta') {
|
|
312
|
-
textReceived += event.delta;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (event.type === 'done') {
|
|
316
|
-
expect(event.reason).toBe('stop');
|
|
317
|
-
expect(event.message.content.length).toBeGreaterThan(0);
|
|
318
|
-
// Check if the response mentions red (case insensitive)
|
|
319
|
-
expect(textReceived.toLowerCase()).toContain('red');
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
expect(textReceived.length).toBeGreaterThan(0);
|
|
324
|
-
}, 30000);
|
|
325
|
-
|
|
326
|
-
it('should handle mixed content (text + image)', async () => {
|
|
327
|
-
const imagePath = path.join(__dirname, '../data/red-circle.png');
|
|
328
|
-
const imageBuffer = fs.readFileSync(imagePath);
|
|
329
|
-
const base64Image = imageBuffer.toString('base64');
|
|
330
|
-
|
|
331
|
-
const context: Context = {
|
|
332
|
-
messages: [
|
|
333
|
-
{
|
|
334
|
-
role: 'user',
|
|
335
|
-
content: [
|
|
336
|
-
{ type: 'text', content: 'Describe this image briefly:' },
|
|
337
|
-
{ type: 'image', data: base64Image, mimeType: 'image/png' },
|
|
338
|
-
{ type: 'text', content: 'Be very concise.' },
|
|
339
|
-
],
|
|
340
|
-
timestamp: Date.now(),
|
|
341
|
-
},
|
|
342
|
-
],
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const response = stream(MODELS.openai['gpt-5-mini'], context);
|
|
346
|
-
|
|
347
|
-
let completed = false;
|
|
348
|
-
|
|
349
|
-
for await (const event of response) {
|
|
350
|
-
if (event.type === 'done') {
|
|
351
|
-
completed = true;
|
|
352
|
-
expect(event.reason).toBe('stop');
|
|
353
|
-
expect(event.message.content.length).toBeGreaterThan(0);
|
|
354
|
-
expect(event.message.usage.totalTokens).toBeGreaterThan(0);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
expect(completed).toBe(true);
|
|
359
|
-
}, 30000);
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
describeGoogle('Google Multimodal Integration', () => {
|
|
363
|
-
it('should handle image input', async () => {
|
|
364
|
-
const imagePath = path.join(__dirname, '../data/red-circle.png');
|
|
365
|
-
const imageBuffer = fs.readFileSync(imagePath);
|
|
366
|
-
const base64Image = imageBuffer.toString('base64');
|
|
367
|
-
|
|
368
|
-
const context: Context = {
|
|
369
|
-
systemPrompt: 'You are a helpful assistant that can analyze images.',
|
|
370
|
-
messages: [
|
|
371
|
-
{
|
|
372
|
-
role: 'user',
|
|
373
|
-
content: [
|
|
374
|
-
{ type: 'text', content: 'What color is the main shape in this image? Answer in one word.' },
|
|
375
|
-
{ type: 'image', data: base64Image, mimeType: 'image/png' },
|
|
376
|
-
],
|
|
377
|
-
timestamp: Date.now(),
|
|
378
|
-
},
|
|
379
|
-
],
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
const response = stream(MODELS.google['gemini-2.5-flash'], context);
|
|
383
|
-
|
|
384
|
-
let textReceived = '';
|
|
385
|
-
|
|
386
|
-
for await (const event of response) {
|
|
387
|
-
if (event.type === 'text_delta') {
|
|
388
|
-
textReceived += event.delta;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (event.type === 'done') {
|
|
392
|
-
expect(event.reason).toBe('stop');
|
|
393
|
-
expect(event.message.content.length).toBeGreaterThan(0);
|
|
394
|
-
// Check if the response mentions red (case insensitive)
|
|
395
|
-
expect(textReceived.toLowerCase()).toContain('red');
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
expect(textReceived.length).toBeGreaterThan(0);
|
|
400
|
-
}, 30000);
|
|
401
|
-
|
|
402
|
-
it('should handle PDF file input', async () => {
|
|
403
|
-
const pdfPath = path.join(__dirname, '../data/superintelligentwill.pdf');
|
|
404
|
-
const pdfBuffer = fs.readFileSync(pdfPath);
|
|
405
|
-
const base64Pdf = pdfBuffer.toString('base64');
|
|
406
|
-
|
|
407
|
-
const context: Context = {
|
|
408
|
-
systemPrompt: 'You are a helpful assistant that can read documents.',
|
|
409
|
-
messages: [
|
|
410
|
-
{
|
|
411
|
-
role: 'user',
|
|
412
|
-
content: [
|
|
413
|
-
{ type: 'text', content: 'What type of document is this? Answer in 3 words or less.' },
|
|
414
|
-
{ type: 'file', data: base64Pdf, mimeType: 'application/pdf' },
|
|
415
|
-
],
|
|
416
|
-
timestamp: Date.now(),
|
|
417
|
-
},
|
|
418
|
-
],
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const response = stream(MODELS.google['gemini-2.5-flash'], context);
|
|
422
|
-
|
|
423
|
-
let textReceived = '';
|
|
424
|
-
|
|
425
|
-
for await (const event of response) {
|
|
426
|
-
if (event.type === 'text_delta') {
|
|
427
|
-
textReceived += event.delta;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (event.type === 'done') {
|
|
431
|
-
expect(event.reason).toBe('stop');
|
|
432
|
-
expect(event.message.content.length).toBeGreaterThan(0);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
expect(textReceived.length).toBeGreaterThan(0);
|
|
437
|
-
}, 30000);
|
|
438
|
-
|
|
439
|
-
it('should handle mixed content (text + image + file)', async () => {
|
|
440
|
-
const imagePath = path.join(__dirname, '../data/red-circle.png');
|
|
441
|
-
const imageBuffer = fs.readFileSync(imagePath);
|
|
442
|
-
const base64Image = imageBuffer.toString('base64');
|
|
443
|
-
|
|
444
|
-
const context: Context = {
|
|
445
|
-
messages: [
|
|
446
|
-
{
|
|
447
|
-
role: 'user',
|
|
448
|
-
content: [
|
|
449
|
-
{ type: 'text', content: 'Describe this image briefly:' },
|
|
450
|
-
{ type: 'image', data: base64Image, mimeType: 'image/png' },
|
|
451
|
-
],
|
|
452
|
-
timestamp: Date.now(),
|
|
453
|
-
},
|
|
454
|
-
],
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const response = stream(MODELS.google['gemini-2.5-flash'], context);
|
|
458
|
-
|
|
459
|
-
let completed = false;
|
|
460
|
-
|
|
461
|
-
for await (const event of response) {
|
|
462
|
-
if (event.type === 'done') {
|
|
463
|
-
completed = true;
|
|
464
|
-
expect(event.reason).toBe('stop');
|
|
465
|
-
expect(event.message.content.length).toBeGreaterThan(0);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
expect(completed).toBe(true);
|
|
470
|
-
}, 30000);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
describe('Cross-provider consistency', () => {
|
|
474
|
-
const itWithBothProviders = HAS_OPENAI_KEY && HAS_GEMINI_KEY ? it : it.skip;
|
|
475
|
-
|
|
476
|
-
itWithBothProviders('should produce similar event structure across providers', async () => {
|
|
477
|
-
const context: Context = {
|
|
478
|
-
messages: [
|
|
479
|
-
{
|
|
480
|
-
role: 'user',
|
|
481
|
-
content: [{ type: 'text', content: 'Hello' }],
|
|
482
|
-
timestamp: Date.now(),
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
const openaiResponse = stream(MODELS.openai['gpt-5-mini'], context);
|
|
488
|
-
const googleResponse = stream(MODELS.google['gemini-2.5-flash'], context);
|
|
489
|
-
|
|
490
|
-
const openaiEvents: string[] = [];
|
|
491
|
-
const googleEvents: string[] = [];
|
|
492
|
-
|
|
493
|
-
for await (const event of openaiResponse) {
|
|
494
|
-
openaiEvents.push(event.type);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
for await (const event of googleResponse) {
|
|
498
|
-
googleEvents.push(event.type);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Both should have start and done events
|
|
502
|
-
expect(openaiEvents).toContain('start');
|
|
503
|
-
expect(openaiEvents).toContain('done');
|
|
504
|
-
expect(googleEvents).toContain('start');
|
|
505
|
-
expect(googleEvents).toContain('done');
|
|
506
|
-
|
|
507
|
-
// Both should have text events
|
|
508
|
-
expect(openaiEvents.some(e => e.startsWith('text_'))).toBe(true);
|
|
509
|
-
expect(googleEvents.some(e => e.startsWith('text_'))).toBe(true);
|
|
510
|
-
}, 60000);
|
|
511
|
-
});
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
// Non-E2E integration tests (always run, use mocks)
|
|
515
|
-
describe('Integration Tests (Mocked)', () => {
|
|
516
|
-
it('should have at least one test model for each provider', () => {
|
|
517
|
-
const allModels = Object.values(MODELS).flatMap(provider => Object.values(provider));
|
|
518
|
-
const openaiModels = allModels.filter((m: any) => m.api === 'openai');
|
|
519
|
-
const googleModels = allModels.filter((m: any) => m.api === 'google');
|
|
520
|
-
|
|
521
|
-
expect(openaiModels.length).toBeGreaterThan(0);
|
|
522
|
-
expect(googleModels.length).toBeGreaterThan(0);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it('should have valid API keys environment setup', () => {
|
|
526
|
-
// Just verify the env vars exist (may be empty)
|
|
527
|
-
expect(process.env).toHaveProperty('OPENAI_API_KEY');
|
|
528
|
-
expect(process.env).toHaveProperty('GEMINI_API_KEY');
|
|
529
|
-
});
|
|
530
|
-
});
|