@machina.ai/cell-cli-core 1.49.0-rc5 → 1.50.0-rc1
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/dist/docs/CHANGES.md +23 -0
- package/dist/docs/adr/001-openai-xai-compatible-adapters.md +75 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/src/agents/agentLoader.d.ts +4 -4
- package/dist/src/code_assist/types.d.ts +20 -20
- package/dist/src/config/config.d.ts +9 -0
- package/dist/src/config/config.js +33 -0
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +17 -6
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/config/defaultModelConfigs.js +65 -4
- package/dist/src/config/defaultModelConfigs.js.map +1 -1
- package/dist/src/config/extensions/integrityTypes.d.ts +2 -2
- package/dist/src/config/flashFallback.test.js +14 -11
- package/dist/src/config/flashFallback.test.js.map +1 -1
- package/dist/src/config/localModel.test.d.ts +6 -0
- package/dist/src/config/localModel.test.js +114 -0
- package/dist/src/config/localModel.test.js.map +1 -0
- package/dist/src/config/models.d.ts +17 -1
- package/dist/src/config/models.js +75 -2
- package/dist/src/config/models.js.map +1 -1
- package/dist/src/config/models.test.js +48 -1
- package/dist/src/config/models.test.js.map +1 -1
- package/dist/src/core/baseLlmClient.js +26 -1
- package/dist/src/core/baseLlmClient.js.map +1 -1
- package/dist/src/core/contentGenerator.js +74 -1
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/contentGenerator.test.js +44 -0
- package/dist/src/core/contentGenerator.test.js.map +1 -1
- package/dist/src/core/openAiCompatibleContentGenerator.d.ts +156 -0
- package/dist/src/core/openAiCompatibleContentGenerator.js +1161 -0
- package/dist/src/core/openAiCompatibleContentGenerator.js.map +1 -0
- package/dist/src/core/openAiCompatibleContentGenerator.test.d.ts +6 -0
- package/dist/src/core/openAiCompatibleContentGenerator.test.js +850 -0
- package/dist/src/core/openAiCompatibleContentGenerator.test.js.map +1 -0
- package/dist/src/generated/git-commit.d.ts +2 -2
- package/dist/src/generated/git-commit.js +2 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/routing/strategies/approvalModeStrategy.test.js +1 -1
- package/dist/src/utils/modelUtils.d.ts +8 -0
- package/dist/src/utils/modelUtils.js +59 -0
- package/dist/src/utils/modelUtils.js.map +1 -1
- package/dist/src/utils/modelUtils.test.js +46 -2
- package/dist/src/utils/modelUtils.test.js.map +1 -1
- package/dist/src/voice/whisperModelManager.js +2 -0
- package/dist/src/voice/whisperModelManager.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
|
+
import { OpenAiCompatibleContentGenerator } from './openAiCompatibleContentGenerator.js';
|
|
8
|
+
import { LlmRole } from '../telemetry/types.js';
|
|
9
|
+
import { Type, ThinkingLevel } from '@google/genai';
|
|
10
|
+
function getFetchBody() {
|
|
11
|
+
const call = vi.mocked(fetch).mock.calls[0];
|
|
12
|
+
const init = call?.[1];
|
|
13
|
+
return JSON.parse(String(init?.body || '{}'));
|
|
14
|
+
}
|
|
15
|
+
function createSseBody(events) {
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
const chunks = events.map((event) => {
|
|
18
|
+
if (typeof event === 'string') {
|
|
19
|
+
return encoder.encode(event);
|
|
20
|
+
}
|
|
21
|
+
return encoder.encode(`data: ${JSON.stringify(event)}
|
|
22
|
+
|
|
23
|
+
`);
|
|
24
|
+
});
|
|
25
|
+
let index = 0;
|
|
26
|
+
return {
|
|
27
|
+
getReader() {
|
|
28
|
+
return {
|
|
29
|
+
async read() {
|
|
30
|
+
if (index >= chunks.length) {
|
|
31
|
+
return { done: true, value: undefined };
|
|
32
|
+
}
|
|
33
|
+
const value = chunks[index];
|
|
34
|
+
index += 1;
|
|
35
|
+
return { done: false, value };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
describe('OpenAiCompatibleContentGenerator', () => {
|
|
42
|
+
let mockConfig;
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
45
|
+
mockConfig = {
|
|
46
|
+
getAgentApiUrl: vi.fn().mockReturnValue('https://cell-api.local/api/'),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.unstubAllGlobals();
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
});
|
|
53
|
+
it('defaults openai and xai providers to the Responses transport', () => {
|
|
54
|
+
const openai = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-5.6-luna', 'mock-keycloak-token');
|
|
55
|
+
expect(openai.provider).toBe('openai');
|
|
56
|
+
expect(openai.modelName).toBe('gpt-5.6-luna');
|
|
57
|
+
expect(openai.transportMode).toBe('responses');
|
|
58
|
+
expect(openai.endpointUrl).toBe('https://cell-api.local/api/cell/v1.1/openai/v1/responses');
|
|
59
|
+
const xai = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
60
|
+
expect(xai.transportMode).toBe('responses');
|
|
61
|
+
expect(xai.endpointUrl).toBe('https://cell-api.local/api/cell/v1.1/xai/v1/responses');
|
|
62
|
+
});
|
|
63
|
+
it('keeps chat_completions transport when explicitly configured', () => {
|
|
64
|
+
mockConfig.getOpenAiTransportMode = vi
|
|
65
|
+
.fn()
|
|
66
|
+
.mockReturnValue('chat_completions');
|
|
67
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-4o', 'mock-keycloak-token');
|
|
68
|
+
expect(generator.transportMode).toBe('chat_completions');
|
|
69
|
+
expect(generator.endpointUrl).toBe('https://cell-api.local/api/cell/v1.1/openai/v1/chat/completions');
|
|
70
|
+
});
|
|
71
|
+
it('should inject Keycloak Authorization token correctly into request headers', async () => {
|
|
72
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-2', 'mock-keycloak-token');
|
|
73
|
+
const mockResponse = {
|
|
74
|
+
ok: true,
|
|
75
|
+
json: async () => ({
|
|
76
|
+
output: [
|
|
77
|
+
{
|
|
78
|
+
type: 'message',
|
|
79
|
+
content: [{ type: 'output_text', text: 'Hello World' }],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
usage: {
|
|
83
|
+
input_tokens: 9,
|
|
84
|
+
output_tokens: 12,
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
89
|
+
const response = await generator.generateContent({
|
|
90
|
+
model: 'xai/grok-2',
|
|
91
|
+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }],
|
|
92
|
+
}, 'user-prompt-1', LlmRole.MAIN);
|
|
93
|
+
expect(response).toBeDefined();
|
|
94
|
+
expect(fetch).toHaveBeenCalledWith('https://cell-api.local/api/cell/v1.1/xai/v1/responses', expect.objectContaining({
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: expect.objectContaining({
|
|
97
|
+
Authorization: 'Bearer mock-keycloak-token',
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
}),
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
it('should inject sessionId header correctly into request headers when passed in constructor', async () => {
|
|
103
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-2', 'mock-keycloak-token', 'test-session-id-param');
|
|
104
|
+
const mockResponse = {
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => ({
|
|
107
|
+
output: [
|
|
108
|
+
{
|
|
109
|
+
type: 'message',
|
|
110
|
+
content: [{ type: 'output_text', text: 'response' }],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
116
|
+
await generator.generateContent({
|
|
117
|
+
model: 'xai/grok-2',
|
|
118
|
+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }],
|
|
119
|
+
}, 'user-prompt-1', LlmRole.MAIN);
|
|
120
|
+
expect(fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
121
|
+
headers: expect.objectContaining({
|
|
122
|
+
sessionId: 'test-session-id-param',
|
|
123
|
+
}),
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
it('should fallback to config.getSessionId() for sessionId header when not passed in constructor', async () => {
|
|
127
|
+
mockConfig.getSessionId = vi.fn().mockReturnValue('fallback-session-id');
|
|
128
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-2', 'mock-keycloak-token');
|
|
129
|
+
const mockResponse = {
|
|
130
|
+
ok: true,
|
|
131
|
+
json: async () => ({
|
|
132
|
+
output: [
|
|
133
|
+
{
|
|
134
|
+
type: 'message',
|
|
135
|
+
content: [{ type: 'output_text', text: 'response' }],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
141
|
+
await generator.generateContent({
|
|
142
|
+
model: 'xai/grok-2',
|
|
143
|
+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }],
|
|
144
|
+
}, 'user-prompt-1', LlmRole.MAIN);
|
|
145
|
+
expect(fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
146
|
+
headers: expect.objectContaining({
|
|
147
|
+
sessionId: 'fallback-session-id',
|
|
148
|
+
}),
|
|
149
|
+
}));
|
|
150
|
+
});
|
|
151
|
+
it('maps conversation history to Responses input and always sets store:false', async () => {
|
|
152
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-4o', 'mock-keycloak-token');
|
|
153
|
+
const mockResponse = {
|
|
154
|
+
ok: true,
|
|
155
|
+
json: async () => ({
|
|
156
|
+
output: [
|
|
157
|
+
{
|
|
158
|
+
type: 'message',
|
|
159
|
+
content: [{ type: 'output_text', text: 'hello back' }],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
165
|
+
await generator.generateContent({
|
|
166
|
+
model: 'openai/gpt-4o',
|
|
167
|
+
contents: [
|
|
168
|
+
{ role: 'user', parts: [{ text: 'Hello' }] },
|
|
169
|
+
{ role: 'model', parts: [{ text: 'Hi, how can I help?' }] },
|
|
170
|
+
{ role: 'user', parts: [{ text: 'Tell me a joke.' }] },
|
|
171
|
+
],
|
|
172
|
+
config: {
|
|
173
|
+
systemInstruction: 'You are a funny assistant.',
|
|
174
|
+
},
|
|
175
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
176
|
+
const body = getFetchBody();
|
|
177
|
+
expect(body).toEqual({
|
|
178
|
+
model: 'gpt-4o',
|
|
179
|
+
store: false,
|
|
180
|
+
input: [
|
|
181
|
+
{ role: 'system', content: 'You are a funny assistant.' },
|
|
182
|
+
{ role: 'user', content: 'Hello' },
|
|
183
|
+
{ role: 'assistant', content: 'Hi, how can I help?' },
|
|
184
|
+
{ role: 'user', content: 'Tell me a joke.' },
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
expect(body).not.toHaveProperty('previous_response_id');
|
|
188
|
+
expect(body).not.toHaveProperty('messages');
|
|
189
|
+
});
|
|
190
|
+
it('maps multi-turn tool history into function_call and function_call_output items', async () => {
|
|
191
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-4o', 'mock-keycloak-token');
|
|
192
|
+
const mockResponse = {
|
|
193
|
+
ok: true,
|
|
194
|
+
json: async () => ({
|
|
195
|
+
output: [
|
|
196
|
+
{
|
|
197
|
+
type: 'message',
|
|
198
|
+
content: [{ type: 'output_text', text: 'tool response processed' }],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
203
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
204
|
+
await generator.generateContent({
|
|
205
|
+
model: 'openai/gpt-4o',
|
|
206
|
+
contents: [
|
|
207
|
+
{ role: 'user', parts: [{ text: 'What is the weather in London?' }] },
|
|
208
|
+
{
|
|
209
|
+
role: 'model',
|
|
210
|
+
parts: [
|
|
211
|
+
{
|
|
212
|
+
functionCall: {
|
|
213
|
+
name: 'get_weather',
|
|
214
|
+
args: { location: 'London' },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
role: 'user',
|
|
221
|
+
parts: [
|
|
222
|
+
{
|
|
223
|
+
functionResponse: {
|
|
224
|
+
name: 'get_weather',
|
|
225
|
+
response: { temperature: '15C' },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
232
|
+
const body = getFetchBody();
|
|
233
|
+
expect(body['input']).toEqual([
|
|
234
|
+
{ role: 'user', content: 'What is the weather in London?' },
|
|
235
|
+
{
|
|
236
|
+
type: 'function_call',
|
|
237
|
+
call_id: 'call_0',
|
|
238
|
+
name: 'get_weather',
|
|
239
|
+
arguments: JSON.stringify({ location: 'London' }),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: 'function_call_output',
|
|
243
|
+
call_id: 'call_0',
|
|
244
|
+
output: JSON.stringify({ temperature: '15C' }),
|
|
245
|
+
},
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
it('maps tools to internally-tagged Responses format with strict:false', async () => {
|
|
249
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-4o', 'mock-keycloak-token');
|
|
250
|
+
const mockResponse = {
|
|
251
|
+
ok: true,
|
|
252
|
+
json: async () => ({
|
|
253
|
+
output: [
|
|
254
|
+
{
|
|
255
|
+
type: 'message',
|
|
256
|
+
content: [
|
|
257
|
+
{ type: 'output_text', text: 'I will call get_weather now' },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
type: 'function_call',
|
|
262
|
+
call_id: 'call_999',
|
|
263
|
+
name: 'get_weather',
|
|
264
|
+
arguments: '{"location":"New York"}',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
usage: {
|
|
268
|
+
input_tokens: 15,
|
|
269
|
+
output_tokens: 25,
|
|
270
|
+
output_tokens_details: { reasoning_tokens: 3 },
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
};
|
|
274
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
275
|
+
const response = await generator.generateContent({
|
|
276
|
+
model: 'openai/gpt-4o',
|
|
277
|
+
contents: [
|
|
278
|
+
{ role: 'user', parts: [{ text: 'Check weather in New York' }] },
|
|
279
|
+
],
|
|
280
|
+
config: {
|
|
281
|
+
tools: [
|
|
282
|
+
{
|
|
283
|
+
functionDeclarations: [
|
|
284
|
+
{
|
|
285
|
+
name: 'get_weather',
|
|
286
|
+
description: 'Get weather description',
|
|
287
|
+
parameters: {
|
|
288
|
+
type: Type.OBJECT,
|
|
289
|
+
properties: {
|
|
290
|
+
location: { type: Type.STRING },
|
|
291
|
+
},
|
|
292
|
+
required: ['location'],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
300
|
+
const body = getFetchBody();
|
|
301
|
+
expect(body['tools']).toEqual([
|
|
302
|
+
{
|
|
303
|
+
type: 'function',
|
|
304
|
+
name: 'get_weather',
|
|
305
|
+
description: 'Get weather description',
|
|
306
|
+
parameters: {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {
|
|
309
|
+
location: { type: 'string' },
|
|
310
|
+
},
|
|
311
|
+
required: ['location'],
|
|
312
|
+
},
|
|
313
|
+
strict: false,
|
|
314
|
+
},
|
|
315
|
+
]);
|
|
316
|
+
expect(response.candidates?.[0]?.content?.parts).toEqual([
|
|
317
|
+
{ text: 'I will call get_weather now' },
|
|
318
|
+
{
|
|
319
|
+
functionCall: {
|
|
320
|
+
id: 'call_999',
|
|
321
|
+
name: 'get_weather',
|
|
322
|
+
args: { location: 'New York' },
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
]);
|
|
326
|
+
expect(response.usageMetadata).toEqual({
|
|
327
|
+
promptTokenCount: 15,
|
|
328
|
+
candidatesTokenCount: 25,
|
|
329
|
+
totalTokenCount: 43,
|
|
330
|
+
thoughtsTokenCount: 3,
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
it('omits reasoning for grok-4-1-fast models', async () => {
|
|
334
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4-1-fast-reasoning', 'mock-keycloak-token');
|
|
335
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
336
|
+
ok: true,
|
|
337
|
+
json: async () => ({
|
|
338
|
+
output: [
|
|
339
|
+
{
|
|
340
|
+
type: 'message',
|
|
341
|
+
content: [{ type: 'output_text', text: 'ok' }],
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
await generator.generateContent({
|
|
347
|
+
model: 'xai/grok-4-1-fast-reasoning',
|
|
348
|
+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }],
|
|
349
|
+
config: {
|
|
350
|
+
thinkingConfig: {
|
|
351
|
+
thinkingLevel: ThinkingLevel.HIGH,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
355
|
+
const body = getFetchBody();
|
|
356
|
+
expect(body).not.toHaveProperty('reasoning');
|
|
357
|
+
});
|
|
358
|
+
it('clamps xhigh reasoning effort to high for xAI models', async () => {
|
|
359
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
360
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
361
|
+
ok: true,
|
|
362
|
+
json: async () => ({
|
|
363
|
+
output: [
|
|
364
|
+
{
|
|
365
|
+
type: 'message',
|
|
366
|
+
content: [{ type: 'output_text', text: 'ok' }],
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
await generator.generateContent({
|
|
372
|
+
model: 'xai/grok-4.5',
|
|
373
|
+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }],
|
|
374
|
+
config: {
|
|
375
|
+
thinkingConfig: {
|
|
376
|
+
// Exercise clamp path via unsupported high-end effort string.
|
|
377
|
+
thinkingLevel: 'xhigh',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
381
|
+
const body = getFetchBody();
|
|
382
|
+
expect(body['reasoning']).toEqual({ effort: 'high' });
|
|
383
|
+
});
|
|
384
|
+
it('sends reasoning.effort for gpt-5.6 models with tools', async () => {
|
|
385
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-5.6-luna', 'mock-keycloak-token');
|
|
386
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
387
|
+
ok: true,
|
|
388
|
+
json: async () => ({
|
|
389
|
+
output: [
|
|
390
|
+
{
|
|
391
|
+
type: 'function_call',
|
|
392
|
+
call_id: 'call_1',
|
|
393
|
+
name: 'read_file',
|
|
394
|
+
arguments: '{"path":"a.ts"}',
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
}),
|
|
398
|
+
});
|
|
399
|
+
await generator.generateContent({
|
|
400
|
+
model: 'openai/gpt-5.6-luna',
|
|
401
|
+
contents: [{ role: 'user', parts: [{ text: 'read a.ts' }] }],
|
|
402
|
+
config: {
|
|
403
|
+
thinkingConfig: {
|
|
404
|
+
thinkingLevel: ThinkingLevel.HIGH,
|
|
405
|
+
},
|
|
406
|
+
tools: [
|
|
407
|
+
{
|
|
408
|
+
functionDeclarations: [
|
|
409
|
+
{
|
|
410
|
+
name: 'read_file',
|
|
411
|
+
description: 'Read a file',
|
|
412
|
+
parameters: {
|
|
413
|
+
type: Type.OBJECT,
|
|
414
|
+
properties: {
|
|
415
|
+
path: { type: Type.STRING },
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
424
|
+
const body = getFetchBody();
|
|
425
|
+
expect(body['reasoning']).toEqual({ effort: 'high' });
|
|
426
|
+
expect(body['store']).toBe(false);
|
|
427
|
+
expect(Array.isArray(body['tools'])).toBe(true);
|
|
428
|
+
});
|
|
429
|
+
it('omits top_p for OpenAI Responses even when request config provides topP', async () => {
|
|
430
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-5.6-luna', 'mock-keycloak-token');
|
|
431
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
432
|
+
ok: true,
|
|
433
|
+
json: async () => ({
|
|
434
|
+
output: [
|
|
435
|
+
{
|
|
436
|
+
type: 'message',
|
|
437
|
+
content: [{ type: 'output_text', text: 'ok' }],
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
}),
|
|
441
|
+
});
|
|
442
|
+
const stream = await generator.generateContentStream({
|
|
443
|
+
model: 'openai/gpt-5.6-luna',
|
|
444
|
+
contents: [{ role: 'user', parts: [{ text: 'hola' }] }],
|
|
445
|
+
config: {
|
|
446
|
+
topP: 0.95,
|
|
447
|
+
temperature: 1,
|
|
448
|
+
},
|
|
449
|
+
}, 'prompt-top-p', LlmRole.MAIN);
|
|
450
|
+
for await (const _chunk of stream) {
|
|
451
|
+
// Drain so makeStream executes the fetch.
|
|
452
|
+
}
|
|
453
|
+
const body = getFetchBody();
|
|
454
|
+
expect(body).not.toHaveProperty('top_p');
|
|
455
|
+
expect(body['temperature']).toBe(1);
|
|
456
|
+
});
|
|
457
|
+
it('uses non-stream Responses for OpenAI GPT simple prompt to avoid stream_options proxy bug', async () => {
|
|
458
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-5.6-luna', 'mock-keycloak-token');
|
|
459
|
+
const mockResponse = {
|
|
460
|
+
ok: true,
|
|
461
|
+
json: async () => ({
|
|
462
|
+
output: [
|
|
463
|
+
{
|
|
464
|
+
type: 'message',
|
|
465
|
+
content: [{ type: 'output_text', text: '¡Hola!' }],
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
usage: { input_tokens: 2, output_tokens: 1 },
|
|
469
|
+
}),
|
|
470
|
+
};
|
|
471
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
472
|
+
const stream = await generator.generateContentStream({
|
|
473
|
+
model: 'openai/gpt-5.6-luna',
|
|
474
|
+
contents: [{ role: 'user', parts: [{ text: 'hola' }] }],
|
|
475
|
+
}, 'prompt-hola', LlmRole.MAIN);
|
|
476
|
+
const texts = [];
|
|
477
|
+
for await (const chunk of stream) {
|
|
478
|
+
const text = chunk.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
479
|
+
if (text)
|
|
480
|
+
texts.push(text);
|
|
481
|
+
}
|
|
482
|
+
expect(texts).toEqual(['¡Hola!']);
|
|
483
|
+
// Critical: non-stream OpenAI must set finishReason or GeminiChat retries 4x
|
|
484
|
+
// and the UI concatenates the same greeting.
|
|
485
|
+
// (asserted via a second drain below on a dedicated response)
|
|
486
|
+
const body = getFetchBody();
|
|
487
|
+
expect(body).toEqual({
|
|
488
|
+
model: 'gpt-5.6-luna',
|
|
489
|
+
store: false,
|
|
490
|
+
input: [{ role: 'user', content: 'hola' }],
|
|
491
|
+
});
|
|
492
|
+
expect(body).not.toHaveProperty('stream');
|
|
493
|
+
expect(body).not.toHaveProperty('stream_options');
|
|
494
|
+
expect(body).not.toHaveProperty('previous_response_id');
|
|
495
|
+
expect(body).not.toHaveProperty('messages');
|
|
496
|
+
expect(body).not.toHaveProperty('reasoning_effort');
|
|
497
|
+
expect(fetch).toHaveBeenCalledWith('https://cell-api.local/api/cell/v1.1/openai/v1/responses', expect.objectContaining({ method: 'POST' }));
|
|
498
|
+
});
|
|
499
|
+
it('sets finishReason STOP on non-stream OpenAI Responses payloads', async () => {
|
|
500
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-5.6-luna', 'mock-keycloak-token');
|
|
501
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
502
|
+
ok: true,
|
|
503
|
+
json: async () => ({
|
|
504
|
+
output: [
|
|
505
|
+
{
|
|
506
|
+
type: 'message',
|
|
507
|
+
content: [{ type: 'output_text', text: '¡Hola!' }],
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
usage: { input_tokens: 2, output_tokens: 1 },
|
|
511
|
+
}),
|
|
512
|
+
});
|
|
513
|
+
const stream = await generator.generateContentStream({
|
|
514
|
+
model: 'openai/gpt-5.6-luna',
|
|
515
|
+
contents: [{ role: 'user', parts: [{ text: 'hola' }] }],
|
|
516
|
+
}, 'prompt-finish', LlmRole.MAIN);
|
|
517
|
+
let finish;
|
|
518
|
+
let text = '';
|
|
519
|
+
for await (const chunk of stream) {
|
|
520
|
+
text += chunk.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
521
|
+
finish = chunk.candidates?.[0]?.finishReason || finish;
|
|
522
|
+
}
|
|
523
|
+
expect(text).toBe('¡Hola!');
|
|
524
|
+
expect(finish).toBe('STOP');
|
|
525
|
+
});
|
|
526
|
+
it('keeps true SSE streaming for xAI Responses', async () => {
|
|
527
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
528
|
+
const mockResponse = {
|
|
529
|
+
ok: true,
|
|
530
|
+
headers: {
|
|
531
|
+
get: (name) => name.toLowerCase() === 'content-type' ? 'text/event-stream' : null,
|
|
532
|
+
},
|
|
533
|
+
body: createSseBody([
|
|
534
|
+
{ type: 'response.output_text.delta', delta: 'Hola Grok' },
|
|
535
|
+
{
|
|
536
|
+
type: 'response.completed',
|
|
537
|
+
response: { usage: { input_tokens: 2, output_tokens: 1 } },
|
|
538
|
+
},
|
|
539
|
+
]),
|
|
540
|
+
};
|
|
541
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
542
|
+
const stream = await generator.generateContentStream({
|
|
543
|
+
model: 'xai/grok-4.5',
|
|
544
|
+
contents: [{ role: 'user', parts: [{ text: 'hola' }] }],
|
|
545
|
+
}, 'prompt-grok', LlmRole.MAIN);
|
|
546
|
+
const texts = [];
|
|
547
|
+
for await (const chunk of stream) {
|
|
548
|
+
const text = chunk.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
549
|
+
if (text)
|
|
550
|
+
texts.push(text);
|
|
551
|
+
}
|
|
552
|
+
expect(texts).toEqual(['Hola Grok']);
|
|
553
|
+
expect(getFetchBody()['stream']).toBe(true);
|
|
554
|
+
expect(getFetchBody()['store']).toBe(false);
|
|
555
|
+
});
|
|
556
|
+
it('falls back to non-stream JSON when Responses returns application/json', async () => {
|
|
557
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-5.6-luna', 'mock-keycloak-token');
|
|
558
|
+
const mockResponse = {
|
|
559
|
+
ok: true,
|
|
560
|
+
headers: {
|
|
561
|
+
get: () => 'application/json',
|
|
562
|
+
},
|
|
563
|
+
json: async () => ({
|
|
564
|
+
output: [
|
|
565
|
+
{
|
|
566
|
+
type: 'message',
|
|
567
|
+
content: [{ type: 'output_text', text: 'hola de vuelta' }],
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
usage: { input_tokens: 1, output_tokens: 2 },
|
|
571
|
+
}),
|
|
572
|
+
body: {
|
|
573
|
+
getReader() {
|
|
574
|
+
throw new Error('should not stream JSON body');
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
579
|
+
const stream = await generator.generateContentStream({
|
|
580
|
+
model: 'openai/gpt-5.6-luna',
|
|
581
|
+
contents: [{ role: 'user', parts: [{ text: 'hola' }] }],
|
|
582
|
+
}, 'prompt-hola-json', LlmRole.MAIN);
|
|
583
|
+
const chunks = [];
|
|
584
|
+
for await (const chunk of stream) {
|
|
585
|
+
chunks.push(chunk);
|
|
586
|
+
}
|
|
587
|
+
expect(chunks).toHaveLength(1);
|
|
588
|
+
expect(chunks[0].candidates?.[0]?.content?.parts?.[0]?.text).toBe('hola de vuelta');
|
|
589
|
+
});
|
|
590
|
+
it('parses Responses SSE text deltas', async () => {
|
|
591
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
592
|
+
const mockResponse = {
|
|
593
|
+
ok: true,
|
|
594
|
+
headers: {
|
|
595
|
+
get: (name) => name.toLowerCase() === 'content-type' ? 'text/event-stream' : null,
|
|
596
|
+
},
|
|
597
|
+
body: createSseBody([
|
|
598
|
+
{ type: 'response.output_text.delta', delta: 'Hello' },
|
|
599
|
+
{ type: 'response.output_text.delta', delta: ' World' },
|
|
600
|
+
{
|
|
601
|
+
type: 'response.completed',
|
|
602
|
+
response: {
|
|
603
|
+
usage: { input_tokens: 4, output_tokens: 2 },
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
]),
|
|
607
|
+
};
|
|
608
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
609
|
+
const stream = await generator.generateContentStream({
|
|
610
|
+
model: 'xai/grok-4.5',
|
|
611
|
+
contents: [{ role: 'user', parts: [{ text: 'stream test' }] }],
|
|
612
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
613
|
+
const chunks = [];
|
|
614
|
+
let usage;
|
|
615
|
+
for await (const chunk of stream) {
|
|
616
|
+
const text = chunk.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
617
|
+
if (text) {
|
|
618
|
+
chunks.push(text);
|
|
619
|
+
}
|
|
620
|
+
if (chunk.usageMetadata) {
|
|
621
|
+
usage = chunk.usageMetadata;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
expect(chunks).toEqual(['Hello', ' World']);
|
|
625
|
+
expect(usage).toEqual({
|
|
626
|
+
promptTokenCount: 4,
|
|
627
|
+
candidatesTokenCount: 2,
|
|
628
|
+
totalTokenCount: 6,
|
|
629
|
+
thoughtsTokenCount: undefined,
|
|
630
|
+
});
|
|
631
|
+
expect(getFetchBody()['stream']).toBe(true);
|
|
632
|
+
expect(getFetchBody()['store']).toBe(false);
|
|
633
|
+
expect(getFetchBody()).not.toHaveProperty('stream_options');
|
|
634
|
+
});
|
|
635
|
+
it('parses Responses SSE single tool call events', async () => {
|
|
636
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
637
|
+
const mockResponse = {
|
|
638
|
+
ok: true,
|
|
639
|
+
body: createSseBody([
|
|
640
|
+
{
|
|
641
|
+
type: 'response.output_item.added',
|
|
642
|
+
item: {
|
|
643
|
+
type: 'function_call',
|
|
644
|
+
id: 'fc_1',
|
|
645
|
+
call_id: 'call_abc',
|
|
646
|
+
name: 'get_weather',
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
type: 'response.function_call_arguments.delta',
|
|
651
|
+
item_id: 'fc_1',
|
|
652
|
+
call_id: 'call_abc',
|
|
653
|
+
delta: '{"location":',
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
type: 'response.function_call_arguments.delta',
|
|
657
|
+
item_id: 'fc_1',
|
|
658
|
+
call_id: 'call_abc',
|
|
659
|
+
delta: '"Paris"}',
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
type: 'response.output_item.done',
|
|
663
|
+
item: {
|
|
664
|
+
type: 'function_call',
|
|
665
|
+
id: 'fc_1',
|
|
666
|
+
call_id: 'call_abc',
|
|
667
|
+
name: 'get_weather',
|
|
668
|
+
arguments: '{"location":"Paris"}',
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
type: 'response.completed',
|
|
673
|
+
response: {
|
|
674
|
+
usage: { input_tokens: 10, output_tokens: 5 },
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
]),
|
|
678
|
+
};
|
|
679
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
680
|
+
const stream = await generator.generateContentStream({
|
|
681
|
+
model: 'xai/grok-4.5',
|
|
682
|
+
contents: [{ role: 'user', parts: [{ text: 'weather?' }] }],
|
|
683
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
684
|
+
const functionCalls = [];
|
|
685
|
+
for await (const chunk of stream) {
|
|
686
|
+
if (chunk.functionCalls) {
|
|
687
|
+
functionCalls.push(...chunk.functionCalls);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
expect(functionCalls).toEqual([
|
|
691
|
+
{
|
|
692
|
+
id: 'call_abc',
|
|
693
|
+
name: 'get_weather',
|
|
694
|
+
args: { location: 'Paris' },
|
|
695
|
+
},
|
|
696
|
+
]);
|
|
697
|
+
});
|
|
698
|
+
it('parses Responses SSE parallel tool calls', async () => {
|
|
699
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
700
|
+
const mockResponse = {
|
|
701
|
+
ok: true,
|
|
702
|
+
body: createSseBody([
|
|
703
|
+
{
|
|
704
|
+
type: 'response.output_item.added',
|
|
705
|
+
item: {
|
|
706
|
+
type: 'function_call',
|
|
707
|
+
id: 'fc_1',
|
|
708
|
+
call_id: 'call_1',
|
|
709
|
+
name: 'tool_a',
|
|
710
|
+
arguments: '{"a":1}',
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
type: 'response.output_item.added',
|
|
715
|
+
item: {
|
|
716
|
+
type: 'function_call',
|
|
717
|
+
id: 'fc_2',
|
|
718
|
+
call_id: 'call_2',
|
|
719
|
+
name: 'tool_b',
|
|
720
|
+
arguments: '{"b":2}',
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
type: 'response.completed',
|
|
725
|
+
response: {
|
|
726
|
+
usage: { input_tokens: 1, output_tokens: 1 },
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
]),
|
|
730
|
+
};
|
|
731
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
732
|
+
const stream = await generator.generateContentStream({
|
|
733
|
+
model: 'xai/grok-4.5',
|
|
734
|
+
contents: [{ role: 'user', parts: [{ text: 'parallel' }] }],
|
|
735
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
736
|
+
const names = [];
|
|
737
|
+
for await (const chunk of stream) {
|
|
738
|
+
for (const call of chunk.functionCalls || []) {
|
|
739
|
+
if (call.name) {
|
|
740
|
+
names.push(call.name);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
expect(names.sort()).toEqual(['tool_a', 'tool_b']);
|
|
745
|
+
});
|
|
746
|
+
it('does not re-emit full text from output_text.done and finishes on message done', async () => {
|
|
747
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
748
|
+
const mockResponse = {
|
|
749
|
+
ok: true,
|
|
750
|
+
headers: {
|
|
751
|
+
get: (name) => name.toLowerCase() === 'content-type' ? 'text/event-stream' : null,
|
|
752
|
+
},
|
|
753
|
+
body: createSseBody([
|
|
754
|
+
{ type: 'response.output_text.delta', delta: 'Hola' },
|
|
755
|
+
{
|
|
756
|
+
type: 'response.output_text.done',
|
|
757
|
+
text: 'Hola',
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
type: 'response.output_item.done',
|
|
761
|
+
item: {
|
|
762
|
+
type: 'message',
|
|
763
|
+
content: [{ type: 'output_text', text: 'Hola' }],
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
type: 'response.completed',
|
|
768
|
+
response: { usage: { input_tokens: 1, output_tokens: 1 } },
|
|
769
|
+
},
|
|
770
|
+
]),
|
|
771
|
+
};
|
|
772
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
773
|
+
const stream = await generator.generateContentStream({
|
|
774
|
+
model: 'xai/grok-4.5',
|
|
775
|
+
contents: [{ role: 'user', parts: [{ text: 'hola' }] }],
|
|
776
|
+
}, 'prompt-dedupe', LlmRole.MAIN);
|
|
777
|
+
const texts = [];
|
|
778
|
+
let finishCount = 0;
|
|
779
|
+
for await (const chunk of stream) {
|
|
780
|
+
const t = chunk.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
781
|
+
if (t)
|
|
782
|
+
texts.push(t);
|
|
783
|
+
if (chunk.candidates?.[0]?.finishReason)
|
|
784
|
+
finishCount += 1;
|
|
785
|
+
}
|
|
786
|
+
expect(texts).toEqual(['Hola']);
|
|
787
|
+
expect(finishCount).toBeGreaterThanOrEqual(1);
|
|
788
|
+
});
|
|
789
|
+
it('surfaces Responses SSE mid-stream errors', async () => {
|
|
790
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'xai/grok-4.5', 'mock-keycloak-token');
|
|
791
|
+
const mockResponse = {
|
|
792
|
+
ok: true,
|
|
793
|
+
body: createSseBody([
|
|
794
|
+
{ type: 'response.output_text.delta', delta: 'partial' },
|
|
795
|
+
{
|
|
796
|
+
type: 'response.failed',
|
|
797
|
+
error: { message: 'boom mid-stream' },
|
|
798
|
+
},
|
|
799
|
+
]),
|
|
800
|
+
};
|
|
801
|
+
vi.mocked(fetch).mockResolvedValue(mockResponse);
|
|
802
|
+
const stream = await generator.generateContentStream({
|
|
803
|
+
model: 'xai/grok-4.5',
|
|
804
|
+
contents: [{ role: 'user', parts: [{ text: 'fail' }] }],
|
|
805
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
806
|
+
await expect(async () => {
|
|
807
|
+
for await (const _chunk of stream) {
|
|
808
|
+
// drain
|
|
809
|
+
}
|
|
810
|
+
}).rejects.toThrow('boom mid-stream');
|
|
811
|
+
});
|
|
812
|
+
it('ignores reasoning output items in non-stream responses', () => {
|
|
813
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'openai/gpt-5.6-terra', 'mock-keycloak-token');
|
|
814
|
+
const parsed = generator.parseResponsesPayload({
|
|
815
|
+
output: [
|
|
816
|
+
{ type: 'reasoning', id: 'r1' },
|
|
817
|
+
{
|
|
818
|
+
type: 'message',
|
|
819
|
+
content: [{ type: 'output_text', text: 'final answer' }],
|
|
820
|
+
},
|
|
821
|
+
],
|
|
822
|
+
usage: { input_tokens: 2, output_tokens: 3 },
|
|
823
|
+
});
|
|
824
|
+
expect(parsed.candidates?.[0]?.content?.parts).toEqual([
|
|
825
|
+
{ text: 'final answer' },
|
|
826
|
+
]);
|
|
827
|
+
});
|
|
828
|
+
it('supports chat_completions transport for non-default providers', async () => {
|
|
829
|
+
const generator = new OpenAiCompatibleContentGenerator(mockConfig, 'local/llama-3', 'mock-token', undefined, 'chat_completions');
|
|
830
|
+
expect(generator.endpointUrl).toBe('https://cell-api.local/api/cell/v1.1/local/v1/chat/completions');
|
|
831
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
832
|
+
ok: true,
|
|
833
|
+
json: async () => ({
|
|
834
|
+
choices: [{ message: { content: 'local ok' } }],
|
|
835
|
+
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
|
|
836
|
+
}),
|
|
837
|
+
});
|
|
838
|
+
const response = await generator.generateContent({
|
|
839
|
+
model: 'local/llama-3',
|
|
840
|
+
contents: [{ role: 'user', parts: [{ text: 'hi' }] }],
|
|
841
|
+
}, 'prompt-1', LlmRole.MAIN);
|
|
842
|
+
const body = getFetchBody();
|
|
843
|
+
expect(body).toEqual({
|
|
844
|
+
model: 'llama-3',
|
|
845
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
846
|
+
});
|
|
847
|
+
expect(response.candidates?.[0]?.content?.parts?.[0]?.text).toBe('local ok');
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
//# sourceMappingURL=openAiCompatibleContentGenerator.test.js.map
|