@machina.ai/cell-cli-core 1.49.0-rc6 → 1.51.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.
Files changed (124) hide show
  1. package/dist/docs/CHANGES.md +23 -0
  2. package/dist/docs/adr/001-openai-xai-compatible-adapters.md +75 -0
  3. package/dist/docs/changelogs/preview.md +11 -3
  4. package/dist/docs/cli/settings.md +21 -1
  5. package/dist/docs/reference/configuration.md +150 -39
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/package.json +2 -2
  10. package/dist/src/agents/agentLoader.d.ts +4 -4
  11. package/dist/src/agents/browser/browserAgentDefinition.js +2 -2
  12. package/dist/src/agents/browser/browserAgentDefinition.js.map +1 -1
  13. package/dist/src/agents/codebase-investigator.js +2 -2
  14. package/dist/src/agents/codebase-investigator.js.map +1 -1
  15. package/dist/src/agents/skill-extraction-agent.js +2 -2
  16. package/dist/src/agents/skill-extraction-agent.js.map +1 -1
  17. package/dist/src/billing/billing.js +3 -1
  18. package/dist/src/billing/billing.js.map +1 -1
  19. package/dist/src/code_assist/types.d.ts +20 -20
  20. package/dist/src/config/config.d.ts +9 -0
  21. package/dist/src/config/config.js +33 -0
  22. package/dist/src/config/config.js.map +1 -1
  23. package/dist/src/config/config.test.js +11 -1
  24. package/dist/src/config/config.test.js.map +1 -1
  25. package/dist/src/config/defaultModelConfigs.js +91 -25
  26. package/dist/src/config/defaultModelConfigs.js.map +1 -1
  27. package/dist/src/config/extensions/integrityTypes.d.ts +2 -2
  28. package/dist/src/config/localModel.test.d.ts +6 -0
  29. package/dist/src/config/localModel.test.js +114 -0
  30. package/dist/src/config/localModel.test.js.map +1 -0
  31. package/dist/src/config/models.d.ts +18 -1
  32. package/dist/src/config/models.js +90 -7
  33. package/dist/src/config/models.js.map +1 -1
  34. package/dist/src/config/models.test.js +63 -16
  35. package/dist/src/config/models.test.js.map +1 -1
  36. package/dist/src/core/baseLlmClient.js +26 -1
  37. package/dist/src/core/baseLlmClient.js.map +1 -1
  38. package/dist/src/core/contentGenerator.js +91 -1
  39. package/dist/src/core/contentGenerator.js.map +1 -1
  40. package/dist/src/core/contentGenerator.test.js +101 -0
  41. package/dist/src/core/contentGenerator.test.js.map +1 -1
  42. package/dist/src/core/geminiChat.test.js +25 -0
  43. package/dist/src/core/geminiChat.test.js.map +1 -1
  44. package/dist/src/core/openAiCompatibleContentGenerator.d.ts +156 -0
  45. package/dist/src/core/openAiCompatibleContentGenerator.js +1161 -0
  46. package/dist/src/core/openAiCompatibleContentGenerator.js.map +1 -0
  47. package/dist/src/core/openAiCompatibleContentGenerator.test.d.ts +6 -0
  48. package/dist/src/core/openAiCompatibleContentGenerator.test.js +850 -0
  49. package/dist/src/core/openAiCompatibleContentGenerator.test.js.map +1 -0
  50. package/dist/src/generated/git-commit.d.ts +2 -2
  51. package/dist/src/generated/git-commit.js +2 -2
  52. package/dist/src/index.d.ts +1 -0
  53. package/dist/src/index.js +1 -0
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/safety/built-in.js +60 -11
  56. package/dist/src/safety/built-in.js.map +1 -1
  57. package/dist/src/safety/built-in.test.js +60 -0
  58. package/dist/src/safety/built-in.test.js.map +1 -1
  59. package/dist/src/test-utils/mockWorkspaceContext.js +12 -1
  60. package/dist/src/test-utils/mockWorkspaceContext.js.map +1 -1
  61. package/dist/src/tools/at-reference-resolution.test.d.ts +6 -0
  62. package/dist/src/tools/at-reference-resolution.test.js +402 -0
  63. package/dist/src/tools/at-reference-resolution.test.js.map +1 -0
  64. package/dist/src/tools/edit.js +91 -12
  65. package/dist/src/tools/edit.js.map +1 -1
  66. package/dist/src/tools/edit.test.js +22 -1
  67. package/dist/src/tools/edit.test.js.map +1 -1
  68. package/dist/src/tools/glob.js +40 -4
  69. package/dist/src/tools/glob.js.map +1 -1
  70. package/dist/src/tools/glob.test.js +2 -1
  71. package/dist/src/tools/glob.test.js.map +1 -1
  72. package/dist/src/tools/grep.js +22 -3
  73. package/dist/src/tools/grep.js.map +1 -1
  74. package/dist/src/tools/grep.test.js +2 -2
  75. package/dist/src/tools/grep.test.js.map +1 -1
  76. package/dist/src/tools/read-file.js +16 -3
  77. package/dist/src/tools/read-file.js.map +1 -1
  78. package/dist/src/tools/read-many-files.test.js +2 -2
  79. package/dist/src/tools/read-many-files.test.js.map +1 -1
  80. package/dist/src/tools/ripGrep.js +30 -3
  81. package/dist/src/tools/ripGrep.js.map +1 -1
  82. package/dist/src/tools/ripGrep.test.js +3 -3
  83. package/dist/src/tools/ripGrep.test.js.map +1 -1
  84. package/dist/src/tools/trackerTools.test.js +1 -1
  85. package/dist/src/tools/trackerTools.test.js.map +1 -1
  86. package/dist/src/tools/write-file.js +76 -9
  87. package/dist/src/tools/write-file.js.map +1 -1
  88. package/dist/src/tools/write-file.test.js +17 -16
  89. package/dist/src/tools/write-file.test.js.map +1 -1
  90. package/dist/src/utils/editCorrector.test.js +6 -0
  91. package/dist/src/utils/editCorrector.test.js.map +1 -1
  92. package/dist/src/utils/fetch.test.js +1 -0
  93. package/dist/src/utils/fetch.test.js.map +1 -1
  94. package/dist/src/utils/historyHardening.d.ts +1 -0
  95. package/dist/src/utils/historyHardening.js +92 -14
  96. package/dist/src/utils/historyHardening.js.map +1 -1
  97. package/dist/src/utils/historyHardening.test.js +189 -1
  98. package/dist/src/utils/historyHardening.test.js.map +1 -1
  99. package/dist/src/utils/memoryImportProcessor.js +24 -3
  100. package/dist/src/utils/memoryImportProcessor.js.map +1 -1
  101. package/dist/src/utils/memoryImportProcessor.test.js +36 -0
  102. package/dist/src/utils/memoryImportProcessor.test.js.map +1 -1
  103. package/dist/src/utils/modelUtils.d.ts +8 -0
  104. package/dist/src/utils/modelUtils.js +59 -0
  105. package/dist/src/utils/modelUtils.js.map +1 -1
  106. package/dist/src/utils/modelUtils.test.js +46 -2
  107. package/dist/src/utils/modelUtils.test.js.map +1 -1
  108. package/dist/src/utils/pathCorrector.js +5 -3
  109. package/dist/src/utils/pathCorrector.js.map +1 -1
  110. package/dist/src/utils/pathCorrector.test.js +2 -1
  111. package/dist/src/utils/pathCorrector.test.js.map +1 -1
  112. package/dist/src/utils/paths.d.ts +5 -0
  113. package/dist/src/utils/paths.js +41 -0
  114. package/dist/src/utils/paths.js.map +1 -1
  115. package/dist/src/utils/paths.test.js +15 -1
  116. package/dist/src/utils/paths.test.js.map +1 -1
  117. package/dist/src/utils/workspaceContext.js +21 -0
  118. package/dist/src/utils/workspaceContext.js.map +1 -1
  119. package/dist/src/utils/workspaceContext.test.js +40 -0
  120. package/dist/src/utils/workspaceContext.test.js.map +1 -1
  121. package/dist/src/voice/whisperModelManager.js +2 -0
  122. package/dist/src/voice/whisperModelManager.js.map +1 -1
  123. package/dist/tsconfig.tsbuildinfo +1 -1
  124. package/package.json +2 -2
@@ -0,0 +1,1161 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { debugLogger } from '../utils/debugLogger.js';
7
+ const OPENAI_REASONING_EFFORTS = new Set([
8
+ 'none',
9
+ 'low',
10
+ 'medium',
11
+ 'high',
12
+ 'xhigh',
13
+ 'max',
14
+ ]);
15
+ const XAI_REASONING_EFFORTS = new Set([
16
+ 'none',
17
+ 'low',
18
+ 'medium',
19
+ 'high',
20
+ ]);
21
+ const RESPONSES_DEFAULT_PROVIDERS = new Set(['openai', 'xai']);
22
+ const RESPONSES_BODY_ALLOWLIST = new Set([
23
+ 'model',
24
+ 'input',
25
+ 'tools',
26
+ 'stream',
27
+ 'tool_choice',
28
+ 'parallel_tool_calls',
29
+ 'reasoning',
30
+ 'max_output_tokens',
31
+ 'store',
32
+ 'temperature',
33
+ 'top_p',
34
+ ]);
35
+ function isRecord(value) {
36
+ return typeof value === 'object' && value !== null;
37
+ }
38
+ function isContentLike(value) {
39
+ return isRecord(value) && ('role' in value || 'parts' in value);
40
+ }
41
+ function isPartLike(value) {
42
+ return isRecord(value);
43
+ }
44
+ function getPartText(part) {
45
+ return typeof part.text === 'string' ? part.text : undefined;
46
+ }
47
+ function getFunctionCall(part) {
48
+ if (!isRecord(part.functionCall)) {
49
+ return undefined;
50
+ }
51
+ return part.functionCall;
52
+ }
53
+ function getFunctionResponse(part) {
54
+ if (!isRecord(part.functionResponse)) {
55
+ return undefined;
56
+ }
57
+ return part.functionResponse;
58
+ }
59
+ function isToolWithFunctionDeclarations(tool) {
60
+ return (isRecord(tool) &&
61
+ 'functionDeclarations' in tool &&
62
+ Array.isArray(tool['functionDeclarations']));
63
+ }
64
+ function asJsonSchema(value) {
65
+ if (!isRecord(value)) {
66
+ return undefined;
67
+ }
68
+ return value;
69
+ }
70
+ function toGenerateContentResponse(response) {
71
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
72
+ return response;
73
+ }
74
+ function parseJsonRecord(value) {
75
+ if (!isRecord(value)) {
76
+ return {};
77
+ }
78
+ return value;
79
+ }
80
+ function mapUsageMetadata(usage) {
81
+ if (!usage) {
82
+ return undefined;
83
+ }
84
+ const promptTokenCount = usage.input_tokens ?? usage.prompt_tokens;
85
+ const candidatesTokenCount = usage.output_tokens ?? usage.completion_tokens;
86
+ const reasoningTokens = usage.output_tokens_details?.reasoning_tokens;
87
+ const totalTokenCount = usage.total_tokens ??
88
+ (promptTokenCount !== undefined || candidatesTokenCount !== undefined
89
+ ? (promptTokenCount ?? 0) +
90
+ (candidatesTokenCount ?? 0) +
91
+ (reasoningTokens ?? 0)
92
+ : undefined);
93
+ return {
94
+ promptTokenCount,
95
+ candidatesTokenCount,
96
+ totalTokenCount,
97
+ thoughtsTokenCount: reasoningTokens,
98
+ };
99
+ }
100
+ function mapFinishReason(finishReasonRaw) {
101
+ if (!finishReasonRaw) {
102
+ return undefined;
103
+ }
104
+ if (finishReasonRaw === 'stop') {
105
+ return 'STOP';
106
+ }
107
+ if (finishReasonRaw === 'length') {
108
+ return 'MAX_TOKENS';
109
+ }
110
+ if (finishReasonRaw === 'tool_calls' || finishReasonRaw === 'function_call') {
111
+ return 'STOP';
112
+ }
113
+ return finishReasonRaw.toUpperCase();
114
+ }
115
+ function shouldOmitReasoning(modelName) {
116
+ return /^(grok-4-1-fast|grok-4-fast)/i.test(modelName);
117
+ }
118
+ function clampReasoningEffort(provider, effort) {
119
+ if (provider === 'xai') {
120
+ if (effort === 'xhigh' || effort === 'max') {
121
+ return 'high';
122
+ }
123
+ if (!XAI_REASONING_EFFORTS.has(effort)) {
124
+ return 'medium';
125
+ }
126
+ return effort;
127
+ }
128
+ if (!OPENAI_REASONING_EFFORTS.has(effort)) {
129
+ return 'medium';
130
+ }
131
+ return effort;
132
+ }
133
+ function resolveReasoningEffortFromRequest(request) {
134
+ const thinkingConfig = request.config?.thinkingConfig;
135
+ if (!thinkingConfig || !isRecord(thinkingConfig)) {
136
+ return undefined;
137
+ }
138
+ const level = thinkingConfig['thinkingLevel'];
139
+ if (typeof level === 'string') {
140
+ const normalized = level.toLowerCase();
141
+ if (normalized === 'minimal' || normalized === 'none') {
142
+ return 'none';
143
+ }
144
+ if (normalized === 'low') {
145
+ return 'low';
146
+ }
147
+ if (normalized === 'medium') {
148
+ return 'medium';
149
+ }
150
+ if (normalized === 'high') {
151
+ return 'high';
152
+ }
153
+ if (normalized === 'xhigh' || normalized === 'max') {
154
+ return normalized;
155
+ }
156
+ }
157
+ const budget = thinkingConfig['thinkingBudget'];
158
+ if (typeof budget === 'number') {
159
+ if (budget <= 0) {
160
+ return 'none';
161
+ }
162
+ if (budget < 1024) {
163
+ return 'low';
164
+ }
165
+ if (budget < 8192) {
166
+ return 'medium';
167
+ }
168
+ return 'high';
169
+ }
170
+ return undefined;
171
+ }
172
+ function parseArgumentsObject(raw) {
173
+ try {
174
+ const parsed = JSON.parse(raw || '{}');
175
+ if (isRecord(parsed)) {
176
+ return parsed;
177
+ }
178
+ }
179
+ catch {
180
+ // ignore invalid JSON arguments
181
+ }
182
+ return {};
183
+ }
184
+ /**
185
+ * Stateless compatibility adapter for OpenAI-compatible APIs hosted via the
186
+ * unified Keycloak proxy. Uses Responses API for openai/xai by default and
187
+ * chat/completions for other OpenAI-compatible backends.
188
+ */
189
+ export class OpenAiCompatibleContentGenerator {
190
+ config;
191
+ token;
192
+ sessionId;
193
+ provider;
194
+ modelName;
195
+ endpointUrl;
196
+ transportMode;
197
+ localModelApiKey;
198
+ constructor(config, model, token, sessionId, transportMode) {
199
+ this.config = config;
200
+ this.token = token;
201
+ this.sessionId = sessionId;
202
+ const parts = model.split('/');
203
+ if (parts.length === 2 && parts[0].length > 0 && parts[1].length > 0) {
204
+ this.provider = parts[0];
205
+ this.modelName = parts[1];
206
+ }
207
+ else {
208
+ throw new Error(`Invalid model format for OpenAI-compatible adapter: ${model}`);
209
+ }
210
+ this.transportMode =
211
+ transportMode ?? this.resolveTransportMode(this.provider, this.config);
212
+ const localModelUrl = this.config.getLocalModelUrl?.();
213
+ if (localModelUrl) {
214
+ const cleanBaseUrl = localModelUrl.replace(/\/+$/, '');
215
+ this.endpointUrl = `${cleanBaseUrl}/chat/completions`;
216
+ this.transportMode = 'chat_completions';
217
+ this.localModelApiKey = this.config.getLocalModelApiKey?.();
218
+ }
219
+ else {
220
+ const agentApiUrl = this.config.getAgentApiUrl();
221
+ if (!agentApiUrl) {
222
+ throw new Error('Agent API URL is not configured.');
223
+ }
224
+ const cleanBaseUrl = agentApiUrl.replace(/\/+$/, '');
225
+ this.endpointUrl = this.buildEndpointUrl(cleanBaseUrl, this.provider, this.transportMode);
226
+ }
227
+ }
228
+ /**
229
+ * Resolve transport mode from provider defaults and optional config override.
230
+ * Config may expose getOpenAiTransportMode(): 'responses' | 'chat_completions'.
231
+ */
232
+ resolveTransportMode(provider, config) {
233
+ const maybeGetter = config.getOpenAiTransportMode;
234
+ const configured = typeof maybeGetter === 'function' ? maybeGetter.call(config) : undefined;
235
+ if (configured === 'responses' || configured === 'chat_completions') {
236
+ return configured;
237
+ }
238
+ return RESPONSES_DEFAULT_PROVIDERS.has(provider)
239
+ ? 'responses'
240
+ : 'chat_completions';
241
+ }
242
+ buildEndpointUrl(cleanBaseUrl, provider, transportMode) {
243
+ const path = transportMode === 'responses' ? 'responses' : 'chat/completions';
244
+ return `${cleanBaseUrl}/cell/v1.1/${provider}/v1/${path}`;
245
+ }
246
+ mapMessages(request) {
247
+ const messages = [];
248
+ const sysInstruction = request.config?.systemInstruction;
249
+ if (sysInstruction) {
250
+ let sysText = '';
251
+ if (typeof sysInstruction === 'string') {
252
+ sysText = sysInstruction;
253
+ }
254
+ else if (isRecord(sysInstruction) && 'parts' in sysInstruction) {
255
+ const parts = Array.isArray(sysInstruction['parts'])
256
+ ? sysInstruction['parts']
257
+ : [];
258
+ sysText = parts
259
+ .filter(isPartLike)
260
+ .map((p) => getPartText(p) || '')
261
+ .filter(Boolean)
262
+ .join('\n');
263
+ }
264
+ if (sysText) {
265
+ messages.push({ role: 'system', content: sysText });
266
+ }
267
+ }
268
+ let toolCallCounter = 0;
269
+ const activeMappings = {};
270
+ const rawContents = request.contents || [];
271
+ const contents = Array.isArray(rawContents) ? rawContents : [rawContents];
272
+ const intermediateMessages = [];
273
+ for (const rawContent of contents) {
274
+ if (!isContentLike(rawContent)) {
275
+ continue;
276
+ }
277
+ const content = rawContent;
278
+ const parts = Array.isArray(content.parts)
279
+ ? content.parts.filter(isPartLike)
280
+ : [];
281
+ if (content.role === 'model') {
282
+ const textParts = parts
283
+ .map((p) => getPartText(p))
284
+ .filter((text) => Boolean(text));
285
+ const textContent = textParts.join('\n');
286
+ const functionCallParts = parts
287
+ .map((p) => getFunctionCall(p))
288
+ .filter((fc) => Boolean(fc));
289
+ const toolCalls = functionCallParts.map((fc) => {
290
+ let toolCallId = fc.id || `call_${toolCallCounter++}`;
291
+ const functionName = fc.name || 'unknown';
292
+ const prefix = `${functionName}__`;
293
+ if (toolCallId.startsWith(prefix)) {
294
+ toolCallId = toolCallId.slice(prefix.length);
295
+ }
296
+ activeMappings[functionName] = toolCallId;
297
+ return {
298
+ id: toolCallId,
299
+ type: 'function',
300
+ function: {
301
+ name: functionName,
302
+ arguments: JSON.stringify(fc.args || {}),
303
+ },
304
+ };
305
+ });
306
+ const assistantMsg = {
307
+ role: 'assistant',
308
+ content: textContent || '',
309
+ };
310
+ if (toolCalls.length > 0) {
311
+ assistantMsg.tool_calls = toolCalls;
312
+ }
313
+ intermediateMessages.push(assistantMsg);
314
+ }
315
+ else {
316
+ const textParts = parts
317
+ .map((p) => getPartText(p))
318
+ .filter((text) => Boolean(text));
319
+ const textContent = textParts.join('\n');
320
+ const functionResponseParts = parts
321
+ .map((p) => getFunctionResponse(p))
322
+ .filter((fr) => Boolean(fr));
323
+ if (textContent || functionResponseParts.length === 0) {
324
+ intermediateMessages.push({
325
+ role: 'user',
326
+ content: textContent || '',
327
+ });
328
+ }
329
+ for (const fr of functionResponseParts) {
330
+ const functionName = fr.name || 'unknown';
331
+ let resolvedId = fr.id || activeMappings[functionName] || 'call_unknown';
332
+ const prefix = `${functionName}__`;
333
+ if (resolvedId.startsWith(prefix)) {
334
+ resolvedId = resolvedId.slice(prefix.length);
335
+ }
336
+ intermediateMessages.push({
337
+ role: 'tool',
338
+ content: JSON.stringify(fr.response || {}),
339
+ tool_call_id: resolvedId,
340
+ });
341
+ }
342
+ }
343
+ }
344
+ // Consolidate/merge consecutive messages of the same role
345
+ for (const msg of intermediateMessages) {
346
+ if (messages.length === 0) {
347
+ messages.push(msg);
348
+ continue;
349
+ }
350
+ const lastMsg = messages[messages.length - 1];
351
+ if (msg.role === lastMsg.role &&
352
+ (msg.role === 'user' ||
353
+ (msg.role === 'assistant' && !msg.tool_calls && !lastMsg.tool_calls))) {
354
+ lastMsg.content =
355
+ (lastMsg.content ? lastMsg.content + '\n\n' : '') +
356
+ (msg.content || '');
357
+ }
358
+ else {
359
+ messages.push(msg);
360
+ }
361
+ }
362
+ return messages;
363
+ }
364
+ /**
365
+ * Map chat-style messages to Responses API input items.
366
+ * Assistant tool_calls become function_call items; tool results become
367
+ * function_call_output items. Roles are preserved as input messages.
368
+ */
369
+ mapMessagesToResponsesInput(messages) {
370
+ const input = [];
371
+ for (const msg of messages) {
372
+ if (msg.role === 'tool') {
373
+ input.push({
374
+ type: 'function_call_output',
375
+ call_id: msg.tool_call_id || 'call_unknown',
376
+ output: msg.content || '',
377
+ });
378
+ continue;
379
+ }
380
+ if (msg.role === 'assistant' &&
381
+ msg.tool_calls &&
382
+ msg.tool_calls.length > 0) {
383
+ if (msg.content) {
384
+ input.push({
385
+ role: 'assistant',
386
+ content: msg.content,
387
+ });
388
+ }
389
+ for (const tc of msg.tool_calls) {
390
+ input.push({
391
+ type: 'function_call',
392
+ call_id: tc.id,
393
+ name: tc.function.name,
394
+ arguments: tc.function.arguments,
395
+ });
396
+ }
397
+ continue;
398
+ }
399
+ input.push({
400
+ role: msg.role,
401
+ content: msg.content || '',
402
+ });
403
+ }
404
+ return input;
405
+ }
406
+ mapSchema(schema) {
407
+ if (!schema || !isRecord(schema)) {
408
+ return undefined;
409
+ }
410
+ const mapped = { ...schema };
411
+ if (typeof mapped.type === 'string') {
412
+ mapped.type = mapped.type.toLowerCase();
413
+ }
414
+ if (mapped.properties && isRecord(mapped.properties)) {
415
+ const newProps = {};
416
+ for (const [key, val] of Object.entries(mapped.properties)) {
417
+ const nested = this.mapSchema(val);
418
+ if (nested) {
419
+ newProps[key] = nested;
420
+ }
421
+ }
422
+ mapped.properties = newProps;
423
+ }
424
+ if (mapped.items) {
425
+ mapped.items = this.mapSchema(mapped.items);
426
+ }
427
+ return mapped;
428
+ }
429
+ mapChatTools(request) {
430
+ const geminiTools = request.config?.tools || [];
431
+ const tools = [];
432
+ for (const t of geminiTools) {
433
+ if (!t || typeof t !== 'object' || !isToolWithFunctionDeclarations(t)) {
434
+ continue;
435
+ }
436
+ for (const fd of t.functionDeclarations) {
437
+ const rawParameters = fd.parameters ||
438
+ (isRecord(fd) ? asJsonSchema(fd['parametersJsonSchema']) : undefined);
439
+ const parameters = this.mapSchema(rawParameters) || {
440
+ type: 'object',
441
+ properties: {},
442
+ };
443
+ tools.push({
444
+ type: 'function',
445
+ function: {
446
+ name: fd.name || 'unknown',
447
+ description: fd.description,
448
+ parameters,
449
+ },
450
+ });
451
+ }
452
+ }
453
+ return tools.length > 0 ? tools : undefined;
454
+ }
455
+ /**
456
+ * Transform Gemini tools into Responses API internally-tagged function tools.
457
+ * strict is always false to preserve non-strict tool calling behavior.
458
+ */
459
+ mapResponsesTools(request) {
460
+ const chatTools = this.mapChatTools(request);
461
+ if (!chatTools) {
462
+ return undefined;
463
+ }
464
+ return chatTools.map((tool) => ({
465
+ type: 'function',
466
+ name: tool.function.name,
467
+ description: tool.function.description,
468
+ parameters: tool.function.parameters,
469
+ strict: false,
470
+ }));
471
+ }
472
+ buildHeaders() {
473
+ const headers = {
474
+ 'Content-Type': 'application/json',
475
+ };
476
+ const isLocal = !!this.config.getLocalModelUrl?.();
477
+ const resolvedToken = isLocal ? this.localModelApiKey : this.token;
478
+ if (resolvedToken) {
479
+ headers['Authorization'] = `Bearer ${resolvedToken}`;
480
+ }
481
+ const finalSessionId = this.sessionId ||
482
+ (typeof this.config.getSessionId === 'function'
483
+ ? this.config.getSessionId()
484
+ : undefined);
485
+ if (finalSessionId && !isLocal) {
486
+ headers['sessionId'] = finalSessionId;
487
+ }
488
+ return headers;
489
+ }
490
+ resolveEndpoint(request) {
491
+ let modelName = this.modelName;
492
+ let provider = this.provider;
493
+ let transportMode = this.transportMode;
494
+ let endpointUrl = this.endpointUrl;
495
+ if (request.model && request.model.includes('/')) {
496
+ const parts = request.model.split('/');
497
+ if (parts.length === 2 && parts[0].length > 0 && parts[1].length > 0) {
498
+ provider = parts[0];
499
+ modelName = parts[1];
500
+ transportMode = this.resolveTransportMode(provider, this.config);
501
+ const localModelUrl = this.config.getLocalModelUrl?.();
502
+ if (localModelUrl) {
503
+ const cleanBaseUrl = localModelUrl.replace(/\/+$/, '');
504
+ endpointUrl = `${cleanBaseUrl}/chat/completions`;
505
+ transportMode = 'chat_completions';
506
+ }
507
+ else {
508
+ const agentApiUrl = this.config.getAgentApiUrl();
509
+ if (agentApiUrl) {
510
+ const cleanBaseUrl = agentApiUrl.replace(/\/+$/, '');
511
+ endpointUrl = this.buildEndpointUrl(cleanBaseUrl, provider, transportMode);
512
+ }
513
+ }
514
+ }
515
+ }
516
+ return { modelName, endpointUrl, provider, transportMode };
517
+ }
518
+ /**
519
+ * Build a Responses API request body from an allowlist of fields.
520
+ * Always sets store:false and never includes previous_response_id.
521
+ */
522
+ buildResponsesRequestBody(params) {
523
+ const { modelName, provider, messages, request } = params;
524
+ const input = this.mapMessagesToResponsesInput(messages);
525
+ const tools = this.mapResponsesTools(request);
526
+ const { stream } = params;
527
+ const candidate = {
528
+ model: modelName,
529
+ input,
530
+ store: false,
531
+ };
532
+ // stream:true is required for SSE. Never send stream_options —
533
+ // Responses rejects stream_options.include_usage.
534
+ if (stream) {
535
+ candidate['stream'] = true;
536
+ }
537
+ if (tools) {
538
+ candidate['tools'] = tools;
539
+ }
540
+ if (!shouldOmitReasoning(modelName)) {
541
+ const effort = resolveReasoningEffortFromRequest(request);
542
+ if (effort) {
543
+ candidate['reasoning'] = {
544
+ effort: clampReasoningEffort(provider, effort),
545
+ };
546
+ }
547
+ }
548
+ const maxOutputTokens = request.config?.maxOutputTokens;
549
+ if (typeof maxOutputTokens === 'number') {
550
+ candidate['max_output_tokens'] = maxOutputTokens;
551
+ }
552
+ const temperature = request.config?.temperature;
553
+ if (typeof temperature === 'number') {
554
+ candidate['temperature'] = temperature;
555
+ }
556
+ // gpt-5.6* (and possibly other OpenAI reasoning models) reject top_p on
557
+ // /v1/responses. Keep it only for non-openai providers that still accept it.
558
+ const topP = request.config?.topP;
559
+ if (typeof topP === 'number' && provider !== 'openai') {
560
+ candidate['top_p'] = topP;
561
+ }
562
+ // Allowlist filter — drop unsupported params and log them.
563
+ // Also drop stream_options/include_usage even if injected by a proxy.
564
+ const body = {};
565
+ for (const [key, value] of Object.entries(candidate)) {
566
+ if (RESPONSES_BODY_ALLOWLIST.has(key) &&
567
+ !key.includes('stream_options') &&
568
+ !key.includes('include_usage')) {
569
+ body[key] = value;
570
+ }
571
+ else {
572
+ debugLogger.debug(`OpenAI Responses transport dropped unsupported param: ${key}`);
573
+ }
574
+ }
575
+ // Hard guarantees for ZDR posture and Responses compatibility.
576
+ body['store'] = false;
577
+ delete body['previous_response_id'];
578
+ delete body['stream_options'];
579
+ return body;
580
+ }
581
+ buildChatCompletionsRequestBody(params) {
582
+ const { modelName, messages, request, stream } = params;
583
+ const tools = this.mapChatTools(request);
584
+ const body = {
585
+ model: modelName,
586
+ messages,
587
+ };
588
+ if (stream) {
589
+ body['stream'] = true;
590
+ }
591
+ if (tools) {
592
+ body['tools'] = tools;
593
+ }
594
+ return body;
595
+ }
596
+ parseResponsesPayload(data) {
597
+ const parts = [];
598
+ const fnCalls = [];
599
+ for (const item of data.output || []) {
600
+ if (item.type === 'reasoning') {
601
+ // Ignore reasoning items — do not render or persist.
602
+ continue;
603
+ }
604
+ if (item.type === 'message') {
605
+ const contentParts = Array.isArray(item.content) ? item.content : [];
606
+ for (const contentPart of contentParts) {
607
+ if (contentPart.type === 'output_text' &&
608
+ typeof contentPart.text === 'string' &&
609
+ contentPart.text.length > 0) {
610
+ parts.push({ text: contentPart.text });
611
+ }
612
+ }
613
+ continue;
614
+ }
615
+ if (item.type === 'function_call') {
616
+ const args = parseArgumentsObject(item.arguments);
617
+ const fnCall = {
618
+ id: item.call_id || item.id,
619
+ name: item.name,
620
+ args,
621
+ };
622
+ fnCalls.push(fnCall);
623
+ parts.push({ functionCall: fnCall });
624
+ }
625
+ }
626
+ // Non-stream Responses are complete in one payload. Always set
627
+ // finishReason so GeminiChat does not throw NO_FINISH_REASON and retry
628
+ // (which previously caused OpenAI answers to appear 3-4 times in the UI).
629
+ return toGenerateContentResponse({
630
+ candidates: [
631
+ {
632
+ content: {
633
+ parts,
634
+ role: 'model',
635
+ },
636
+ finishReason: 'STOP',
637
+ },
638
+ ],
639
+ functionCalls: fnCalls.length > 0 ? fnCalls : undefined,
640
+ usageMetadata: mapUsageMetadata(data.usage),
641
+ });
642
+ }
643
+ parseChatCompletionsPayload(data) {
644
+ const parts = [];
645
+ const choice = data.choices?.[0];
646
+ if (choice?.message?.content) {
647
+ parts.push({ text: choice.message.content });
648
+ }
649
+ const fnCalls = [];
650
+ if (choice?.message?.tool_calls) {
651
+ for (const tc of choice.message.tool_calls) {
652
+ if (tc.type === 'function' || tc.function) {
653
+ const args = parseArgumentsObject(tc.function?.arguments);
654
+ const fnCall = {
655
+ id: tc.id,
656
+ name: tc.function?.name,
657
+ args,
658
+ };
659
+ fnCalls.push(fnCall);
660
+ parts.push({
661
+ functionCall: fnCall,
662
+ });
663
+ }
664
+ }
665
+ }
666
+ return toGenerateContentResponse({
667
+ candidates: [
668
+ {
669
+ content: {
670
+ parts,
671
+ role: 'model',
672
+ },
673
+ finishReason: mapFinishReason(choice?.finish_reason) || 'STOP',
674
+ },
675
+ ],
676
+ functionCalls: fnCalls.length > 0 ? fnCalls : undefined,
677
+ usageMetadata: mapUsageMetadata(data.usage),
678
+ });
679
+ }
680
+ /**
681
+ * Generates content.
682
+ */
683
+ async generateContent(request, _userPromptId, _role) {
684
+ const headers = this.buildHeaders();
685
+ const messages = this.mapMessages(request);
686
+ const { modelName, endpointUrl, provider, transportMode } = this.resolveEndpoint(request);
687
+ const body = transportMode === 'responses'
688
+ ? this.buildResponsesRequestBody({
689
+ modelName,
690
+ provider,
691
+ messages,
692
+ request,
693
+ })
694
+ : this.buildChatCompletionsRequestBody({
695
+ modelName,
696
+ messages,
697
+ request,
698
+ });
699
+ const response = await fetch(endpointUrl, {
700
+ method: 'POST',
701
+ headers,
702
+ body: JSON.stringify(body),
703
+ });
704
+ if (!response.ok) {
705
+ const errText = await response.text().catch(() => '');
706
+ throw new Error(`OpenAI-compatible request failed: ${response.statusText} - ${errText}`);
707
+ }
708
+ const raw = parseJsonRecord(await response.json());
709
+ if (transportMode === 'responses') {
710
+ return this.parseResponsesPayload(raw);
711
+ }
712
+ return this.parseChatCompletionsPayload(raw);
713
+ }
714
+ /**
715
+ * Generates content stream.
716
+ */
717
+ async generateContentStream(request, _userPromptId, _role) {
718
+ return this.makeStream(request);
719
+ }
720
+ async *makeStream(request) {
721
+ const headers = this.buildHeaders();
722
+ const messages = this.mapMessages(request);
723
+ const { modelName, endpointUrl, provider, transportMode } = this.resolveEndpoint(request);
724
+ // OpenAI Responses via Cell gateway rejects stream:true because the
725
+ // gateway injects stream_options.include_usage (unsupported there).
726
+ // Use non-stream Responses for openai and emit a single chunk.
727
+ // xAI Responses streaming works and keeps true SSE.
728
+ const useOpenAiNonStreamResponses = transportMode === 'responses' && provider === 'openai';
729
+ const body = transportMode === 'responses'
730
+ ? this.buildResponsesRequestBody({
731
+ modelName,
732
+ provider,
733
+ messages,
734
+ request,
735
+ stream: !useOpenAiNonStreamResponses,
736
+ })
737
+ : this.buildChatCompletionsRequestBody({
738
+ modelName,
739
+ messages,
740
+ request,
741
+ stream: true,
742
+ });
743
+ const response = await fetch(endpointUrl, {
744
+ method: 'POST',
745
+ headers,
746
+ body: JSON.stringify(body),
747
+ });
748
+ if (!response.ok) {
749
+ const errText = await response.text().catch(() => '');
750
+ throw new Error(`OpenAI-compatible stream request failed: ${response.statusText} - ${errText}`);
751
+ }
752
+ if (useOpenAiNonStreamResponses) {
753
+ const raw = parseJsonRecord(await response.json());
754
+ yield this.parseResponsesPayload(raw);
755
+ return;
756
+ }
757
+ if (!response.body) {
758
+ throw new Error('Response body is empty');
759
+ }
760
+ // Some proxies return a full JSON body even when stream was requested.
761
+ const contentType = response.headers?.get?.('content-type') || '';
762
+ if (transportMode === 'responses' &&
763
+ contentType.includes('application/json') &&
764
+ !contentType.includes('text/event-stream')) {
765
+ const raw = parseJsonRecord(await response.json());
766
+ yield this.parseResponsesPayload(raw);
767
+ return;
768
+ }
769
+ if (transportMode === 'responses') {
770
+ yield* this.parseResponsesStream(response.body);
771
+ return;
772
+ }
773
+ yield* this.parseChatCompletionsStream(response.body);
774
+ }
775
+ /**
776
+ * Parse Responses API SSE semantic events into Gemini-compatible chunks.
777
+ */
778
+ async *parseResponsesStream(bodyStream) {
779
+ const decoder = new TextDecoder('utf-8');
780
+ let buffer = '';
781
+ const accumulatedToolCalls = {};
782
+ let yieldedToolCalls = false;
783
+ let streamError;
784
+ const reader = bodyStream.getReader();
785
+ while (true) {
786
+ const { done, value: chunk } = await reader.read();
787
+ if (done) {
788
+ break;
789
+ }
790
+ const bytes = chunk instanceof Uint8Array
791
+ ? chunk
792
+ : new TextEncoder().encode(String(chunk));
793
+ buffer += decoder.decode(bytes, { stream: true });
794
+ const lines = buffer.split('\n');
795
+ buffer = lines.pop() || '';
796
+ /* eslint-disable no-restricted-syntax */
797
+ for (const line of lines) {
798
+ const trimmed = line.trim();
799
+ if (!trimmed || trimmed.startsWith('event:')) {
800
+ // Event name is redundant when payload carries `type`.
801
+ continue;
802
+ }
803
+ if (!trimmed.startsWith('data:')) {
804
+ continue;
805
+ }
806
+ const dataContent = trimmed.slice(trimmed.indexOf(':') + 1).trim();
807
+ if (!dataContent || dataContent === '[DONE]') {
808
+ continue;
809
+ }
810
+ let parsed;
811
+ try {
812
+ parsed = parseJsonRecord(JSON.parse(dataContent));
813
+ }
814
+ catch {
815
+ continue;
816
+ }
817
+ const eventType = typeof parsed['type'] === 'string' ? parsed['type'] : '';
818
+ if (eventType === 'response.failed' ||
819
+ eventType === 'error' ||
820
+ parsed['error']) {
821
+ const errObj = isRecord(parsed['error']) ? parsed['error'] : parsed;
822
+ streamError =
823
+ (typeof errObj['message'] === 'string' && errObj['message']) ||
824
+ 'OpenAI Responses stream failed';
825
+ throw new Error(streamError);
826
+ }
827
+ if (eventType === 'response.output_text.delta') {
828
+ const delta = typeof parsed['delta'] === 'string' ? parsed['delta'] : '';
829
+ if (delta) {
830
+ yield toGenerateContentResponse({
831
+ candidates: [
832
+ {
833
+ content: {
834
+ parts: [{ text: delta }],
835
+ role: 'model',
836
+ },
837
+ },
838
+ ],
839
+ });
840
+ }
841
+ continue;
842
+ }
843
+ // Full text is already streamed via deltas — never re-emit it from
844
+ // .done events (that would duplicate the answer in the UI).
845
+ if (eventType === 'response.output_text.done' ||
846
+ eventType === 'response.content_part.done') {
847
+ continue;
848
+ }
849
+ if (eventType === 'response.output_item.added') {
850
+ const item = isRecord(parsed['item']) ? parsed['item'] : undefined;
851
+ if (item && item['type'] === 'function_call') {
852
+ const key = (typeof item['id'] === 'string' && item['id']) ||
853
+ (typeof item['call_id'] === 'string' && item['call_id']) ||
854
+ `item_${Object.keys(accumulatedToolCalls).length}`;
855
+ accumulatedToolCalls[key] = {
856
+ id: (typeof item['call_id'] === 'string' && item['call_id']) || key,
857
+ name: typeof item['name'] === 'string' ? item['name'] : '',
858
+ arguments: typeof item['arguments'] === 'string' ? item['arguments'] : '',
859
+ };
860
+ }
861
+ continue;
862
+ }
863
+ if (eventType === 'response.function_call_arguments.delta' ||
864
+ eventType === 'response.function_call_arguments.done') {
865
+ const itemId = (typeof parsed['item_id'] === 'string' && parsed['item_id']) ||
866
+ (typeof parsed['output_index'] === 'number'
867
+ ? String(parsed['output_index'])
868
+ : undefined);
869
+ const callId = typeof parsed['call_id'] === 'string'
870
+ ? parsed['call_id']
871
+ : undefined;
872
+ const key = itemId ||
873
+ callId ||
874
+ Object.keys(accumulatedToolCalls).slice(-1)[0] ||
875
+ '0';
876
+ if (!accumulatedToolCalls[key]) {
877
+ accumulatedToolCalls[key] = {
878
+ id: callId || key,
879
+ name: '',
880
+ arguments: '',
881
+ };
882
+ }
883
+ if (callId) {
884
+ accumulatedToolCalls[key].id = callId;
885
+ }
886
+ if (eventType === 'response.function_call_arguments.delta') {
887
+ const delta = typeof parsed['delta'] === 'string' ? parsed['delta'] : '';
888
+ accumulatedToolCalls[key].arguments += delta;
889
+ }
890
+ else if (typeof parsed['arguments'] === 'string') {
891
+ accumulatedToolCalls[key].arguments = parsed['arguments'];
892
+ }
893
+ continue;
894
+ }
895
+ if (eventType === 'response.output_item.done') {
896
+ const item = isRecord(parsed['item']) ? parsed['item'] : undefined;
897
+ if (item && item['type'] === 'function_call') {
898
+ const key = (typeof item['id'] === 'string' && item['id']) ||
899
+ (typeof item['call_id'] === 'string' && item['call_id']) ||
900
+ Object.keys(accumulatedToolCalls).slice(-1)[0] ||
901
+ '0';
902
+ if (!accumulatedToolCalls[key]) {
903
+ accumulatedToolCalls[key] = {
904
+ id: (typeof item['call_id'] === 'string' && item['call_id']) ||
905
+ key,
906
+ name: typeof item['name'] === 'string' ? item['name'] : '',
907
+ arguments: '',
908
+ };
909
+ }
910
+ if (typeof item['call_id'] === 'string') {
911
+ accumulatedToolCalls[key].id = item['call_id'];
912
+ }
913
+ if (typeof item['name'] === 'string') {
914
+ accumulatedToolCalls[key].name = item['name'];
915
+ }
916
+ if (typeof item['arguments'] === 'string') {
917
+ accumulatedToolCalls[key].arguments = item['arguments'];
918
+ }
919
+ continue;
920
+ }
921
+ // Message completed: signal finish without re-emitting text.
922
+ // This reduces NO_FINISH_REASON retries if response.completed is lost.
923
+ if (item &&
924
+ item['type'] === 'message' &&
925
+ Object.keys(accumulatedToolCalls).length === 0) {
926
+ yield toGenerateContentResponse({
927
+ candidates: [
928
+ {
929
+ finishReason: 'STOP',
930
+ },
931
+ ],
932
+ });
933
+ }
934
+ continue;
935
+ }
936
+ if (eventType === 'response.completed') {
937
+ const responseObj = isRecord(parsed['response'])
938
+ ? parsed['response']
939
+ : parsed;
940
+ const usage = isRecord(responseObj['usage'])
941
+ ? responseObj['usage']
942
+ : undefined;
943
+ const chunkFunctionCalls = [];
944
+ const parts = [];
945
+ if (Object.keys(accumulatedToolCalls).length > 0) {
946
+ yieldedToolCalls = true;
947
+ for (const acc of Object.values(accumulatedToolCalls)) {
948
+ if (!acc?.name)
949
+ continue;
950
+ const args = parseArgumentsObject(acc.arguments);
951
+ const fnCall = {
952
+ id: acc.id,
953
+ name: acc.name,
954
+ args,
955
+ };
956
+ chunkFunctionCalls.push(fnCall);
957
+ parts.push({ functionCall: fnCall });
958
+ }
959
+ }
960
+ yield toGenerateContentResponse({
961
+ candidates: [
962
+ {
963
+ content: parts.length > 0
964
+ ? {
965
+ parts,
966
+ role: 'model',
967
+ }
968
+ : undefined,
969
+ finishReason: 'STOP',
970
+ },
971
+ ],
972
+ functionCalls: chunkFunctionCalls.length > 0 ? chunkFunctionCalls : undefined,
973
+ usageMetadata: mapUsageMetadata(usage),
974
+ });
975
+ }
976
+ // Unknown event types are ignored for forward compatibility.
977
+ }
978
+ /* eslint-enable no-restricted-syntax */
979
+ }
980
+ if (Object.keys(accumulatedToolCalls).length > 0 && !yieldedToolCalls) {
981
+ const finalFunctionCalls = [];
982
+ const finalParts = [];
983
+ for (const acc of Object.values(accumulatedToolCalls)) {
984
+ if (!acc?.name)
985
+ continue;
986
+ const args = parseArgumentsObject(acc.arguments);
987
+ const fnCall = {
988
+ id: acc.id,
989
+ name: acc.name,
990
+ args,
991
+ };
992
+ finalFunctionCalls.push(fnCall);
993
+ finalParts.push({ functionCall: fnCall });
994
+ }
995
+ if (finalFunctionCalls.length > 0) {
996
+ yield toGenerateContentResponse({
997
+ candidates: [
998
+ {
999
+ content: {
1000
+ parts: finalParts,
1001
+ role: 'model',
1002
+ },
1003
+ finishReason: 'STOP',
1004
+ },
1005
+ ],
1006
+ functionCalls: finalFunctionCalls,
1007
+ });
1008
+ }
1009
+ }
1010
+ }
1011
+ async *parseChatCompletionsStream(bodyStream) {
1012
+ const decoder = new TextDecoder('utf-8');
1013
+ let buffer = '';
1014
+ const accumulatedToolCalls = {};
1015
+ let yieldedToolCalls = false;
1016
+ const reader = bodyStream.getReader();
1017
+ while (true) {
1018
+ const { done, value: chunk } = await reader.read();
1019
+ if (done) {
1020
+ break;
1021
+ }
1022
+ const bytes = chunk instanceof Uint8Array
1023
+ ? chunk
1024
+ : new TextEncoder().encode(String(chunk));
1025
+ buffer += decoder.decode(bytes, { stream: true });
1026
+ const lines = buffer.split('\n');
1027
+ buffer = lines.pop() || '';
1028
+ for (const line of lines) {
1029
+ const trimmed = line.trim();
1030
+ if (!trimmed)
1031
+ continue;
1032
+ if (trimmed.startsWith('data: ')) {
1033
+ const dataContent = trimmed.slice(6).trim();
1034
+ if (dataContent === '[DONE]') {
1035
+ break;
1036
+ }
1037
+ try {
1038
+ const parsed = parseJsonRecord(JSON.parse(dataContent));
1039
+ const choice = parsed.choices?.[0];
1040
+ const text = choice?.delta?.content || '';
1041
+ const toolCalls = choice?.delta?.tool_calls;
1042
+ const finishReason = mapFinishReason(choice?.finish_reason);
1043
+ const parts = [];
1044
+ if (text) {
1045
+ parts.push({ text });
1046
+ }
1047
+ if (toolCalls) {
1048
+ for (const tc of toolCalls) {
1049
+ const idx = tc.index ?? 0;
1050
+ if (!accumulatedToolCalls[idx]) {
1051
+ accumulatedToolCalls[idx] = {
1052
+ id: tc.id || '',
1053
+ name: tc.function?.name || '',
1054
+ arguments: '',
1055
+ };
1056
+ }
1057
+ if (tc.id) {
1058
+ accumulatedToolCalls[idx].id = tc.id;
1059
+ }
1060
+ if (tc.function?.name) {
1061
+ accumulatedToolCalls[idx].name = tc.function.name;
1062
+ }
1063
+ if (tc.function?.arguments) {
1064
+ accumulatedToolCalls[idx].arguments += tc.function.arguments;
1065
+ }
1066
+ }
1067
+ }
1068
+ const chunkFunctionCalls = [];
1069
+ const isFinalChunk = !!finishReason;
1070
+ if (isFinalChunk && Object.keys(accumulatedToolCalls).length > 0) {
1071
+ yieldedToolCalls = true;
1072
+ for (const acc of Object.values(accumulatedToolCalls)) {
1073
+ if (acc && acc.name) {
1074
+ const args = parseArgumentsObject(acc.arguments);
1075
+ const fnCall = {
1076
+ id: acc.id,
1077
+ name: acc.name,
1078
+ args,
1079
+ };
1080
+ chunkFunctionCalls.push(fnCall);
1081
+ parts.push({
1082
+ functionCall: fnCall,
1083
+ });
1084
+ }
1085
+ }
1086
+ }
1087
+ if (parts.length > 0 ||
1088
+ finishReason ||
1089
+ chunkFunctionCalls.length > 0) {
1090
+ yield toGenerateContentResponse({
1091
+ candidates: [
1092
+ {
1093
+ content: parts.length > 0
1094
+ ? {
1095
+ parts,
1096
+ role: 'model',
1097
+ }
1098
+ : undefined,
1099
+ finishReason,
1100
+ },
1101
+ ],
1102
+ functionCalls: chunkFunctionCalls.length > 0
1103
+ ? chunkFunctionCalls
1104
+ : undefined,
1105
+ usageMetadata: mapUsageMetadata(parsed.usage),
1106
+ });
1107
+ }
1108
+ }
1109
+ catch {
1110
+ // ignore malformed SSE chunks
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ if (Object.keys(accumulatedToolCalls).length > 0 && !yieldedToolCalls) {
1116
+ const finalFunctionCalls = [];
1117
+ const finalParts = [];
1118
+ for (const acc of Object.values(accumulatedToolCalls)) {
1119
+ if (acc && acc.name) {
1120
+ const args = parseArgumentsObject(acc.arguments);
1121
+ const fnCall = {
1122
+ id: acc.id,
1123
+ name: acc.name,
1124
+ args,
1125
+ };
1126
+ finalFunctionCalls.push(fnCall);
1127
+ finalParts.push({
1128
+ functionCall: fnCall,
1129
+ });
1130
+ }
1131
+ }
1132
+ if (finalFunctionCalls.length > 0) {
1133
+ yield toGenerateContentResponse({
1134
+ candidates: [
1135
+ {
1136
+ content: {
1137
+ parts: finalParts,
1138
+ role: 'model',
1139
+ },
1140
+ finishReason: 'STOP',
1141
+ },
1142
+ ],
1143
+ functionCalls: finalFunctionCalls,
1144
+ });
1145
+ }
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Counts tokens.
1150
+ */
1151
+ async countTokens(_request) {
1152
+ return { totalTokens: 0 };
1153
+ }
1154
+ /**
1155
+ * Embeds content.
1156
+ */
1157
+ async embedContent(_request) {
1158
+ return { embeddings: [] };
1159
+ }
1160
+ }
1161
+ //# sourceMappingURL=openAiCompatibleContentGenerator.js.map