@vybestack/llxprt-code-core 0.1.23-nightly.250905.97906524 → 0.2.2
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/src/adapters/IStreamAdapter.d.ts +3 -3
- package/dist/src/auth/precedence.d.ts +1 -1
- package/dist/src/auth/precedence.js +9 -4
- package/dist/src/auth/precedence.js.map +1 -1
- package/dist/src/auth/types.d.ts +4 -4
- package/dist/src/code_assist/codeAssist.js +8 -6
- package/dist/src/code_assist/codeAssist.js.map +1 -1
- package/dist/src/code_assist/setup.js +9 -7
- package/dist/src/code_assist/setup.js.map +1 -1
- package/dist/src/config/index.d.ts +7 -0
- package/dist/src/config/index.js +8 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/core/client.d.ts +9 -21
- package/dist/src/core/client.js +55 -156
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/compression-config.d.ts +1 -1
- package/dist/src/core/compression-config.js +4 -5
- package/dist/src/core/compression-config.js.map +1 -1
- package/dist/src/core/coreToolScheduler.js +50 -15
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +51 -2
- package/dist/src/core/geminiChat.js +616 -106
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.js +70 -19
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/prompts.js +34 -26
- package/dist/src/core/prompts.js.map +1 -1
- package/dist/src/core/turn.d.ts +1 -0
- package/dist/src/core/turn.js +8 -6
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/prompt-config/TemplateEngine.js +17 -0
- package/dist/src/prompt-config/TemplateEngine.js.map +1 -1
- package/dist/src/prompt-config/defaults/core-defaults.js +39 -32
- package/dist/src/prompt-config/defaults/core-defaults.js.map +1 -1
- package/dist/src/prompt-config/defaults/core.md +2 -0
- package/dist/src/prompt-config/defaults/provider-defaults.js +34 -27
- package/dist/src/prompt-config/defaults/provider-defaults.js.map +1 -1
- package/dist/src/prompt-config/defaults/providers/gemini/core.md +229 -43
- package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/core.md +12 -0
- package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/gemini-2-5-flash/core.md +12 -0
- package/dist/src/prompt-config/types.d.ts +2 -0
- package/dist/src/providers/BaseProvider.d.ts +32 -6
- package/dist/src/providers/BaseProvider.js +79 -22
- package/dist/src/providers/BaseProvider.js.map +1 -1
- package/dist/src/providers/IProvider.d.ts +9 -3
- package/dist/src/providers/LoggingProviderWrapper.d.ts +10 -3
- package/dist/src/providers/LoggingProviderWrapper.js +33 -27
- package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
- package/dist/src/providers/ProviderContentGenerator.d.ts +2 -2
- package/dist/src/providers/ProviderContentGenerator.js +9 -6
- package/dist/src/providers/ProviderContentGenerator.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.d.ts +27 -21
- package/dist/src/providers/anthropic/AnthropicProvider.js +473 -472
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/gemini/GeminiProvider.d.ts +14 -9
- package/dist/src/providers/gemini/GeminiProvider.js +202 -486
- package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
- package/dist/src/providers/openai/ConversationCache.d.ts +3 -3
- package/dist/src/providers/openai/IChatGenerateParams.d.ts +9 -4
- package/dist/src/providers/openai/OpenAIProvider.d.ts +44 -115
- package/dist/src/providers/openai/OpenAIProvider.js +535 -948
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/providers/openai/buildResponsesRequest.d.ts +3 -3
- package/dist/src/providers/openai/buildResponsesRequest.js +67 -37
- package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
- package/dist/src/providers/openai/estimateRemoteTokens.d.ts +2 -2
- package/dist/src/providers/openai/estimateRemoteTokens.js +21 -8
- package/dist/src/providers/openai/estimateRemoteTokens.js.map +1 -1
- package/dist/src/providers/openai/parseResponsesStream.d.ts +6 -2
- package/dist/src/providers/openai/parseResponsesStream.js +99 -391
- package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
- package/dist/src/providers/openai/syntheticToolResponses.d.ts +5 -5
- package/dist/src/providers/openai/syntheticToolResponses.js +102 -91
- package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +18 -20
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +250 -239
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
- package/dist/src/providers/tokenizers/OpenAITokenizer.js +3 -3
- package/dist/src/providers/tokenizers/OpenAITokenizer.js.map +1 -1
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/services/history/ContentConverters.d.ts +6 -1
- package/dist/src/services/history/ContentConverters.js +155 -18
- package/dist/src/services/history/ContentConverters.js.map +1 -1
- package/dist/src/services/history/HistoryService.d.ts +52 -0
- package/dist/src/services/history/HistoryService.js +245 -93
- package/dist/src/services/history/HistoryService.js.map +1 -1
- package/dist/src/services/history/IContent.d.ts +4 -0
- package/dist/src/services/history/IContent.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +16 -4
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/IToolFormatter.d.ts +2 -2
- package/dist/src/tools/ToolFormatter.d.ts +42 -4
- package/dist/src/tools/ToolFormatter.js +159 -37
- package/dist/src/tools/ToolFormatter.js.map +1 -1
- package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
- package/dist/src/tools/doubleEscapeUtils.js +241 -0
- package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
- package/dist/src/tools/read-file.js +5 -2
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/todo-schemas.d.ts +4 -4
- package/dist/src/tools/write-file.js +5 -2
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/types/modelParams.d.ts +8 -0
- package/dist/src/utils/bfsFileSearch.js +2 -6
- package/dist/src/utils/bfsFileSearch.js.map +1 -1
- package/package.json +8 -7
- package/dist/src/core/ContentGeneratorAdapter.d.ts +0 -37
- package/dist/src/core/ContentGeneratorAdapter.js +0 -58
- package/dist/src/core/ContentGeneratorAdapter.js.map +0 -1
- package/dist/src/providers/IMessage.d.ts +0 -38
- package/dist/src/providers/IMessage.js +0 -17
- package/dist/src/providers/IMessage.js.map +0 -1
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.js.map +0 -1
@@ -17,6 +17,9 @@ import { ApiErrorEvent, ApiRequestEvent, ApiResponseEvent, } from '../telemetry/
|
|
17
17
|
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
|
18
18
|
import { hasCycleInSchema } from '../tools/tools.js';
|
19
19
|
import { isStructuredError } from '../utils/quotaErrorDetection.js';
|
20
|
+
import { DebugLogger } from '../debug/index.js';
|
21
|
+
import { getCompressionPrompt } from './prompts.js';
|
22
|
+
import { COMPRESSION_TOKEN_THRESHOLD, COMPRESSION_PRESERVE_THRESHOLD, } from './compression-config.js';
|
20
23
|
/**
|
21
24
|
* Custom createUserContent function that properly handles function response arrays.
|
22
25
|
* This fixes the issue where multiple function responses are incorrectly nested.
|
@@ -56,9 +59,6 @@ function createUserContentWithFunctionResponseFix(message) {
|
|
56
59
|
}
|
57
60
|
else if (Array.isArray(item)) {
|
58
61
|
// Nested array case - flatten it
|
59
|
-
if (process.env.DEBUG) {
|
60
|
-
console.log('[DEBUG] createUserContentWithFunctionResponseFix - flattening nested array:', JSON.stringify(item, null, 2));
|
61
|
-
}
|
62
62
|
for (const subItem of item) {
|
63
63
|
parts.push(subItem);
|
64
64
|
}
|
@@ -184,27 +184,59 @@ export class EmptyStreamError extends Error {
|
|
184
184
|
*/
|
185
185
|
export class GeminiChat {
|
186
186
|
config;
|
187
|
-
contentGenerator;
|
188
187
|
generationConfig;
|
189
188
|
// A promise to represent the current state of the message being sent to the
|
190
189
|
// model.
|
191
190
|
sendPromise = Promise.resolve();
|
191
|
+
// A promise to represent any ongoing compression operation
|
192
|
+
compressionPromise = null;
|
192
193
|
historyService;
|
194
|
+
logger = new DebugLogger('llxprt:gemini:chat');
|
195
|
+
// Cache the compression threshold to avoid recalculating
|
196
|
+
cachedCompressionThreshold = null;
|
193
197
|
constructor(config, contentGenerator, generationConfig = {}, initialHistory = [], historyService) {
|
194
198
|
this.config = config;
|
195
|
-
this.contentGenerator = contentGenerator;
|
196
199
|
this.generationConfig = generationConfig;
|
197
200
|
validateHistory(initialHistory);
|
198
201
|
// Use provided HistoryService or create a new one
|
199
202
|
this.historyService = historyService || new HistoryService();
|
203
|
+
this.logger.debug('GeminiChat initialized:', {
|
204
|
+
model: this.config.getModel(),
|
205
|
+
initialHistoryLength: initialHistory.length,
|
206
|
+
hasHistoryService: !!historyService,
|
207
|
+
});
|
200
208
|
// Convert and add initial history if provided
|
201
209
|
if (initialHistory.length > 0) {
|
202
210
|
const currentModel = this.config.getModel();
|
211
|
+
this.logger.debug('Adding initial history to service:', {
|
212
|
+
count: initialHistory.length,
|
213
|
+
});
|
214
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
203
215
|
for (const content of initialHistory) {
|
204
|
-
this.
|
216
|
+
const matcher = this.makePositionMatcher();
|
217
|
+
this.historyService.add(ContentConverters.toIContent(content, idGen, matcher), currentModel);
|
205
218
|
}
|
206
219
|
}
|
207
220
|
}
|
221
|
+
/**
|
222
|
+
* Create a position-based matcher for Gemini tool responses.
|
223
|
+
* It returns the next unmatched tool call from the current history.
|
224
|
+
*/
|
225
|
+
makePositionMatcher() {
|
226
|
+
const queue = this.historyService
|
227
|
+
.findUnmatchedToolCalls()
|
228
|
+
.map((b) => ({ historyId: b.id, toolName: b.name }));
|
229
|
+
// Return undefined if there are no unmatched tool calls
|
230
|
+
if (queue.length === 0) {
|
231
|
+
return undefined;
|
232
|
+
}
|
233
|
+
// Return a function that always returns a valid value (never undefined)
|
234
|
+
return () => {
|
235
|
+
const result = queue.shift();
|
236
|
+
// If queue is empty, return a fallback value
|
237
|
+
return result || { historyId: '', toolName: undefined };
|
238
|
+
};
|
239
|
+
}
|
208
240
|
_getRequestTextFromContents(contents) {
|
209
241
|
return JSON.stringify(contents);
|
210
242
|
}
|
@@ -288,28 +320,79 @@ export class GeminiChat {
|
|
288
320
|
*/
|
289
321
|
async sendMessage(params, prompt_id) {
|
290
322
|
await this.sendPromise;
|
323
|
+
// Check compression - first check if already compressing, then check if needed
|
324
|
+
if (this.compressionPromise) {
|
325
|
+
this.logger.debug('Waiting for ongoing compression to complete');
|
326
|
+
await this.compressionPromise;
|
327
|
+
}
|
328
|
+
else if (this.shouldCompress()) {
|
329
|
+
// Only check shouldCompress if not already compressing
|
330
|
+
this.logger.debug('Triggering compression before message send');
|
331
|
+
this.compressionPromise = this.performCompression(prompt_id);
|
332
|
+
await this.compressionPromise;
|
333
|
+
this.compressionPromise = null;
|
334
|
+
}
|
291
335
|
const userContent = createUserContentWithFunctionResponseFix(params.message);
|
292
|
-
//
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
336
|
+
// DO NOT add user content to history yet - use send-then-commit pattern
|
337
|
+
// Get the active provider
|
338
|
+
const provider = this.getActiveProvider();
|
339
|
+
if (!provider) {
|
340
|
+
throw new Error('No active provider configured');
|
341
|
+
}
|
342
|
+
// Check if provider supports IContent interface
|
343
|
+
if (!this.providerSupportsIContent(provider)) {
|
344
|
+
throw new Error(`Provider ${provider.name} does not support IContent interface`);
|
345
|
+
}
|
346
|
+
// Get curated history WITHOUT the new user message
|
347
|
+
const currentHistory = this.historyService.getCuratedForProvider();
|
348
|
+
// Convert user content to IContent
|
349
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
350
|
+
const matcher = this.makePositionMatcher();
|
351
|
+
const userIContent = ContentConverters.toIContent(userContent, idGen, matcher);
|
352
|
+
// Build request with history + new message
|
353
|
+
const iContents = [...currentHistory, userIContent];
|
354
|
+
this._logApiRequest(ContentConverters.toGeminiContents(iContents), this.config.getModel(), prompt_id);
|
298
355
|
const startTime = Date.now();
|
299
356
|
let response;
|
300
357
|
try {
|
301
|
-
const apiCall = () => {
|
358
|
+
const apiCall = async () => {
|
302
359
|
const modelToUse = this.config.getModel() || DEFAULT_GEMINI_FLASH_MODEL;
|
303
360
|
// Prevent Flash model calls immediately after quota error
|
304
361
|
if (this.config.getQuotaErrorOccurred() &&
|
305
362
|
modelToUse === DEFAULT_GEMINI_FLASH_MODEL) {
|
306
363
|
throw new Error('Please submit a new query to continue with the Flash model.');
|
307
364
|
}
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
365
|
+
// Get tools in the format the provider expects
|
366
|
+
const tools = this.generationConfig.tools;
|
367
|
+
// Debug log what tools we're passing to the provider
|
368
|
+
this.logger.debug(() => `[GeminiChat] Passing tools to provider.generateChatCompletion:`, {
|
369
|
+
hasTools: !!tools,
|
370
|
+
toolsLength: tools?.length,
|
371
|
+
toolsType: typeof tools,
|
372
|
+
isArray: Array.isArray(tools),
|
373
|
+
firstTool: tools?.[0],
|
374
|
+
toolNames: Array.isArray(tools)
|
375
|
+
? tools.map((t) => {
|
376
|
+
const toolObj = t;
|
377
|
+
return (toolObj.functionDeclarations?.[0]?.name ||
|
378
|
+
toolObj.name ||
|
379
|
+
'unknown');
|
380
|
+
})
|
381
|
+
: 'not-an-array',
|
382
|
+
providerName: provider.name,
|
383
|
+
});
|
384
|
+
// Call the provider directly with IContent
|
385
|
+
const streamResponse = provider.generateChatCompletion(iContents, tools);
|
386
|
+
// Collect all chunks from the stream
|
387
|
+
let lastResponse;
|
388
|
+
for await (const iContent of streamResponse) {
|
389
|
+
lastResponse = iContent;
|
390
|
+
}
|
391
|
+
if (!lastResponse) {
|
392
|
+
throw new Error('No response from provider');
|
393
|
+
}
|
394
|
+
// Convert the final IContent to GenerateContentResponse
|
395
|
+
return this.convertIContentToResponse(lastResponse);
|
313
396
|
};
|
314
397
|
response = await retryWithBackoff(apiCall, {
|
315
398
|
shouldRetry: (error) => {
|
@@ -331,51 +414,46 @@ export class GeminiChat {
|
|
331
414
|
await this._logApiResponse(durationMs, prompt_id, response.usageMetadata, JSON.stringify(response));
|
332
415
|
this.sendPromise = (async () => {
|
333
416
|
const outputContent = response.candidates?.[0]?.content;
|
334
|
-
//
|
335
|
-
|
336
|
-
//
|
417
|
+
// Send-then-commit: Now that we have a successful response, add both user and model messages
|
418
|
+
const currentModel = this.config.getModel();
|
419
|
+
// Handle AFC history or regular history
|
337
420
|
const fullAutomaticFunctionCallingHistory = response.automaticFunctionCallingHistory;
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
}
|
345
|
-
// Note: modelOutput variable no longer used directly since we handle
|
346
|
-
// responses inline below
|
347
|
-
// Remove the user content we added and handle AFC history if present
|
348
|
-
// Only do this if AFC history actually has content
|
349
|
-
if (automaticFunctionCallingHistory &&
|
350
|
-
automaticFunctionCallingHistory.length > 0) {
|
351
|
-
// Pop the user content and replace with AFC history
|
352
|
-
const allHistory = this.historyService.getAll();
|
353
|
-
const trimmedHistory = allHistory.slice(0, -1);
|
354
|
-
this.historyService.clear();
|
355
|
-
const currentModel = this.config.getModel();
|
356
|
-
for (const content of trimmedHistory) {
|
357
|
-
this.historyService.add(content, currentModel);
|
358
|
-
}
|
421
|
+
if (fullAutomaticFunctionCallingHistory &&
|
422
|
+
fullAutomaticFunctionCallingHistory.length > 0) {
|
423
|
+
// AFC case: Add the AFC history which includes the user input
|
424
|
+
const curatedHistory = this.historyService.getCurated();
|
425
|
+
const index = ContentConverters.toGeminiContents(curatedHistory).length;
|
426
|
+
const automaticFunctionCallingHistory = fullAutomaticFunctionCallingHistory.slice(index) ?? [];
|
359
427
|
for (const content of automaticFunctionCallingHistory) {
|
360
|
-
this.historyService.
|
428
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
429
|
+
const matcher = this.makePositionMatcher();
|
430
|
+
this.historyService.add(ContentConverters.toIContent(content, idGen, matcher), currentModel);
|
361
431
|
}
|
362
432
|
}
|
433
|
+
else {
|
434
|
+
// Regular case: Add user content first
|
435
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
436
|
+
const matcher = this.makePositionMatcher();
|
437
|
+
this.historyService.add(ContentConverters.toIContent(userContent, idGen, matcher), currentModel);
|
438
|
+
}
|
363
439
|
// Add model response if we have one (but filter out pure thinking responses)
|
364
440
|
if (outputContent) {
|
365
441
|
// Check if this is pure thinking content that should be filtered
|
366
442
|
if (!this.isThoughtContent(outputContent)) {
|
367
443
|
// Not pure thinking, add it
|
368
|
-
this.historyService.
|
444
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
445
|
+
this.historyService.add(ContentConverters.toIContent(outputContent, idGen), currentModel);
|
369
446
|
}
|
370
447
|
// If it's pure thinking content, don't add it to history
|
371
448
|
}
|
372
449
|
else if (response.candidates && response.candidates.length > 0) {
|
373
450
|
// We have candidates but no content - add empty model response
|
374
451
|
// This handles the case where the model returns empty content
|
375
|
-
if (!
|
376
|
-
|
452
|
+
if (!fullAutomaticFunctionCallingHistory ||
|
453
|
+
fullAutomaticFunctionCallingHistory.length === 0) {
|
377
454
|
const emptyModelContent = { role: 'model', parts: [] };
|
378
|
-
this.historyService.
|
455
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
456
|
+
this.historyService.add(ContentConverters.toIContent(emptyModelContent, idGen), currentModel);
|
379
457
|
}
|
380
458
|
}
|
381
459
|
// If no candidates at all, don't add anything (error case)
|
@@ -417,37 +495,72 @@ export class GeminiChat {
|
|
417
495
|
* ```
|
418
496
|
*/
|
419
497
|
async sendMessageStream(params, prompt_id) {
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
}
|
427
|
-
|
428
|
-
|
429
|
-
console.log('DEBUG: GeminiChat.sendMessageStream params:', JSON.stringify(params, null, 2));
|
430
|
-
console.log('DEBUG: GeminiChat.sendMessageStream params.message type:', typeof params.message);
|
431
|
-
console.log('DEBUG: GeminiChat.sendMessageStream params.message:', JSON.stringify(params.message, null, 2));
|
432
|
-
}
|
498
|
+
this.logger.debug(() => 'DEBUG [geminiChat]: ===== SEND MESSAGE STREAM START =====');
|
499
|
+
this.logger.debug(() => `DEBUG [geminiChat]: Model from config: ${this.config.getModel()}`);
|
500
|
+
this.logger.debug(() => `DEBUG [geminiChat]: Params: ${JSON.stringify(params, null, 2)}`);
|
501
|
+
this.logger.debug(() => `DEBUG [geminiChat]: Message type: ${typeof params.message}`);
|
502
|
+
this.logger.debug(() => `DEBUG [geminiChat]: Message content: ${JSON.stringify(params.message, null, 2)}`);
|
503
|
+
this.logger.debug(() => 'DEBUG: GeminiChat.sendMessageStream called');
|
504
|
+
this.logger.debug(() => `DEBUG: GeminiChat.sendMessageStream params: ${JSON.stringify(params, null, 2)}`);
|
505
|
+
this.logger.debug(() => `DEBUG: GeminiChat.sendMessageStream params.message type: ${typeof params.message}`);
|
506
|
+
this.logger.debug(() => `DEBUG: GeminiChat.sendMessageStream params.message: ${JSON.stringify(params.message, null, 2)}`);
|
433
507
|
await this.sendPromise;
|
508
|
+
// Check compression - first check if already compressing, then check if needed
|
509
|
+
if (this.compressionPromise) {
|
510
|
+
this.logger.debug('Waiting for ongoing compression to complete');
|
511
|
+
await this.compressionPromise;
|
512
|
+
}
|
513
|
+
else if (this.shouldCompress()) {
|
514
|
+
// Only check shouldCompress if not already compressing
|
515
|
+
this.logger.debug('Triggering compression before message send in stream');
|
516
|
+
this.compressionPromise = this.performCompression(prompt_id);
|
517
|
+
await this.compressionPromise;
|
518
|
+
this.compressionPromise = null;
|
519
|
+
}
|
520
|
+
// Check if this is a paired tool call/response array
|
521
|
+
let userContent;
|
522
|
+
// Quick check for paired tool call/response
|
523
|
+
const messageArray = Array.isArray(params.message) ? params.message : null;
|
524
|
+
const isPairedToolResponse = messageArray &&
|
525
|
+
messageArray.length === 2 &&
|
526
|
+
messageArray[0] &&
|
527
|
+
typeof messageArray[0] === 'object' &&
|
528
|
+
'functionCall' in messageArray[0] &&
|
529
|
+
messageArray[1] &&
|
530
|
+
typeof messageArray[1] === 'object' &&
|
531
|
+
'functionResponse' in messageArray[1];
|
532
|
+
if (isPairedToolResponse && messageArray) {
|
533
|
+
// This is a paired tool call/response from the executor
|
534
|
+
// Create separate Content objects with correct roles
|
535
|
+
userContent = [
|
536
|
+
{
|
537
|
+
role: 'model',
|
538
|
+
parts: [messageArray[0]],
|
539
|
+
},
|
540
|
+
{
|
541
|
+
role: 'user',
|
542
|
+
parts: [messageArray[1]],
|
543
|
+
},
|
544
|
+
];
|
545
|
+
}
|
546
|
+
else {
|
547
|
+
userContent = createUserContentWithFunctionResponseFix(params.message);
|
548
|
+
}
|
549
|
+
// DO NOT add anything to history here - wait until after successful send!
|
550
|
+
// Tool responses will be handled in recordHistory after the model responds
|
434
551
|
let streamDoneResolver;
|
435
552
|
const streamDonePromise = new Promise((resolve) => {
|
436
553
|
streamDoneResolver = resolve;
|
437
554
|
});
|
438
555
|
this.sendPromise = streamDonePromise;
|
439
|
-
|
440
|
-
//
|
441
|
-
|
442
|
-
// Note: requestContents is no longer needed as adapter gets history from HistoryService
|
443
|
-
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
444
|
-
const self = this;
|
445
|
-
return (async function* () {
|
556
|
+
// DO NOT add user content to history yet - wait until successful send
|
557
|
+
// This is the send-then-commit pattern to avoid orphaned tool calls
|
558
|
+
return (async function* (instance) {
|
446
559
|
try {
|
447
560
|
let lastError = new Error('Request failed after all retries.');
|
448
561
|
for (let attempt = 0; attempt <= INVALID_CONTENT_RETRY_OPTIONS.maxAttempts; attempt++) {
|
449
562
|
try {
|
450
|
-
const stream = await
|
563
|
+
const stream = await instance.makeApiCallAndProcessStream(params, prompt_id, userContent);
|
451
564
|
for await (const chunk of stream) {
|
452
565
|
yield chunk;
|
453
566
|
}
|
@@ -469,31 +582,27 @@ export class GeminiChat {
|
|
469
582
|
}
|
470
583
|
}
|
471
584
|
if (lastError) {
|
472
|
-
//
|
473
|
-
|
474
|
-
const lastIContent = allHistory[allHistory.length - 1];
|
475
|
-
const userIContent = ContentConverters.toIContent(userContent);
|
476
|
-
// Check if the last content is the user content we just added
|
477
|
-
if (lastIContent?.speaker === userIContent.speaker &&
|
478
|
-
JSON.stringify(lastIContent?.blocks) ===
|
479
|
-
JSON.stringify(userIContent.blocks)) {
|
480
|
-
// Remove the last item from history
|
481
|
-
const trimmedHistory = allHistory.slice(0, -1);
|
482
|
-
self.historyService.clear();
|
483
|
-
for (const content of trimmedHistory) {
|
484
|
-
self.historyService.add(content, self.config.getModel());
|
485
|
-
}
|
486
|
-
}
|
585
|
+
// With send-then-commit pattern, we don't add to history until success,
|
586
|
+
// so there's nothing to remove on failure
|
487
587
|
throw lastError;
|
488
588
|
}
|
489
589
|
}
|
490
590
|
finally {
|
491
591
|
streamDoneResolver();
|
492
592
|
}
|
493
|
-
})();
|
593
|
+
})(this);
|
494
594
|
}
|
495
|
-
async makeApiCallAndProcessStream(
|
496
|
-
|
595
|
+
async makeApiCallAndProcessStream(_params, _prompt_id, userContent) {
|
596
|
+
// Get the active provider
|
597
|
+
const provider = this.getActiveProvider();
|
598
|
+
if (!provider) {
|
599
|
+
throw new Error('No active provider configured');
|
600
|
+
}
|
601
|
+
// Check if provider supports IContent interface
|
602
|
+
if (!this.providerSupportsIContent(provider)) {
|
603
|
+
throw new Error(`Provider ${provider.name} does not support IContent interface`);
|
604
|
+
}
|
605
|
+
const apiCall = async () => {
|
497
606
|
const modelToUse = this.config.getModel();
|
498
607
|
const authType = this.config.getContentGeneratorConfig()?.authType;
|
499
608
|
// Prevent Flash model calls immediately after quota error (only for Gemini providers)
|
@@ -502,14 +611,37 @@ export class GeminiChat {
|
|
502
611
|
modelToUse === DEFAULT_GEMINI_FLASH_MODEL) {
|
503
612
|
throw new Error('Please submit a new query to continue with the Flash model.');
|
504
613
|
}
|
505
|
-
//
|
506
|
-
const
|
507
|
-
const
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
614
|
+
// Convert user content to IContent first so we can check if it's a tool response
|
615
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
616
|
+
const matcher = this.makePositionMatcher();
|
617
|
+
let requestContents;
|
618
|
+
if (Array.isArray(userContent)) {
|
619
|
+
// This is a paired tool call/response - convert each separately
|
620
|
+
const userIContents = userContent.map((content) => ContentConverters.toIContent(content, idGen, matcher));
|
621
|
+
// Get curated history WITHOUT the new user message (since we haven't added it yet)
|
622
|
+
const currentHistory = this.historyService.getCuratedForProvider();
|
623
|
+
// Build request with history + new messages (but don't commit to history yet)
|
624
|
+
requestContents = [...currentHistory, ...userIContents];
|
625
|
+
}
|
626
|
+
else {
|
627
|
+
const userIContent = ContentConverters.toIContent(userContent, idGen, matcher);
|
628
|
+
// Get curated history WITHOUT the new user message (since we haven't added it yet)
|
629
|
+
const currentHistory = this.historyService.getCuratedForProvider();
|
630
|
+
// Build request with history + new message (but don't commit to history yet)
|
631
|
+
requestContents = [...currentHistory, userIContent];
|
632
|
+
}
|
633
|
+
// DEBUG: Check for malformed entries
|
634
|
+
this.logger.debug(() => `[DEBUG] geminiChat IContent request (history + new message): ${JSON.stringify(requestContents, null, 2)}`);
|
635
|
+
// Get tools in the format the provider expects
|
636
|
+
const tools = this.generationConfig.tools;
|
637
|
+
// Call the provider directly with IContent
|
638
|
+
const streamResponse = provider.generateChatCompletion(requestContents, tools);
|
639
|
+
// Convert the IContent stream to GenerateContentResponse stream
|
640
|
+
return (async function* (instance) {
|
641
|
+
for await (const iContent of streamResponse) {
|
642
|
+
yield instance.convertIContentToResponse(iContent);
|
643
|
+
}
|
644
|
+
})(this);
|
513
645
|
};
|
514
646
|
const streamResponse = await retryWithBackoff(apiCall, {
|
515
647
|
shouldRetry: (error) => {
|
@@ -584,6 +716,188 @@ export class GeminiChat {
|
|
584
716
|
setTools(tools) {
|
585
717
|
this.generationConfig.tools = tools;
|
586
718
|
}
|
719
|
+
/**
|
720
|
+
* Check if compression is needed based on token count
|
721
|
+
*/
|
722
|
+
shouldCompress() {
|
723
|
+
// Calculate compression threshold only if not cached
|
724
|
+
if (this.cachedCompressionThreshold === null) {
|
725
|
+
const threshold = this.config.getEphemeralSetting('compression-threshold') ?? COMPRESSION_TOKEN_THRESHOLD;
|
726
|
+
const contextLimit = this.config.getEphemeralSetting('context-limit') ?? 60000; // Default context limit
|
727
|
+
this.cachedCompressionThreshold = threshold * contextLimit;
|
728
|
+
this.logger.debug('Calculated compression threshold:', {
|
729
|
+
threshold,
|
730
|
+
contextLimit,
|
731
|
+
compressionThreshold: this.cachedCompressionThreshold,
|
732
|
+
});
|
733
|
+
}
|
734
|
+
const currentTokens = this.historyService.getTotalTokens();
|
735
|
+
const shouldCompress = currentTokens >= this.cachedCompressionThreshold;
|
736
|
+
if (shouldCompress) {
|
737
|
+
this.logger.debug('Compression needed:', {
|
738
|
+
currentTokens,
|
739
|
+
threshold: this.cachedCompressionThreshold,
|
740
|
+
});
|
741
|
+
}
|
742
|
+
return shouldCompress;
|
743
|
+
}
|
744
|
+
/**
|
745
|
+
* Perform compression of chat history
|
746
|
+
* Made public to allow manual compression triggering
|
747
|
+
*/
|
748
|
+
async performCompression(prompt_id) {
|
749
|
+
this.logger.debug('Starting compression');
|
750
|
+
// Reset cached threshold after compression in case settings changed
|
751
|
+
this.cachedCompressionThreshold = null;
|
752
|
+
// Lock history service
|
753
|
+
this.historyService.startCompression();
|
754
|
+
try {
|
755
|
+
// Get compression split
|
756
|
+
const { toCompress, toKeep } = this.getCompressionSplit();
|
757
|
+
if (toCompress.length === 0) {
|
758
|
+
this.logger.debug('Nothing to compress');
|
759
|
+
return;
|
760
|
+
}
|
761
|
+
// Perform direct compression API call
|
762
|
+
const summary = await this.directCompressionCall(toCompress, prompt_id);
|
763
|
+
// Apply compression atomically
|
764
|
+
this.applyCompression(summary, toKeep);
|
765
|
+
this.logger.debug('Compression completed successfully');
|
766
|
+
}
|
767
|
+
catch (error) {
|
768
|
+
this.logger.error('Compression failed:', error);
|
769
|
+
throw error;
|
770
|
+
}
|
771
|
+
finally {
|
772
|
+
// Always unlock
|
773
|
+
this.historyService.endCompression();
|
774
|
+
}
|
775
|
+
}
|
776
|
+
/**
|
777
|
+
* Get the split point for compression
|
778
|
+
*/
|
779
|
+
getCompressionSplit() {
|
780
|
+
const curated = this.historyService.getCurated();
|
781
|
+
// Calculate split point (keep last 30%)
|
782
|
+
const preserveThreshold = this.config.getEphemeralSetting('compression-preserve-threshold') ?? COMPRESSION_PRESERVE_THRESHOLD;
|
783
|
+
let splitIndex = Math.floor(curated.length * (1 - preserveThreshold));
|
784
|
+
// Adjust for tool call boundaries
|
785
|
+
splitIndex = this.adjustForToolCallBoundary(curated, splitIndex);
|
786
|
+
// Never compress if too few messages
|
787
|
+
if (splitIndex < 4) {
|
788
|
+
return { toCompress: [], toKeep: curated };
|
789
|
+
}
|
790
|
+
return {
|
791
|
+
toCompress: curated.slice(0, splitIndex),
|
792
|
+
toKeep: curated.slice(splitIndex),
|
793
|
+
};
|
794
|
+
}
|
795
|
+
/**
|
796
|
+
* Adjust compression boundary to not split tool call/response pairs
|
797
|
+
*/
|
798
|
+
adjustForToolCallBoundary(history, index) {
|
799
|
+
// Don't split tool responses from their calls
|
800
|
+
while (index < history.length && history[index].speaker === 'tool') {
|
801
|
+
index++;
|
802
|
+
}
|
803
|
+
// Check if previous message has unmatched tool calls
|
804
|
+
if (index > 0) {
|
805
|
+
const prev = history[index - 1];
|
806
|
+
if (prev.speaker === 'ai') {
|
807
|
+
const toolCalls = prev.blocks.filter((b) => b.type === 'tool_call');
|
808
|
+
if (toolCalls.length > 0) {
|
809
|
+
// Check if there are matching tool responses in the kept portion
|
810
|
+
const keptHistory = history.slice(index);
|
811
|
+
const hasMatchingResponses = toolCalls.every((call) => {
|
812
|
+
const toolCall = call;
|
813
|
+
return keptHistory.some((msg) => msg.speaker === 'tool' &&
|
814
|
+
msg.blocks.some((b) => b.type === 'tool_response' &&
|
815
|
+
b.callId === toolCall.id));
|
816
|
+
});
|
817
|
+
if (!hasMatchingResponses) {
|
818
|
+
// Include the AI message with unmatched calls in the compression
|
819
|
+
return index - 1;
|
820
|
+
}
|
821
|
+
}
|
822
|
+
}
|
823
|
+
}
|
824
|
+
return index;
|
825
|
+
}
|
826
|
+
/**
|
827
|
+
* Direct API call for compression, bypassing normal message flow
|
828
|
+
*/
|
829
|
+
async directCompressionCall(historyToCompress, _prompt_id) {
|
830
|
+
const provider = this.getActiveProvider();
|
831
|
+
if (!provider || !this.providerSupportsIContent(provider)) {
|
832
|
+
throw new Error('Provider does not support compression');
|
833
|
+
}
|
834
|
+
// Build compression request with system prompt and user history
|
835
|
+
const compressionRequest = [
|
836
|
+
// Add system instruction as the first message
|
837
|
+
{
|
838
|
+
speaker: 'human',
|
839
|
+
blocks: [
|
840
|
+
{
|
841
|
+
type: 'text',
|
842
|
+
text: getCompressionPrompt(),
|
843
|
+
},
|
844
|
+
],
|
845
|
+
},
|
846
|
+
// Add the history to compress
|
847
|
+
...historyToCompress,
|
848
|
+
// Add the trigger instruction
|
849
|
+
{
|
850
|
+
speaker: 'human',
|
851
|
+
blocks: [
|
852
|
+
{
|
853
|
+
type: 'text',
|
854
|
+
text: 'First, reason in your scratchpad. Then, generate the <state_snapshot>.',
|
855
|
+
},
|
856
|
+
],
|
857
|
+
},
|
858
|
+
];
|
859
|
+
// Direct provider call without tools for compression
|
860
|
+
const stream = provider.generateChatCompletion(compressionRequest, undefined);
|
861
|
+
// Collect response
|
862
|
+
let summary = '';
|
863
|
+
for await (const chunk of stream) {
|
864
|
+
if (chunk.blocks) {
|
865
|
+
for (const block of chunk.blocks) {
|
866
|
+
if (block.type === 'text') {
|
867
|
+
summary += block.text;
|
868
|
+
}
|
869
|
+
}
|
870
|
+
}
|
871
|
+
}
|
872
|
+
return summary;
|
873
|
+
}
|
874
|
+
/**
|
875
|
+
* Apply compression results to history
|
876
|
+
*/
|
877
|
+
applyCompression(summary, toKeep) {
|
878
|
+
// Clear and rebuild history atomically
|
879
|
+
this.historyService.clear();
|
880
|
+
const currentModel = this.config.getModel();
|
881
|
+
// Add compressed summary as user message
|
882
|
+
this.historyService.add({
|
883
|
+
speaker: 'human',
|
884
|
+
blocks: [{ type: 'text', text: summary }],
|
885
|
+
}, currentModel);
|
886
|
+
// Add acknowledgment from AI
|
887
|
+
this.historyService.add({
|
888
|
+
speaker: 'ai',
|
889
|
+
blocks: [
|
890
|
+
{
|
891
|
+
type: 'text',
|
892
|
+
text: 'Got it. Thanks for the additional context!',
|
893
|
+
},
|
894
|
+
],
|
895
|
+
}, currentModel);
|
896
|
+
// Add back the kept messages
|
897
|
+
for (const content of toKeep) {
|
898
|
+
this.historyService.add(content, currentModel);
|
899
|
+
}
|
900
|
+
}
|
587
901
|
getFinalUsageMetadata(chunks) {
|
588
902
|
const lastChunkWithMetadata = chunks
|
589
903
|
.slice()
|
@@ -655,16 +969,20 @@ export class GeminiChat {
|
|
655
969
|
}
|
656
970
|
}
|
657
971
|
else {
|
658
|
-
//
|
659
|
-
const
|
660
|
-
const
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
972
|
+
// Handle both single Content and Content[] (for paired tool call/response)
|
973
|
+
const idGen = this.historyService.getIdGeneratorCallback();
|
974
|
+
const matcher = this.makePositionMatcher();
|
975
|
+
if (Array.isArray(userInput)) {
|
976
|
+
// This is a paired tool call/response from the executor
|
977
|
+
// Add each part to history
|
978
|
+
for (const content of userInput) {
|
979
|
+
const userIContent = ContentConverters.toIContent(content, idGen, matcher);
|
980
|
+
newHistoryEntries.push(userIContent);
|
981
|
+
}
|
982
|
+
}
|
983
|
+
else {
|
984
|
+
// Normal user message
|
985
|
+
const userIContent = ContentConverters.toIContent(userInput, idGen, matcher);
|
668
986
|
newHistoryEntries.push(userIContent);
|
669
987
|
}
|
670
988
|
}
|
@@ -675,6 +993,7 @@ export class GeminiChat {
|
|
675
993
|
outputContents = nonThoughtModelOutput;
|
676
994
|
}
|
677
995
|
else if (modelOutput.length === 0 &&
|
996
|
+
!Array.isArray(userInput) &&
|
678
997
|
!isFunctionResponse(userInput) &&
|
679
998
|
!automaticFunctionCallingHistory) {
|
680
999
|
// Add an empty model response if the model truly returned nothing.
|
@@ -702,7 +1021,15 @@ export class GeminiChat {
|
|
702
1021
|
this.historyService.add(entry, currentModel);
|
703
1022
|
}
|
704
1023
|
for (const content of consolidatedOutputContents) {
|
705
|
-
this
|
1024
|
+
// Check if this contains tool calls
|
1025
|
+
const hasToolCalls = content.parts?.some((part) => part && typeof part === 'object' && 'functionCall' in part);
|
1026
|
+
if (!hasToolCalls) {
|
1027
|
+
// Only add non-tool-call responses to history immediately
|
1028
|
+
// Tool calls will be added when the executor returns with the response
|
1029
|
+
this.historyService.add(ContentConverters.toIContent(content), currentModel);
|
1030
|
+
}
|
1031
|
+
// Tool calls are NOT added here - they'll come back from the executor
|
1032
|
+
// along with their responses and be added together
|
706
1033
|
}
|
707
1034
|
}
|
708
1035
|
hasTextContent(content) {
|
@@ -855,7 +1182,7 @@ export class GeminiChat {
|
|
855
1182
|
// Check for potentially problematic cyclic tools with cyclic schemas
|
856
1183
|
// and include a recommendation to remove potentially problematic tools.
|
857
1184
|
if (isStructuredError(error) && isSchemaDepthError(error.message)) {
|
858
|
-
const tools =
|
1185
|
+
const tools = this.config.getToolRegistry().getAllTools();
|
859
1186
|
const cyclicSchemaTools = [];
|
860
1187
|
for (const tool of tools) {
|
861
1188
|
if ((tool.schema.parametersJsonSchema &&
|
@@ -872,6 +1199,189 @@ export class GeminiChat {
|
|
872
1199
|
}
|
873
1200
|
}
|
874
1201
|
}
|
1202
|
+
/**
|
1203
|
+
* Convert PartListUnion (user input) to IContent format for provider/history
|
1204
|
+
*/
|
1205
|
+
convertPartListUnionToIContent(input) {
|
1206
|
+
const blocks = [];
|
1207
|
+
if (typeof input === 'string') {
|
1208
|
+
// Simple string input from user
|
1209
|
+
return {
|
1210
|
+
speaker: 'human',
|
1211
|
+
blocks: [{ type: 'text', text: input }],
|
1212
|
+
};
|
1213
|
+
}
|
1214
|
+
// Handle Part or Part[]
|
1215
|
+
const parts = Array.isArray(input) ? input : [input];
|
1216
|
+
// Check if all parts are function responses (tool responses)
|
1217
|
+
const allFunctionResponses = parts.every((part) => part && typeof part === 'object' && 'functionResponse' in part);
|
1218
|
+
if (allFunctionResponses) {
|
1219
|
+
// Tool responses - speaker is 'tool'
|
1220
|
+
for (const part of parts) {
|
1221
|
+
if (typeof part === 'object' &&
|
1222
|
+
'functionResponse' in part &&
|
1223
|
+
part.functionResponse) {
|
1224
|
+
blocks.push({
|
1225
|
+
type: 'tool_response',
|
1226
|
+
callId: part.functionResponse.id || '',
|
1227
|
+
toolName: part.functionResponse.name || '',
|
1228
|
+
result: part.functionResponse.response || {},
|
1229
|
+
error: undefined,
|
1230
|
+
});
|
1231
|
+
}
|
1232
|
+
}
|
1233
|
+
return {
|
1234
|
+
speaker: 'tool',
|
1235
|
+
blocks,
|
1236
|
+
};
|
1237
|
+
}
|
1238
|
+
// Mixed content or function calls - must be from AI
|
1239
|
+
let hasAIContent = false;
|
1240
|
+
for (const part of parts) {
|
1241
|
+
if (typeof part === 'string') {
|
1242
|
+
blocks.push({ type: 'text', text: part });
|
1243
|
+
}
|
1244
|
+
else if ('text' in part && part.text !== undefined) {
|
1245
|
+
blocks.push({ type: 'text', text: part.text });
|
1246
|
+
}
|
1247
|
+
else if ('functionCall' in part && part.functionCall) {
|
1248
|
+
hasAIContent = true; // Function calls only come from AI
|
1249
|
+
blocks.push({
|
1250
|
+
type: 'tool_call',
|
1251
|
+
id: part.functionCall.id || '',
|
1252
|
+
name: part.functionCall.name || '',
|
1253
|
+
parameters: part.functionCall.args || {},
|
1254
|
+
});
|
1255
|
+
}
|
1256
|
+
else if ('functionResponse' in part && part.functionResponse) {
|
1257
|
+
// Single function response in mixed content
|
1258
|
+
blocks.push({
|
1259
|
+
type: 'tool_response',
|
1260
|
+
callId: part.functionResponse.id || '',
|
1261
|
+
toolName: part.functionResponse.name || '',
|
1262
|
+
result: part.functionResponse.response || {},
|
1263
|
+
error: undefined,
|
1264
|
+
});
|
1265
|
+
}
|
1266
|
+
}
|
1267
|
+
// If we have function calls, it's AI content; otherwise assume human
|
1268
|
+
return {
|
1269
|
+
speaker: hasAIContent ? 'ai' : 'human',
|
1270
|
+
blocks,
|
1271
|
+
};
|
1272
|
+
}
|
1273
|
+
/**
|
1274
|
+
* Convert IContent (from provider) to GenerateContentResponse for SDK compatibility
|
1275
|
+
*/
|
1276
|
+
convertIContentToResponse(input) {
|
1277
|
+
// Convert IContent blocks to Gemini Parts
|
1278
|
+
const parts = [];
|
1279
|
+
for (const block of input.blocks) {
|
1280
|
+
switch (block.type) {
|
1281
|
+
case 'text':
|
1282
|
+
parts.push({ text: block.text });
|
1283
|
+
break;
|
1284
|
+
case 'tool_call': {
|
1285
|
+
const toolCall = block;
|
1286
|
+
parts.push({
|
1287
|
+
functionCall: {
|
1288
|
+
id: toolCall.id,
|
1289
|
+
name: toolCall.name,
|
1290
|
+
args: toolCall.parameters,
|
1291
|
+
},
|
1292
|
+
});
|
1293
|
+
break;
|
1294
|
+
}
|
1295
|
+
case 'tool_response': {
|
1296
|
+
const toolResponse = block;
|
1297
|
+
parts.push({
|
1298
|
+
functionResponse: {
|
1299
|
+
id: toolResponse.callId,
|
1300
|
+
name: toolResponse.toolName,
|
1301
|
+
response: toolResponse.result,
|
1302
|
+
},
|
1303
|
+
});
|
1304
|
+
break;
|
1305
|
+
}
|
1306
|
+
case 'thinking':
|
1307
|
+
// Include thinking blocks as thought parts
|
1308
|
+
parts.push({
|
1309
|
+
thought: true,
|
1310
|
+
text: block.thought,
|
1311
|
+
});
|
1312
|
+
break;
|
1313
|
+
default:
|
1314
|
+
// Skip unsupported block types
|
1315
|
+
break;
|
1316
|
+
}
|
1317
|
+
}
|
1318
|
+
// Build the response structure
|
1319
|
+
const response = {
|
1320
|
+
candidates: [
|
1321
|
+
{
|
1322
|
+
content: {
|
1323
|
+
role: 'model',
|
1324
|
+
parts,
|
1325
|
+
},
|
1326
|
+
},
|
1327
|
+
],
|
1328
|
+
// These are required properties that must be present
|
1329
|
+
get text() {
|
1330
|
+
return parts.find((p) => 'text' in p)?.text || '';
|
1331
|
+
},
|
1332
|
+
functionCalls: parts
|
1333
|
+
.filter((p) => 'functionCall' in p)
|
1334
|
+
.map((p) => p.functionCall),
|
1335
|
+
executableCode: undefined,
|
1336
|
+
codeExecutionResult: undefined,
|
1337
|
+
// data property will be added below
|
1338
|
+
};
|
1339
|
+
// Add data property that returns self-reference
|
1340
|
+
// Make it non-enumerable to avoid circular reference in JSON.stringify
|
1341
|
+
Object.defineProperty(response, 'data', {
|
1342
|
+
get() {
|
1343
|
+
return response;
|
1344
|
+
},
|
1345
|
+
enumerable: false, // Changed from true to false
|
1346
|
+
configurable: true,
|
1347
|
+
});
|
1348
|
+
// Add usage metadata if present
|
1349
|
+
if (input.metadata?.usage) {
|
1350
|
+
response.usageMetadata = {
|
1351
|
+
promptTokenCount: input.metadata.usage.promptTokens || 0,
|
1352
|
+
candidatesTokenCount: input.metadata.usage.completionTokens || 0,
|
1353
|
+
totalTokenCount: input.metadata.usage.totalTokens || 0,
|
1354
|
+
};
|
1355
|
+
}
|
1356
|
+
return response;
|
1357
|
+
}
|
1358
|
+
/**
|
1359
|
+
* Get the active provider from the ProviderManager via Config
|
1360
|
+
*/
|
1361
|
+
getActiveProvider() {
|
1362
|
+
const providerManager = this.config.getProviderManager();
|
1363
|
+
if (!providerManager) {
|
1364
|
+
return undefined;
|
1365
|
+
}
|
1366
|
+
try {
|
1367
|
+
return providerManager.getActiveProvider();
|
1368
|
+
}
|
1369
|
+
catch {
|
1370
|
+
// No active provider set
|
1371
|
+
return undefined;
|
1372
|
+
}
|
1373
|
+
}
|
1374
|
+
/**
|
1375
|
+
* Check if a provider supports the IContent interface
|
1376
|
+
*/
|
1377
|
+
providerSupportsIContent(provider) {
|
1378
|
+
if (!provider) {
|
1379
|
+
return false;
|
1380
|
+
}
|
1381
|
+
// Check if the provider has the IContent method
|
1382
|
+
return (typeof provider
|
1383
|
+
.generateChatCompletion === 'function');
|
1384
|
+
}
|
875
1385
|
}
|
876
1386
|
/** Visible for Testing */
|
877
1387
|
export function isSchemaDepthError(errorMessage) {
|