@hazeljs/ai 0.2.0-beta.54 → 0.2.0-beta.56
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/README.md +1 -1
- package/dist/ai-enhanced.service.js +1 -1
- package/dist/ai-enhanced.service.test.d.ts +2 -0
- package/dist/ai-enhanced.service.test.d.ts.map +1 -0
- package/dist/ai-enhanced.service.test.js +501 -0
- package/dist/ai-enhanced.test.d.ts +2 -0
- package/dist/ai-enhanced.test.d.ts.map +1 -0
- package/dist/ai-enhanced.test.js +587 -0
- package/dist/ai.decorator.test.d.ts +2 -0
- package/dist/ai.decorator.test.d.ts.map +1 -0
- package/dist/ai.decorator.test.js +189 -0
- package/dist/ai.module.test.d.ts +2 -0
- package/dist/ai.module.test.d.ts.map +1 -0
- package/dist/ai.module.test.js +23 -0
- package/dist/ai.service.js +1 -1
- package/dist/ai.service.test.d.ts +2 -0
- package/dist/ai.service.test.d.ts.map +1 -0
- package/dist/ai.service.test.js +222 -0
- package/dist/context/context.manager.test.d.ts +2 -0
- package/dist/context/context.manager.test.d.ts.map +1 -0
- package/dist/context/context.manager.test.js +180 -0
- package/dist/providers/anthropic.provider.test.d.ts +2 -0
- package/dist/providers/anthropic.provider.test.d.ts.map +1 -0
- package/dist/providers/anthropic.provider.test.js +222 -0
- package/dist/providers/cohere.provider.test.d.ts +2 -0
- package/dist/providers/cohere.provider.test.d.ts.map +1 -0
- package/dist/providers/cohere.provider.test.js +267 -0
- package/dist/providers/gemini.provider.test.d.ts +2 -0
- package/dist/providers/gemini.provider.test.d.ts.map +1 -0
- package/dist/providers/gemini.provider.test.js +219 -0
- package/dist/providers/ollama.provider.test.d.ts +2 -0
- package/dist/providers/ollama.provider.test.d.ts.map +1 -0
- package/dist/providers/ollama.provider.test.js +267 -0
- package/dist/providers/openai.provider.test.d.ts +2 -0
- package/dist/providers/openai.provider.test.d.ts.map +1 -0
- package/dist/providers/openai.provider.test.js +364 -0
- package/dist/tracking/token.tracker.js +1 -1
- package/dist/tracking/token.tracker.test.d.ts +2 -0
- package/dist/tracking/token.tracker.test.d.ts.map +1 -0
- package/dist/tracking/token.tracker.test.js +272 -0
- package/dist/vector/vector.service.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('@hazeljs/core', () => ({
|
|
4
|
+
__esModule: true,
|
|
5
|
+
default: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
6
|
+
}));
|
|
7
|
+
const mockMessagesCreate = jest.fn();
|
|
8
|
+
const mockMessagesStream = jest.fn();
|
|
9
|
+
jest.mock('@anthropic-ai/sdk', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: jest.fn().mockImplementation(() => ({
|
|
12
|
+
messages: {
|
|
13
|
+
create: mockMessagesCreate,
|
|
14
|
+
stream: mockMessagesStream,
|
|
15
|
+
},
|
|
16
|
+
})),
|
|
17
|
+
}));
|
|
18
|
+
const anthropic_provider_1 = require("./anthropic.provider");
|
|
19
|
+
const BASE_REQUEST = {
|
|
20
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
21
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
22
|
+
};
|
|
23
|
+
const MOCK_RESPONSE = {
|
|
24
|
+
id: 'msg_001',
|
|
25
|
+
content: [{ type: 'text', text: 'Hello there!' }],
|
|
26
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
27
|
+
stop_reason: 'end_turn',
|
|
28
|
+
usage: { input_tokens: 10, output_tokens: 15 },
|
|
29
|
+
};
|
|
30
|
+
describe('AnthropicProvider', () => {
|
|
31
|
+
let provider;
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
provider = new anthropic_provider_1.AnthropicProvider('test-api-key');
|
|
35
|
+
});
|
|
36
|
+
describe('constructor', () => {
|
|
37
|
+
it('sets name to anthropic', () => {
|
|
38
|
+
expect(provider.name).toBe('anthropic');
|
|
39
|
+
});
|
|
40
|
+
it('warns when no API key provided', () => {
|
|
41
|
+
new anthropic_provider_1.AnthropicProvider();
|
|
42
|
+
// Constructor runs without throwing
|
|
43
|
+
});
|
|
44
|
+
it('uses ANTHROPIC_API_KEY env var', () => {
|
|
45
|
+
process.env.ANTHROPIC_API_KEY = 'env-key';
|
|
46
|
+
const p = new anthropic_provider_1.AnthropicProvider();
|
|
47
|
+
expect(p).toBeDefined();
|
|
48
|
+
delete process.env.ANTHROPIC_API_KEY;
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('getSupportedModels()', () => {
|
|
52
|
+
it('returns a list of claude models', () => {
|
|
53
|
+
const models = provider.getSupportedModels();
|
|
54
|
+
expect(models).toContain('claude-3-5-sonnet-20241022');
|
|
55
|
+
expect(models.length).toBeGreaterThan(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('complete()', () => {
|
|
59
|
+
it('returns a completion response', async () => {
|
|
60
|
+
mockMessagesCreate.mockResolvedValue(MOCK_RESPONSE);
|
|
61
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
62
|
+
expect(result.content).toBe('Hello there!');
|
|
63
|
+
expect(result.role).toBe('assistant');
|
|
64
|
+
expect(result.usage?.promptTokens).toBe(10);
|
|
65
|
+
expect(result.usage?.completionTokens).toBe(15);
|
|
66
|
+
expect(result.usage?.totalTokens).toBe(25);
|
|
67
|
+
expect(result.finishReason).toBe('end_turn');
|
|
68
|
+
});
|
|
69
|
+
it('uses default model when not specified', async () => {
|
|
70
|
+
mockMessagesCreate.mockResolvedValue(MOCK_RESPONSE);
|
|
71
|
+
await provider.complete({ messages: [{ role: 'user', content: 'hi' }] });
|
|
72
|
+
expect(mockMessagesCreate).toHaveBeenCalledWith(expect.objectContaining({ model: 'claude-3-5-sonnet-20241022' }));
|
|
73
|
+
});
|
|
74
|
+
it('passes maxTokens when specified', async () => {
|
|
75
|
+
mockMessagesCreate.mockResolvedValue(MOCK_RESPONSE);
|
|
76
|
+
await provider.complete({ ...BASE_REQUEST, maxTokens: 500 });
|
|
77
|
+
expect(mockMessagesCreate).toHaveBeenCalledWith(expect.objectContaining({ max_tokens: 500 }));
|
|
78
|
+
});
|
|
79
|
+
it('separates system messages from conversation', async () => {
|
|
80
|
+
mockMessagesCreate.mockResolvedValue(MOCK_RESPONSE);
|
|
81
|
+
await provider.complete({
|
|
82
|
+
messages: [
|
|
83
|
+
{ role: 'system', content: 'You are helpful.' },
|
|
84
|
+
{ role: 'user', content: 'Hello' },
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
expect(mockMessagesCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
88
|
+
system: 'You are helpful.',
|
|
89
|
+
messages: [{ role: 'user', content: 'Hello' }],
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
it('handles response with no stop_reason', async () => {
|
|
93
|
+
mockMessagesCreate.mockResolvedValue({ ...MOCK_RESPONSE, stop_reason: null });
|
|
94
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
95
|
+
expect(result.finishReason).toBe('end_turn');
|
|
96
|
+
});
|
|
97
|
+
it('concatenates multiple text content blocks', async () => {
|
|
98
|
+
mockMessagesCreate.mockResolvedValue({
|
|
99
|
+
...MOCK_RESPONSE,
|
|
100
|
+
content: [
|
|
101
|
+
{ type: 'text', text: 'Part 1. ' },
|
|
102
|
+
{ type: 'text', text: 'Part 2.' },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
106
|
+
expect(result.content).toBe('Part 1. Part 2.');
|
|
107
|
+
});
|
|
108
|
+
it('ignores non-text content blocks', async () => {
|
|
109
|
+
mockMessagesCreate.mockResolvedValue({
|
|
110
|
+
...MOCK_RESPONSE,
|
|
111
|
+
content: [
|
|
112
|
+
{ type: 'tool_use', id: 'tool_1' },
|
|
113
|
+
{ type: 'text', text: 'Some text' },
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
117
|
+
expect(result.content).toBe('Some text');
|
|
118
|
+
});
|
|
119
|
+
it('throws wrapped error on API failure', async () => {
|
|
120
|
+
mockMessagesCreate.mockRejectedValue(new Error('Rate limit exceeded'));
|
|
121
|
+
await expect(provider.complete(BASE_REQUEST)).rejects.toThrow('Anthropic API error: Rate limit exceeded');
|
|
122
|
+
});
|
|
123
|
+
it('handles non-Error thrown objects', async () => {
|
|
124
|
+
mockMessagesCreate.mockRejectedValue('string error');
|
|
125
|
+
await expect(provider.complete(BASE_REQUEST)).rejects.toThrow('Anthropic API error: Unknown error');
|
|
126
|
+
});
|
|
127
|
+
it('sets undefined system when no system messages', async () => {
|
|
128
|
+
mockMessagesCreate.mockResolvedValue(MOCK_RESPONSE);
|
|
129
|
+
await provider.complete({ messages: [{ role: 'user', content: 'hi' }] });
|
|
130
|
+
expect(mockMessagesCreate).toHaveBeenCalledWith(expect.objectContaining({ system: undefined }));
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
describe('streamComplete()', () => {
|
|
134
|
+
function makeStreamEvents(events) {
|
|
135
|
+
return (async function* () {
|
|
136
|
+
for (const ev of events) {
|
|
137
|
+
yield ev;
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
141
|
+
it('yields chunks for message_start and content_block_delta events', async () => {
|
|
142
|
+
mockMessagesStream.mockReturnValue(makeStreamEvents([
|
|
143
|
+
{ type: 'message_start', message: { id: 'msg_1', usage: { input_tokens: 5 } } },
|
|
144
|
+
{ type: 'content_block_delta', delta: { type: 'text_delta', text: 'Hello' } },
|
|
145
|
+
{ type: 'content_block_delta', delta: { type: 'text_delta', text: ' world' } },
|
|
146
|
+
{ type: 'message_delta', usage: { output_tokens: 10 } },
|
|
147
|
+
{ type: 'message_stop' },
|
|
148
|
+
]));
|
|
149
|
+
const results = [];
|
|
150
|
+
for await (const chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
151
|
+
results.push(chunk);
|
|
152
|
+
}
|
|
153
|
+
// 2 content deltas + 1 message_stop
|
|
154
|
+
expect(results.length).toBeGreaterThan(0);
|
|
155
|
+
});
|
|
156
|
+
it('includes usage in final chunk', async () => {
|
|
157
|
+
mockMessagesStream.mockReturnValue(makeStreamEvents([
|
|
158
|
+
{ type: 'message_start', message: { id: 'msg_2', usage: { input_tokens: 8 } } },
|
|
159
|
+
{ type: 'message_delta', usage: { output_tokens: 12 } },
|
|
160
|
+
{ type: 'message_stop' },
|
|
161
|
+
]));
|
|
162
|
+
const results = [];
|
|
163
|
+
for await (const chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
164
|
+
results.push(chunk);
|
|
165
|
+
}
|
|
166
|
+
const finalChunk = results[results.length - 1];
|
|
167
|
+
expect(finalChunk.done).toBe(true);
|
|
168
|
+
expect(finalChunk.usage).toBeDefined();
|
|
169
|
+
});
|
|
170
|
+
it('ignores non-text_delta content blocks', async () => {
|
|
171
|
+
mockMessagesStream.mockReturnValue(makeStreamEvents([
|
|
172
|
+
{ type: 'content_block_delta', delta: { type: 'input_json_delta', partial_json: '{}' } },
|
|
173
|
+
{ type: 'message_stop' },
|
|
174
|
+
]));
|
|
175
|
+
const results = [];
|
|
176
|
+
for await (const chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
177
|
+
results.push(chunk);
|
|
178
|
+
}
|
|
179
|
+
// Only message_stop chunk
|
|
180
|
+
expect(results).toHaveLength(1);
|
|
181
|
+
});
|
|
182
|
+
it('uses default model for streaming', async () => {
|
|
183
|
+
mockMessagesStream.mockReturnValue(makeStreamEvents([{ type: 'message_stop' }]));
|
|
184
|
+
for await (const _chunk of provider.streamComplete({
|
|
185
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
186
|
+
})) {
|
|
187
|
+
// consume
|
|
188
|
+
}
|
|
189
|
+
expect(mockMessagesStream).toHaveBeenCalledWith(expect.objectContaining({ model: 'claude-3-5-sonnet-20241022' }));
|
|
190
|
+
});
|
|
191
|
+
it('throws wrapped error on streaming failure', async () => {
|
|
192
|
+
mockMessagesStream.mockImplementation(async function* () {
|
|
193
|
+
throw new Error('Stream error');
|
|
194
|
+
yield { type: 'message_stop' };
|
|
195
|
+
});
|
|
196
|
+
await expect(async () => {
|
|
197
|
+
for await (const _chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
198
|
+
// consume
|
|
199
|
+
}
|
|
200
|
+
}).rejects.toThrow('Anthropic streaming error: Stream error');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('embed()', () => {
|
|
204
|
+
it('throws not supported error', async () => {
|
|
205
|
+
await expect(provider.embed({ input: 'test' })).rejects.toThrow('Anthropic does not support embeddings');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe('isAvailable()', () => {
|
|
209
|
+
it('returns false when no API key', async () => {
|
|
210
|
+
const p = new anthropic_provider_1.AnthropicProvider('');
|
|
211
|
+
expect(await p.isAvailable()).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
it('returns true when API responds successfully', async () => {
|
|
214
|
+
mockMessagesCreate.mockResolvedValue(MOCK_RESPONSE);
|
|
215
|
+
expect(await provider.isAvailable()).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
it('returns false on API error', async () => {
|
|
218
|
+
mockMessagesCreate.mockRejectedValue(new Error('Unauthorized'));
|
|
219
|
+
expect(await provider.isAvailable()).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cohere.provider.test.d.ts","sourceRoot":"","sources":["../../src/providers/cohere.provider.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('@hazeljs/core', () => ({
|
|
4
|
+
__esModule: true,
|
|
5
|
+
default: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
6
|
+
}));
|
|
7
|
+
const mockGenerate = jest.fn();
|
|
8
|
+
const mockGenerateStream = jest.fn();
|
|
9
|
+
const mockEmbed = jest.fn();
|
|
10
|
+
const mockRerank = jest.fn();
|
|
11
|
+
jest.mock('cohere-ai', () => ({
|
|
12
|
+
CohereClient: jest.fn().mockImplementation(() => ({
|
|
13
|
+
generate: mockGenerate,
|
|
14
|
+
generateStream: mockGenerateStream,
|
|
15
|
+
embed: mockEmbed,
|
|
16
|
+
rerank: mockRerank,
|
|
17
|
+
})),
|
|
18
|
+
}));
|
|
19
|
+
const cohere_provider_1 = require("./cohere.provider");
|
|
20
|
+
const BASE_REQUEST = {
|
|
21
|
+
messages: [{ role: 'user', content: 'Hello Cohere' }],
|
|
22
|
+
model: 'command',
|
|
23
|
+
};
|
|
24
|
+
const MOCK_GENERATE_RESPONSE = {
|
|
25
|
+
id: 'cohere-123',
|
|
26
|
+
generations: [{ text: 'Cohere response' }],
|
|
27
|
+
meta: { billedUnits: { inputTokens: 10, outputTokens: 20 } },
|
|
28
|
+
};
|
|
29
|
+
describe('CohereProvider', () => {
|
|
30
|
+
let provider;
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
provider = new cohere_provider_1.CohereProvider('test-api-key');
|
|
34
|
+
});
|
|
35
|
+
describe('constructor', () => {
|
|
36
|
+
it('sets name to cohere', () => {
|
|
37
|
+
expect(provider.name).toBe('cohere');
|
|
38
|
+
});
|
|
39
|
+
it('warns when no API key', () => {
|
|
40
|
+
new cohere_provider_1.CohereProvider(); // Should not throw
|
|
41
|
+
});
|
|
42
|
+
it('uses COHERE_API_KEY env var', () => {
|
|
43
|
+
process.env.COHERE_API_KEY = 'env-key';
|
|
44
|
+
const p = new cohere_provider_1.CohereProvider();
|
|
45
|
+
expect(p).toBeDefined();
|
|
46
|
+
delete process.env.COHERE_API_KEY;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('getSupportedModels()', () => {
|
|
50
|
+
it('returns list of cohere models', () => {
|
|
51
|
+
const models = provider.getSupportedModels();
|
|
52
|
+
expect(models).toContain('command');
|
|
53
|
+
expect(models.length).toBeGreaterThan(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('complete()', () => {
|
|
57
|
+
it('returns a completion response', async () => {
|
|
58
|
+
mockGenerate.mockResolvedValue(MOCK_GENERATE_RESPONSE);
|
|
59
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
60
|
+
expect(result.content).toBe('Cohere response');
|
|
61
|
+
expect(result.role).toBe('assistant');
|
|
62
|
+
expect(result.usage?.promptTokens).toBe(10);
|
|
63
|
+
expect(result.usage?.completionTokens).toBe(20);
|
|
64
|
+
expect(result.usage?.totalTokens).toBe(30);
|
|
65
|
+
expect(result.finishReason).toBe('COMPLETE');
|
|
66
|
+
});
|
|
67
|
+
it('uses default model when not specified', async () => {
|
|
68
|
+
mockGenerate.mockResolvedValue(MOCK_GENERATE_RESPONSE);
|
|
69
|
+
await provider.complete({ messages: [{ role: 'user', content: 'hi' }] });
|
|
70
|
+
expect(mockGenerate).toHaveBeenCalledWith(expect.objectContaining({ model: 'command' }));
|
|
71
|
+
});
|
|
72
|
+
it('passes temperature and maxTokens to API', async () => {
|
|
73
|
+
mockGenerate.mockResolvedValue(MOCK_GENERATE_RESPONSE);
|
|
74
|
+
await provider.complete({ ...BASE_REQUEST, temperature: 0.5, maxTokens: 200, topP: 0.9 });
|
|
75
|
+
expect(mockGenerate).toHaveBeenCalledWith(expect.objectContaining({ temperature: 0.5, maxTokens: 200, p: 0.9 }));
|
|
76
|
+
});
|
|
77
|
+
it('handles missing meta/billedUnits gracefully', async () => {
|
|
78
|
+
mockGenerate.mockResolvedValue({
|
|
79
|
+
generations: [{ text: 'ok' }],
|
|
80
|
+
meta: undefined,
|
|
81
|
+
});
|
|
82
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
83
|
+
expect(result.usage?.totalTokens).toBe(0);
|
|
84
|
+
});
|
|
85
|
+
it('uses generated id when response has one', async () => {
|
|
86
|
+
mockGenerate.mockResolvedValue(MOCK_GENERATE_RESPONSE);
|
|
87
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
88
|
+
expect(result.id).toBe('cohere-123');
|
|
89
|
+
});
|
|
90
|
+
it('generates a fallback id when response has no id', async () => {
|
|
91
|
+
mockGenerate.mockResolvedValue({ ...MOCK_GENERATE_RESPONSE, id: undefined });
|
|
92
|
+
const result = await provider.complete(BASE_REQUEST);
|
|
93
|
+
expect(result.id).toMatch(/^cohere-\d+$/);
|
|
94
|
+
});
|
|
95
|
+
it('converts messages to prompt format', async () => {
|
|
96
|
+
mockGenerate.mockResolvedValue(MOCK_GENERATE_RESPONSE);
|
|
97
|
+
await provider.complete({
|
|
98
|
+
messages: [
|
|
99
|
+
{ role: 'user', content: 'User msg' },
|
|
100
|
+
{ role: 'assistant', content: 'Asst msg' },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
const callArg = mockGenerate.mock.calls[0][0];
|
|
104
|
+
expect(callArg.prompt).toContain('user: User msg');
|
|
105
|
+
expect(callArg.prompt).toContain('assistant: Asst msg');
|
|
106
|
+
});
|
|
107
|
+
it('throws wrapped error on API failure', async () => {
|
|
108
|
+
mockGenerate.mockRejectedValue(new Error('Quota exceeded'));
|
|
109
|
+
await expect(provider.complete(BASE_REQUEST)).rejects.toThrow('Cohere API error: Quota exceeded');
|
|
110
|
+
});
|
|
111
|
+
it('wraps non-Error thrown values', async () => {
|
|
112
|
+
mockGenerate.mockRejectedValue('string error');
|
|
113
|
+
await expect(provider.complete(BASE_REQUEST)).rejects.toThrow('Cohere API error: Unknown error');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('streamComplete()', () => {
|
|
117
|
+
it('yields text-generation chunks', async () => {
|
|
118
|
+
async function* mockStream() {
|
|
119
|
+
yield { eventType: 'text-generation', text: 'Hello ' };
|
|
120
|
+
yield { eventType: 'text-generation', text: 'world' };
|
|
121
|
+
yield {
|
|
122
|
+
eventType: 'stream-end',
|
|
123
|
+
response: {
|
|
124
|
+
meta: { billedUnits: { inputTokens: 5, outputTokens: 10 } },
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
mockGenerateStream.mockReturnValue(mockStream());
|
|
129
|
+
const results = [];
|
|
130
|
+
for await (const chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
131
|
+
results.push(chunk);
|
|
132
|
+
}
|
|
133
|
+
// 2 text chunks + 1 stream-end
|
|
134
|
+
expect(results.length).toBe(3);
|
|
135
|
+
});
|
|
136
|
+
it('yields done=true chunk on stream-end with usage', async () => {
|
|
137
|
+
async function* mockStream() {
|
|
138
|
+
yield {
|
|
139
|
+
eventType: 'stream-end',
|
|
140
|
+
response: { meta: { billedUnits: { inputTokens: 5, outputTokens: 3 } } },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
mockGenerateStream.mockReturnValue(mockStream());
|
|
144
|
+
const results = [];
|
|
145
|
+
for await (const chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
146
|
+
results.push(chunk);
|
|
147
|
+
}
|
|
148
|
+
const last = results[results.length - 1];
|
|
149
|
+
expect(last.done).toBe(true);
|
|
150
|
+
expect(last.usage).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
it('yields stream-end with undefined usage when no billedUnits', async () => {
|
|
153
|
+
async function* mockStream() {
|
|
154
|
+
yield { eventType: 'stream-end', response: { meta: {} } };
|
|
155
|
+
}
|
|
156
|
+
mockGenerateStream.mockReturnValue(mockStream());
|
|
157
|
+
const results = [];
|
|
158
|
+
for await (const chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
159
|
+
results.push(chunk);
|
|
160
|
+
}
|
|
161
|
+
expect(results[0].usage).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
it('ignores unknown event types', async () => {
|
|
164
|
+
async function* mockStream() {
|
|
165
|
+
yield { eventType: 'unknown-event' };
|
|
166
|
+
yield { eventType: 'stream-end', response: {} };
|
|
167
|
+
}
|
|
168
|
+
mockGenerateStream.mockReturnValue(mockStream());
|
|
169
|
+
const results = [];
|
|
170
|
+
for await (const chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
171
|
+
results.push(chunk);
|
|
172
|
+
}
|
|
173
|
+
expect(results.length).toBe(1); // only stream-end
|
|
174
|
+
});
|
|
175
|
+
it('throws wrapped error on streaming failure', async () => {
|
|
176
|
+
mockGenerateStream.mockImplementation(async function* () {
|
|
177
|
+
throw new Error('Stream crashed');
|
|
178
|
+
yield { eventType: 'stream-end' };
|
|
179
|
+
});
|
|
180
|
+
await expect(async () => {
|
|
181
|
+
for await (const _chunk of provider.streamComplete(BASE_REQUEST)) {
|
|
182
|
+
// consume
|
|
183
|
+
}
|
|
184
|
+
}).rejects.toThrow('Cohere streaming error: Stream crashed');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('embed()', () => {
|
|
188
|
+
it('returns embeddings for string array', async () => {
|
|
189
|
+
mockEmbed.mockResolvedValue({
|
|
190
|
+
embeddings: [
|
|
191
|
+
[0.1, 0.2],
|
|
192
|
+
[0.3, 0.4],
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
const result = await provider.embed({ input: ['first', 'second'] });
|
|
196
|
+
expect(result.embeddings).toHaveLength(2);
|
|
197
|
+
expect(result.model).toBe('embed-english-v3.0');
|
|
198
|
+
});
|
|
199
|
+
it('handles single string input', async () => {
|
|
200
|
+
mockEmbed.mockResolvedValue({ embeddings: [[0.5, 0.6]] });
|
|
201
|
+
const result = await provider.embed({ input: 'single' });
|
|
202
|
+
expect(result.embeddings).toHaveLength(1);
|
|
203
|
+
});
|
|
204
|
+
it('handles { float: number[][] } response format', async () => {
|
|
205
|
+
mockEmbed.mockResolvedValue({ embeddings: { float: [[0.7, 0.8]] } });
|
|
206
|
+
const result = await provider.embed({ input: 'test' });
|
|
207
|
+
expect(result.embeddings).toEqual([[0.7, 0.8]]);
|
|
208
|
+
});
|
|
209
|
+
it('uses custom model when specified', async () => {
|
|
210
|
+
mockEmbed.mockResolvedValue({ embeddings: [[0.1]] });
|
|
211
|
+
const result = await provider.embed({ input: 'test', model: 'embed-multilingual-v3.0' });
|
|
212
|
+
expect(result.model).toBe('embed-multilingual-v3.0');
|
|
213
|
+
});
|
|
214
|
+
it('estimates token usage from input length', async () => {
|
|
215
|
+
mockEmbed.mockResolvedValue({ embeddings: [[0.1]] });
|
|
216
|
+
const result = await provider.embed({ input: 'hello world' }); // 11 chars → ~3 tokens
|
|
217
|
+
expect(result.usage?.promptTokens).toBeGreaterThan(0);
|
|
218
|
+
});
|
|
219
|
+
it('throws wrapped error on failure', async () => {
|
|
220
|
+
mockEmbed.mockRejectedValue(new Error('Embedding failed'));
|
|
221
|
+
await expect(provider.embed({ input: 'test' })).rejects.toThrow('Cohere embedding error: Embedding failed');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('isAvailable()', () => {
|
|
225
|
+
it('returns false when no API key', async () => {
|
|
226
|
+
const p = new cohere_provider_1.CohereProvider('');
|
|
227
|
+
expect(await p.isAvailable()).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
it('returns true when API responds', async () => {
|
|
230
|
+
mockGenerate.mockResolvedValue(MOCK_GENERATE_RESPONSE);
|
|
231
|
+
expect(await provider.isAvailable()).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
it('returns false on API error', async () => {
|
|
234
|
+
mockGenerate.mockRejectedValue(new Error('Unauthorized'));
|
|
235
|
+
expect(await provider.isAvailable()).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
describe('rerank()', () => {
|
|
239
|
+
it('returns ranked documents', async () => {
|
|
240
|
+
mockRerank.mockResolvedValue({
|
|
241
|
+
results: [
|
|
242
|
+
{ index: 1, relevanceScore: 0.9 },
|
|
243
|
+
{ index: 0, relevanceScore: 0.7 },
|
|
244
|
+
],
|
|
245
|
+
});
|
|
246
|
+
const result = await provider.rerank('query', ['doc0', 'doc1'], 2);
|
|
247
|
+
expect(result).toHaveLength(2);
|
|
248
|
+
expect(result[0].score).toBe(0.9);
|
|
249
|
+
expect(result[0].index).toBe(1);
|
|
250
|
+
expect(result[0].document).toBe('doc1');
|
|
251
|
+
});
|
|
252
|
+
it('uses default rerank model', async () => {
|
|
253
|
+
mockRerank.mockResolvedValue({ results: [] });
|
|
254
|
+
await provider.rerank('query', ['doc1']);
|
|
255
|
+
expect(mockRerank).toHaveBeenCalledWith(expect.objectContaining({ model: 'rerank-english-v3.0' }));
|
|
256
|
+
});
|
|
257
|
+
it('uses custom model when specified', async () => {
|
|
258
|
+
mockRerank.mockResolvedValue({ results: [] });
|
|
259
|
+
await provider.rerank('query', ['doc'], undefined, 'rerank-multilingual-v3.0');
|
|
260
|
+
expect(mockRerank).toHaveBeenCalledWith(expect.objectContaining({ model: 'rerank-multilingual-v3.0' }));
|
|
261
|
+
});
|
|
262
|
+
it('throws wrapped error on rerank failure', async () => {
|
|
263
|
+
mockRerank.mockRejectedValue(new Error('Rerank failed'));
|
|
264
|
+
await expect(provider.rerank('q', ['d'])).rejects.toThrow('Cohere rerank error: Rerank failed');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gemini.provider.test.d.ts","sourceRoot":"","sources":["../../src/providers/gemini.provider.test.ts"],"names":[],"mappings":""}
|