@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.
Files changed (118) hide show
  1. package/dist/src/adapters/IStreamAdapter.d.ts +3 -3
  2. package/dist/src/auth/precedence.d.ts +1 -1
  3. package/dist/src/auth/precedence.js +9 -4
  4. package/dist/src/auth/precedence.js.map +1 -1
  5. package/dist/src/auth/types.d.ts +4 -4
  6. package/dist/src/code_assist/codeAssist.js +8 -6
  7. package/dist/src/code_assist/codeAssist.js.map +1 -1
  8. package/dist/src/code_assist/setup.js +9 -7
  9. package/dist/src/code_assist/setup.js.map +1 -1
  10. package/dist/src/config/index.d.ts +7 -0
  11. package/dist/src/config/index.js +8 -0
  12. package/dist/src/config/index.js.map +1 -0
  13. package/dist/src/core/client.d.ts +9 -21
  14. package/dist/src/core/client.js +55 -156
  15. package/dist/src/core/client.js.map +1 -1
  16. package/dist/src/core/compression-config.d.ts +1 -1
  17. package/dist/src/core/compression-config.js +4 -5
  18. package/dist/src/core/compression-config.js.map +1 -1
  19. package/dist/src/core/coreToolScheduler.js +50 -15
  20. package/dist/src/core/coreToolScheduler.js.map +1 -1
  21. package/dist/src/core/geminiChat.d.ts +51 -2
  22. package/dist/src/core/geminiChat.js +616 -106
  23. package/dist/src/core/geminiChat.js.map +1 -1
  24. package/dist/src/core/nonInteractiveToolExecutor.js +70 -19
  25. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  26. package/dist/src/core/prompts.js +34 -26
  27. package/dist/src/core/prompts.js.map +1 -1
  28. package/dist/src/core/turn.d.ts +1 -0
  29. package/dist/src/core/turn.js +8 -6
  30. package/dist/src/core/turn.js.map +1 -1
  31. package/dist/src/index.d.ts +1 -2
  32. package/dist/src/index.js +2 -2
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/src/prompt-config/TemplateEngine.js +17 -0
  35. package/dist/src/prompt-config/TemplateEngine.js.map +1 -1
  36. package/dist/src/prompt-config/defaults/core-defaults.js +39 -32
  37. package/dist/src/prompt-config/defaults/core-defaults.js.map +1 -1
  38. package/dist/src/prompt-config/defaults/core.md +2 -0
  39. package/dist/src/prompt-config/defaults/provider-defaults.js +34 -27
  40. package/dist/src/prompt-config/defaults/provider-defaults.js.map +1 -1
  41. package/dist/src/prompt-config/defaults/providers/gemini/core.md +229 -43
  42. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/core.md +12 -0
  43. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/gemini-2-5-flash/core.md +12 -0
  44. package/dist/src/prompt-config/types.d.ts +2 -0
  45. package/dist/src/providers/BaseProvider.d.ts +32 -6
  46. package/dist/src/providers/BaseProvider.js +79 -22
  47. package/dist/src/providers/BaseProvider.js.map +1 -1
  48. package/dist/src/providers/IProvider.d.ts +9 -3
  49. package/dist/src/providers/LoggingProviderWrapper.d.ts +10 -3
  50. package/dist/src/providers/LoggingProviderWrapper.js +33 -27
  51. package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
  52. package/dist/src/providers/ProviderContentGenerator.d.ts +2 -2
  53. package/dist/src/providers/ProviderContentGenerator.js +9 -6
  54. package/dist/src/providers/ProviderContentGenerator.js.map +1 -1
  55. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +27 -21
  56. package/dist/src/providers/anthropic/AnthropicProvider.js +473 -472
  57. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  58. package/dist/src/providers/gemini/GeminiProvider.d.ts +14 -9
  59. package/dist/src/providers/gemini/GeminiProvider.js +202 -486
  60. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  61. package/dist/src/providers/openai/ConversationCache.d.ts +3 -3
  62. package/dist/src/providers/openai/IChatGenerateParams.d.ts +9 -4
  63. package/dist/src/providers/openai/OpenAIProvider.d.ts +44 -115
  64. package/dist/src/providers/openai/OpenAIProvider.js +535 -948
  65. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  66. package/dist/src/providers/openai/buildResponsesRequest.d.ts +3 -3
  67. package/dist/src/providers/openai/buildResponsesRequest.js +67 -37
  68. package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
  69. package/dist/src/providers/openai/estimateRemoteTokens.d.ts +2 -2
  70. package/dist/src/providers/openai/estimateRemoteTokens.js +21 -8
  71. package/dist/src/providers/openai/estimateRemoteTokens.js.map +1 -1
  72. package/dist/src/providers/openai/parseResponsesStream.d.ts +6 -2
  73. package/dist/src/providers/openai/parseResponsesStream.js +99 -391
  74. package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
  75. package/dist/src/providers/openai/syntheticToolResponses.d.ts +5 -5
  76. package/dist/src/providers/openai/syntheticToolResponses.js +102 -91
  77. package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
  78. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +18 -20
  79. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +250 -239
  80. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
  81. package/dist/src/providers/tokenizers/OpenAITokenizer.js +3 -3
  82. package/dist/src/providers/tokenizers/OpenAITokenizer.js.map +1 -1
  83. package/dist/src/providers/types.d.ts +1 -1
  84. package/dist/src/services/history/ContentConverters.d.ts +6 -1
  85. package/dist/src/services/history/ContentConverters.js +155 -18
  86. package/dist/src/services/history/ContentConverters.js.map +1 -1
  87. package/dist/src/services/history/HistoryService.d.ts +52 -0
  88. package/dist/src/services/history/HistoryService.js +245 -93
  89. package/dist/src/services/history/HistoryService.js.map +1 -1
  90. package/dist/src/services/history/IContent.d.ts +4 -0
  91. package/dist/src/services/history/IContent.js.map +1 -1
  92. package/dist/src/telemetry/types.d.ts +16 -4
  93. package/dist/src/telemetry/types.js.map +1 -1
  94. package/dist/src/tools/IToolFormatter.d.ts +2 -2
  95. package/dist/src/tools/ToolFormatter.d.ts +42 -4
  96. package/dist/src/tools/ToolFormatter.js +159 -37
  97. package/dist/src/tools/ToolFormatter.js.map +1 -1
  98. package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
  99. package/dist/src/tools/doubleEscapeUtils.js +241 -0
  100. package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
  101. package/dist/src/tools/read-file.js +5 -2
  102. package/dist/src/tools/read-file.js.map +1 -1
  103. package/dist/src/tools/todo-schemas.d.ts +4 -4
  104. package/dist/src/tools/write-file.js +5 -2
  105. package/dist/src/tools/write-file.js.map +1 -1
  106. package/dist/src/types/modelParams.d.ts +8 -0
  107. package/dist/src/utils/bfsFileSearch.js +2 -6
  108. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  109. package/package.json +8 -7
  110. package/dist/src/core/ContentGeneratorAdapter.d.ts +0 -37
  111. package/dist/src/core/ContentGeneratorAdapter.js +0 -58
  112. package/dist/src/core/ContentGeneratorAdapter.js.map +0 -1
  113. package/dist/src/providers/IMessage.d.ts +0 -38
  114. package/dist/src/providers/IMessage.js +0 -17
  115. package/dist/src/providers/IMessage.js.map +0 -1
  116. package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
  117. package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
  118. package/dist/src/providers/adapters/GeminiCompatibleWrapper.js.map +0 -1
@@ -4,18 +4,20 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { DebugLogger } from '../../debug/index.js';
7
- import { Config, AuthType, ContentGeneratorRole, AuthenticationRequiredError, getCoreSystemPromptAsync, createCodeAssistContentGenerator, } from '@vybestack/llxprt-code-core';
7
+ import { Config } from '../../config/config.js';
8
+ import { AuthType } from '../../core/contentGenerator.js';
9
+ import { AuthenticationRequiredError } from '../errors.js';
10
+ import { getCoreSystemPromptAsync } from '../../core/prompts.js';
11
+ import { createCodeAssistContentGenerator } from '../../code_assist/codeAssist.js';
12
+ import { Type, } from '@google/genai';
8
13
  import { BaseProvider } from '../BaseProvider.js';
9
14
  import { getSettingsService } from '../../settings/settingsServiceInstance.js';
10
15
  export class GeminiProvider extends BaseProvider {
11
16
  logger;
12
17
  authMode = 'none';
13
- geminiConfig;
14
18
  currentModel = 'gemini-2.5-pro';
15
19
  modelExplicitlySet = false;
16
- baseURL;
17
20
  modelParams;
18
- toolSchemas;
19
21
  geminiOAuthManager;
20
22
  constructor(apiKey, baseURL, config, oauthManager) {
21
23
  // Initialize base provider with auth configuration
@@ -28,11 +30,8 @@ export class GeminiProvider extends BaseProvider {
28
30
  oauthProvider: 'gemini',
29
31
  oauthManager, // Keep the manager for checking enablement
30
32
  };
31
- super(baseConfig);
33
+ super(baseConfig, config, undefined);
32
34
  this.logger = new DebugLogger('llxprt:gemini:provider');
33
- // Store Gemini-specific configuration
34
- this.geminiConfig = config;
35
- this.baseURL = baseURL;
36
35
  this.geminiOAuthManager = oauthManager;
37
36
  // Do not determine auth mode on instantiation.
38
37
  // This will be done lazily when a chat completion is requested.
@@ -111,7 +110,7 @@ export class GeminiProvider extends BaseProvider {
111
110
  return 'USE_LOGIN_WITH_GOOGLE';
112
111
  }
113
112
  // Handle case where no auth is available
114
- const authType = this.geminiConfig?.getContentGeneratorConfig()?.authType;
113
+ const authType = this.globalConfig?.getContentGeneratorConfig()?.authType;
115
114
  if (authType === AuthType.USE_NONE) {
116
115
  this.authMode = 'none';
117
116
  throw new AuthenticationRequiredError('Authentication is set to USE_NONE but no credentials are available', this.authMode, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
@@ -171,7 +170,6 @@ export class GeminiProvider extends BaseProvider {
171
170
  * Sets the config instance for reading OAuth credentials
172
171
  */
173
172
  setConfig(config) {
174
- this.geminiConfig = config;
175
173
  // Sync with config model if user hasn't explicitly set a model
176
174
  // This ensures consistency between config and provider state
177
175
  const configModel = config.getModel();
@@ -212,8 +210,9 @@ export class GeminiProvider extends BaseProvider {
212
210
  const apiKey = (await this.getAuthToken()) || process.env.GEMINI_API_KEY;
213
211
  if (apiKey) {
214
212
  try {
215
- const url = this.baseURL
216
- ? `${this.baseURL.replace(/\/$/, '')}/v1beta/models?key=${apiKey}`
213
+ const baseURL = this.getBaseURL();
214
+ const url = baseURL
215
+ ? `${baseURL.replace(/\/$/, '')}/v1beta/models?key=${apiKey}`
217
216
  : `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`;
218
217
  const response = await fetch(url, {
219
218
  method: 'GET',
@@ -260,462 +259,6 @@ export class GeminiProvider extends BaseProvider {
260
259
  },
261
260
  ];
262
261
  }
263
- async *generateChatCompletion(messages, tools, _toolFormat) {
264
- // Comprehensive debug logging
265
- this.logger.debug(() => `generateChatCompletion called with ${messages.length} messages`);
266
- this.logger.debug(() => `Messages: ${JSON.stringify(messages, null, 2)}`);
267
- this.logger.debug(() => `First message: ${messages[0] ? JSON.stringify(messages[0], null, 2) : 'NO FIRST MESSAGE'}`);
268
- this.logger.debug(() => `Tools: ${tools ? JSON.stringify(tools.map((t) => t.function.name)) : 'NO TOOLS'}`);
269
- // Lazily determine the best auth method now that it's needed.
270
- // This implements lazy OAuth triggering - OAuth is only triggered when making API calls
271
- let authToken;
272
- try {
273
- authToken = await this.determineBestAuth();
274
- }
275
- catch (error) {
276
- if (error instanceof AuthenticationRequiredError) {
277
- throw error;
278
- }
279
- throw new AuthenticationRequiredError('Failed to resolve authentication for Gemini provider', this.authMode, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
280
- }
281
- // Authentication has already been resolved by determineBestAuth()
282
- // No need for additional validation since the auth token is already obtained
283
- // Import the necessary modules dynamically to avoid circular dependencies
284
- const { GoogleGenAI } = await import('@google/genai');
285
- // Create the appropriate client based on auth mode
286
- let genAI;
287
- const httpOptions = {
288
- headers: {
289
- 'User-Agent': `LLxprt-Code/${process.env.CLI_VERSION || process.version} (${process.platform}; ${process.arch})`,
290
- },
291
- };
292
- switch (this.authMode) {
293
- case 'gemini-api-key':
294
- genAI = new GoogleGenAI({
295
- apiKey: authToken,
296
- httpOptions: this.baseURL
297
- ? {
298
- ...httpOptions,
299
- baseUrl: this.baseURL,
300
- }
301
- : httpOptions,
302
- });
303
- break;
304
- case 'vertex-ai':
305
- genAI = new GoogleGenAI({
306
- apiKey: authToken,
307
- vertexai: true,
308
- httpOptions: this.baseURL
309
- ? {
310
- ...httpOptions,
311
- baseUrl: this.baseURL,
312
- }
313
- : httpOptions,
314
- });
315
- break;
316
- case 'oauth': {
317
- // For OAuth, create a minimal config-like object if we don't have one
318
- const configForOAuth = this.geminiConfig || {
319
- getProxy: () => undefined, // OAuth only needs this from config
320
- };
321
- // For OAuth, we need to use the code assist server
322
- const contentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, configForOAuth, this.baseURL);
323
- // Convert messages to Gemini request format
324
- // Use config model in OAuth mode to ensure synchronization
325
- const oauthModel = this.modelExplicitlySet
326
- ? this.currentModel
327
- : this.geminiConfig?.getModel() || this.currentModel;
328
- // Generate systemInstruction using getCoreSystemPrompt
329
- // Get user memory from config if available
330
- const userMemory = this.geminiConfig?.getUserMemory
331
- ? this.geminiConfig.getUserMemory()
332
- : '';
333
- const systemInstruction = await getCoreSystemPromptAsync(userMemory, oauthModel);
334
- // Store tools if provided
335
- if (tools && tools.length > 0) {
336
- this.toolSchemas = this.convertToolsToGeminiFormat(tools);
337
- }
338
- // Use provided tools or stored tools
339
- let geminiTools = tools
340
- ? this.convertToolsToGeminiFormat(tools)
341
- : this.toolSchemas;
342
- // For Flash models, always include tools if available
343
- if (oauthModel.includes('flash') && !geminiTools && this.toolSchemas) {
344
- geminiTools = this.toolSchemas;
345
- }
346
- const request = {
347
- model: oauthModel,
348
- contents: this.convertMessagesToGeminiFormat(messages),
349
- systemInstruction,
350
- config: {
351
- tools: geminiTools,
352
- ...this.modelParams,
353
- },
354
- };
355
- // Use the content generator stream
356
- // PRIVACY FIX: Removed sessionId to prevent transmission to Google servers
357
- const streamResult = await contentGenerator.generateContentStream(request, 'oauth-session');
358
- // Reset global state variables after successful authentication
359
- /**
360
- * @plan PLAN-20250822-GEMINIFALLBACK.P12
361
- * @requirement REQ-003.3
362
- * @pseudocode lines 17-18, 25-26
363
- */
364
- global.__oauth_needs_code = false;
365
- global.__oauth_provider = undefined;
366
- // Convert the stream to our format
367
- for await (const response of streamResult) {
368
- // Extract text from the response
369
- const text = response.candidates?.[0]?.content?.parts
370
- ?.filter((part) => 'text' in part)
371
- ?.map((part) => part.text)
372
- ?.join('') || '';
373
- // Extract function calls from the response
374
- const functionCalls = response.candidates?.[0]?.content?.parts
375
- ?.filter((part) => 'functionCall' in part)
376
- ?.map((part) => part.functionCall) || [];
377
- // Build response message
378
- const message = {
379
- role: ContentGeneratorRole.ASSISTANT,
380
- content: text,
381
- };
382
- // Add function calls if any
383
- if (functionCalls && functionCalls.length > 0) {
384
- message.tool_calls = functionCalls.map((call) => ({
385
- id: call.id ||
386
- `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
387
- type: 'function',
388
- function: {
389
- name: call.name || 'unknown_function',
390
- arguments: JSON.stringify(call.args || {}),
391
- },
392
- }));
393
- }
394
- // Only yield if there's content or tool calls
395
- if (text || (functionCalls && functionCalls.length > 0)) {
396
- yield message;
397
- }
398
- }
399
- return;
400
- }
401
- case 'none':
402
- // For 'none' mode, use the resolved auth token
403
- genAI = new GoogleGenAI({
404
- apiKey: authToken,
405
- vertexai: this.hasVertexAICredentials(),
406
- httpOptions: this.baseURL
407
- ? {
408
- ...httpOptions,
409
- baseUrl: this.baseURL,
410
- }
411
- : httpOptions,
412
- });
413
- break;
414
- default:
415
- throw new Error(`Unsupported auth mode: ${this.authMode}`);
416
- }
417
- // Get the models interface (which is a ContentGenerator)
418
- const contentGenerator = genAI.models;
419
- // Store tools if provided
420
- if (tools && tools.length > 0) {
421
- this.toolSchemas = this.convertToolsToGeminiFormat(tools);
422
- }
423
- // Convert IMessage[] to Gemini format - do this after storing tools so priming can access them
424
- const contents = this.convertMessagesToGeminiFormat(messages);
425
- // Use provided tools or stored tools
426
- let geminiTools = tools
427
- ? this.convertToolsToGeminiFormat(tools)
428
- : this.toolSchemas;
429
- // Create the request - ContentGenerator expects model in the request
430
- // Use explicit model if set, otherwise fall back to config model
431
- const modelToUse = this.modelExplicitlySet
432
- ? this.currentModel
433
- : this.geminiConfig?.getModel() || this.currentModel;
434
- // For Flash models, always include tools if available
435
- if (modelToUse.includes('flash') && !geminiTools && this.toolSchemas) {
436
- geminiTools = this.toolSchemas;
437
- }
438
- // Generate systemInstruction using getCoreSystemPrompt
439
- // Get user memory from config if available
440
- const userMemory = this.geminiConfig?.getUserMemory
441
- ? this.geminiConfig.getUserMemory()
442
- : '';
443
- const systemInstruction = await getCoreSystemPromptAsync(userMemory, modelToUse);
444
- const request = {
445
- model: modelToUse,
446
- contents,
447
- systemInstruction,
448
- config: {
449
- tools: geminiTools,
450
- ...this.modelParams,
451
- },
452
- };
453
- // Generate content stream using the ContentGenerator interface
454
- const stream = await contentGenerator.generateContentStream(request);
455
- // Stream the response
456
- for await (const response of stream) {
457
- // Extract text from the response
458
- const text = response.candidates?.[0]?.content?.parts
459
- ?.filter((part) => 'text' in part)
460
- ?.map((part) => part.text)
461
- ?.join('') || '';
462
- // Extract function calls from the response
463
- const functionCalls = response.candidates?.[0]?.content?.parts
464
- ?.filter((part) => 'functionCall' in part)
465
- ?.map((part) => part.functionCall) || [];
466
- // Build response message
467
- const message = {
468
- role: ContentGeneratorRole.ASSISTANT,
469
- content: text || '',
470
- };
471
- // Add function calls if any
472
- if (functionCalls && functionCalls.length > 0) {
473
- message.tool_calls = functionCalls.map((call) => ({
474
- id: call.id ||
475
- `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
476
- type: 'function',
477
- function: {
478
- name: call.name || 'unknown_function',
479
- arguments: JSON.stringify(call.args || {}),
480
- },
481
- }));
482
- }
483
- // Only yield if there's content or tool calls
484
- if (text || (functionCalls && functionCalls.length > 0)) {
485
- yield message;
486
- }
487
- }
488
- }
489
- convertMessagesToGeminiFormat(messages) {
490
- const contents = [];
491
- // Enhanced tracking with more details
492
- const functionCalls = new Map();
493
- const functionResponses = new Map();
494
- for (let i = 0; i < messages.length; i++) {
495
- const msg = messages[i];
496
- // Handle tool responses - each in its own Content object
497
- if (msg.role === ContentGeneratorRole.TOOL) {
498
- if (!msg.tool_call_id) {
499
- this.logger.debug(() => `Tool response at index ${i} missing tool_call_id, skipping: ${JSON.stringify(msg)}`);
500
- continue;
501
- }
502
- functionResponses.set(msg.tool_call_id, {
503
- name: msg.tool_name || 'unknown_function',
504
- contentIndex: contents.length,
505
- messageIndex: i,
506
- });
507
- // Add each tool response as a separate content immediately
508
- contents.push({
509
- role: 'user',
510
- parts: [
511
- {
512
- functionResponse: {
513
- id: msg.tool_call_id,
514
- name: msg.tool_name || 'unknown_function',
515
- response: {
516
- output: msg.content || '',
517
- },
518
- },
519
- },
520
- ],
521
- });
522
- continue;
523
- }
524
- // For non-tool messages, convert normally
525
- const parts = [];
526
- // Check for parts first (for messages with PDF/image parts but no text content)
527
- if (msg.parts && msg.parts.length > 0) {
528
- parts.push(...msg.parts);
529
- }
530
- else if (msg.content) {
531
- // Handle PartListUnion: string | Part | Part[]
532
- // In practice, content can be PartListUnion even though IMessage types it as string
533
- const content = msg.content;
534
- if (typeof content === 'string') {
535
- // Try to parse string in case it's a stringified Part or Part[]
536
- if ((content.startsWith('{') && content.endsWith('}')) ||
537
- (content.startsWith('[') && content.endsWith(']'))) {
538
- try {
539
- const parsed = JSON.parse(content);
540
- if (Array.isArray(parsed)) {
541
- parts.push(...parsed);
542
- }
543
- else {
544
- parts.push(parsed);
545
- }
546
- }
547
- catch (_e) {
548
- // Not valid JSON, treat as text
549
- parts.push({ text: content });
550
- }
551
- }
552
- else {
553
- parts.push({ text: content });
554
- }
555
- }
556
- else if (Array.isArray(content)) {
557
- // Content is Part[]
558
- parts.push(...content);
559
- }
560
- else {
561
- // Content is a single Part
562
- parts.push(content);
563
- }
564
- }
565
- // Handle tool calls
566
- if (msg.tool_calls && msg.tool_calls.length > 0) {
567
- // Check if function calls were already added via parts
568
- const existingFunctionCallIds = new Set();
569
- if (msg.parts && msg.parts.length > 0) {
570
- for (const part of parts) {
571
- if ('functionCall' in part) {
572
- const fc = part;
573
- if (fc.functionCall.id) {
574
- existingFunctionCallIds.add(fc.functionCall.id);
575
- }
576
- }
577
- }
578
- }
579
- for (const toolCall of msg.tool_calls) {
580
- // Skip if this function call was already added via parts
581
- if (toolCall.id && existingFunctionCallIds.has(toolCall.id)) {
582
- continue;
583
- }
584
- // Ensure tool call has an ID
585
- if (!toolCall.id) {
586
- this.logger.debug(() => `Tool call at message ${i} missing ID, generating one: ${JSON.stringify(toolCall)}`);
587
- // Generate a unique ID for the function call
588
- toolCall.id = `generated_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
589
- }
590
- const partIndex = parts.length;
591
- parts.push({
592
- functionCall: {
593
- id: toolCall.id,
594
- name: toolCall.function.name,
595
- args: JSON.parse(toolCall.function.arguments),
596
- },
597
- });
598
- // Track this function call with its position
599
- functionCalls.set(toolCall.id, {
600
- name: toolCall.function.name,
601
- contentIndex: contents.length,
602
- partIndex,
603
- messageIndex: i,
604
- });
605
- }
606
- }
607
- // Map roles
608
- let role = 'user';
609
- if (msg.role === ContentGeneratorRole.ASSISTANT) {
610
- role = 'model';
611
- }
612
- else if (msg.role === ContentGeneratorRole.USER) {
613
- role = 'user';
614
- }
615
- else if (msg.role === 'system') {
616
- // Gemini doesn't have system role in contents, handle separately
617
- role = 'user';
618
- }
619
- if (parts.length > 0) {
620
- contents.push({
621
- role,
622
- parts,
623
- });
624
- }
625
- }
626
- // Validate and add missing function responses
627
- for (const [callId, callInfo] of Array.from(functionCalls.entries())) {
628
- if (!functionResponses.has(callId)) {
629
- // Create a placeholder response for missing function response
630
- this.logger.debug(() => `Function call ${callInfo.name} (id: ${callId}) has no matching response, adding placeholder`);
631
- // Add each function response as a separate content object (same as regular tool responses)
632
- contents.push({
633
- role: 'user',
634
- parts: [
635
- {
636
- functionResponse: {
637
- id: callId,
638
- name: callInfo.name,
639
- response: {
640
- output: JSON.stringify({
641
- error: 'Function call was interrupted or no response received',
642
- message: `The function "${callInfo.name}" was called but did not receive a response. This may occur if the function execution was interrupted, requires authentication, or encountered an error.`,
643
- callId,
644
- functionName: callInfo.name,
645
- }),
646
- },
647
- },
648
- },
649
- ],
650
- });
651
- // Mark this response as added
652
- functionResponses.set(callId, {
653
- name: callInfo.name,
654
- contentIndex: contents.length - 1,
655
- messageIndex: -1, // Placeholder response doesn't have original message index
656
- });
657
- }
658
- }
659
- // Final validation - count function calls and responses
660
- let totalFunctionCalls = 0;
661
- let totalFunctionResponses = 0;
662
- const callsDetail = [];
663
- const responsesDetail = [];
664
- const unmatchedCalls = new Set();
665
- const unmatchedResponses = new Set();
666
- // First pass: collect all function calls and responses with their IDs
667
- for (let i = 0; i < contents.length; i++) {
668
- const content = contents[i];
669
- for (const part of content.parts) {
670
- if ('functionCall' in part) {
671
- totalFunctionCalls++;
672
- const fc = part;
673
- callsDetail.push(`${i}: ${fc.functionCall.name} (${fc.functionCall.id})`);
674
- if (fc.functionCall.id) {
675
- unmatchedCalls.add(fc.functionCall.id);
676
- }
677
- }
678
- else if ('functionResponse' in part) {
679
- totalFunctionResponses++;
680
- const fr = part;
681
- responsesDetail.push(`${i}: ${fr.functionResponse.name} (${fr.functionResponse.id})`);
682
- if (fr.functionResponse.id) {
683
- unmatchedResponses.add(fr.functionResponse.id);
684
- }
685
- }
686
- }
687
- }
688
- // Second pass: match calls with responses
689
- for (const id of unmatchedCalls) {
690
- if (unmatchedResponses.has(id)) {
691
- unmatchedCalls.delete(id);
692
- unmatchedResponses.delete(id);
693
- }
694
- }
695
- if (totalFunctionCalls !== totalFunctionResponses) {
696
- this.logger.debug(() => `Function parts count mismatch: ${totalFunctionCalls} calls vs ${totalFunctionResponses} responses`);
697
- this.logger.debug(() => `Function calls: ${JSON.stringify(callsDetail)}`);
698
- this.logger.debug(() => `Function responses: ${JSON.stringify(responsesDetail)}`);
699
- this.logger.debug(() => `Unmatched call IDs: ${JSON.stringify(Array.from(unmatchedCalls))}`);
700
- this.logger.debug(() => `Unmatched response IDs: ${JSON.stringify(Array.from(unmatchedResponses))}`);
701
- // This is now just a warning, not an error, since we've added placeholders
702
- // The Gemini API should handle this gracefully
703
- }
704
- return contents;
705
- }
706
- convertToolsToGeminiFormat(tools) {
707
- const result = [
708
- {
709
- functionDeclarations: tools.map((tool) => ({
710
- name: tool.function.name,
711
- description: tool.function.description,
712
- parameters: tool.function.parameters,
713
- })),
714
- },
715
- ];
716
- this.logger.debug(() => `Converted tools to Gemini format: ${JSON.stringify(result, null, 2)}`);
717
- return result;
718
- }
719
262
  setApiKey(apiKey) {
720
263
  // Call base provider implementation
721
264
  super.setApiKey(apiKey);
@@ -731,8 +274,8 @@ export class GeminiProvider extends BaseProvider {
731
274
  this.clearAuthCache();
732
275
  }
733
276
  setBaseUrl(baseUrl) {
734
- // If no baseUrl is provided or it's an empty string, clear to undefined
735
- this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
277
+ // Call base provider implementation which stores in ephemeral settings
278
+ super.setBaseUrl?.(baseUrl);
736
279
  }
737
280
  /**
738
281
  * Gets the current authentication mode
@@ -796,8 +339,8 @@ export class GeminiProvider extends BaseProvider {
796
339
  this.modelExplicitlySet = true;
797
340
  // Always update config if available, not just in OAuth mode
798
341
  // This ensures the model is properly synchronized
799
- if (this.geminiConfig) {
800
- this.geminiConfig.setModel(modelId);
342
+ if (this.globalConfig) {
343
+ this.globalConfig.setModel(modelId);
801
344
  }
802
345
  }
803
346
  /**
@@ -834,6 +377,8 @@ export class GeminiProvider extends BaseProvider {
834
377
  this.currentModel = 'gemini-2.5-pro';
835
378
  }
836
379
  // Note: We don't clear config or apiKey as they might be needed
380
+ // Clear auth cache
381
+ this.clearAuthCache();
837
382
  }
838
383
  /**
839
384
  * Clear all authentication including environment variable
@@ -864,7 +409,7 @@ export class GeminiProvider extends BaseProvider {
864
409
  async invokeServerTool(toolName, params, _config) {
865
410
  if (toolName === 'web_search') {
866
411
  this.logger.debug(() => `invokeServerTool: web_search called with params: ${JSON.stringify(params)}`);
867
- this.logger.debug(() => `invokeServerTool: geminiConfig is ${this.geminiConfig ? 'set' : 'null/undefined'}`);
412
+ this.logger.debug(() => `invokeServerTool: globalConfig is ${this.globalConfig ? 'set' : 'null/undefined'}`);
868
413
  this.logger.debug(() => `invokeServerTool: current authMode is ${this.authMode}`);
869
414
  // Import the necessary modules dynamically
870
415
  const { GoogleGenAI } = await import('@google/genai');
@@ -894,10 +439,10 @@ export class GeminiProvider extends BaseProvider {
894
439
  }
895
440
  genAI = new GoogleGenAI({
896
441
  apiKey: authToken,
897
- httpOptions: this.baseURL
442
+ httpOptions: this.getBaseURL()
898
443
  ? {
899
444
  ...httpOptions,
900
- baseUrl: this.baseURL,
445
+ baseUrl: this.getBaseURL(),
901
446
  }
902
447
  : httpOptions,
903
448
  });
@@ -922,10 +467,10 @@ export class GeminiProvider extends BaseProvider {
922
467
  genAI = new GoogleGenAI({
923
468
  apiKey: authToken,
924
469
  vertexai: true,
925
- httpOptions: this.baseURL
470
+ httpOptions: this.getBaseURL()
926
471
  ? {
927
472
  ...httpOptions,
928
- baseUrl: this.baseURL,
473
+ baseUrl: this.getBaseURL(),
929
474
  }
930
475
  : httpOptions,
931
476
  });
@@ -949,11 +494,11 @@ export class GeminiProvider extends BaseProvider {
949
494
  case 'oauth': {
950
495
  try {
951
496
  this.logger.debug(() => `invokeServerTool: OAuth case - creating content generator`);
952
- // If geminiConfig is not set (e.g., when using non-Gemini provider),
497
+ // If globalConfig is not set (e.g., when using non-Gemini provider),
953
498
  // create a minimal config for OAuth
954
- let configForOAuth = this.geminiConfig;
499
+ let configForOAuth = this.globalConfig;
955
500
  if (!configForOAuth) {
956
- this.logger.debug(() => `invokeServerTool: geminiConfig is null, creating minimal config for OAuth`);
501
+ this.logger.debug(() => `invokeServerTool: globalConfig is null, creating minimal config for OAuth`);
957
502
  // Use crypto for UUID generation
958
503
  const { randomUUID } = await import('crypto');
959
504
  configForOAuth = new Config({
@@ -966,7 +511,7 @@ export class GeminiProvider extends BaseProvider {
966
511
  // The OAuth flow will handle authentication
967
512
  }
968
513
  // For OAuth, use the code assist content generator
969
- this.logger.debug(() => `invokeServerTool: calling createCodeAssistContentGenerator`);
514
+ // Note: Detailed logging is now handled by DebugLogger in codeAssist.ts with namespace llxprt:code:assist
970
515
  const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, configForOAuth);
971
516
  this.logger.debug(() => `invokeServerTool: OAuth content generator created successfully`);
972
517
  // For web search, always use gemini-2.5-flash regardless of the active model
@@ -1016,10 +561,10 @@ export class GeminiProvider extends BaseProvider {
1016
561
  case 'gemini-api-key': {
1017
562
  genAI = new GoogleGenAI({
1018
563
  apiKey: authToken,
1019
- httpOptions: this.baseURL
564
+ httpOptions: this.getBaseURL()
1020
565
  ? {
1021
566
  ...httpOptions,
1022
- baseUrl: this.baseURL,
567
+ baseUrl: this.getBaseURL(),
1023
568
  }
1024
569
  : httpOptions,
1025
570
  });
@@ -1044,10 +589,10 @@ export class GeminiProvider extends BaseProvider {
1044
589
  genAI = new GoogleGenAI({
1045
590
  apiKey: authToken,
1046
591
  vertexai: true,
1047
- httpOptions: this.baseURL
592
+ httpOptions: this.getBaseURL()
1048
593
  ? {
1049
594
  ...httpOptions,
1050
- baseUrl: this.baseURL,
595
+ baseUrl: this.getBaseURL(),
1051
596
  }
1052
597
  : httpOptions,
1053
598
  });
@@ -1070,7 +615,7 @@ export class GeminiProvider extends BaseProvider {
1070
615
  }
1071
616
  case 'oauth': {
1072
617
  // For OAuth, use the code assist content generator
1073
- const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, this.geminiConfig);
618
+ const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, this.globalConfig);
1074
619
  // For web fetch, always use gemini-2.5-flash regardless of the active model
1075
620
  const oauthRequest = {
1076
621
  model: 'gemini-2.5-flash',
@@ -1096,5 +641,176 @@ export class GeminiProvider extends BaseProvider {
1096
641
  throw new Error(`Unknown server tool: ${toolName}`);
1097
642
  }
1098
643
  }
644
+ /**
645
+ * Generate chat completion with IContent interface
646
+ * Direct implementation for Gemini API with IContent interface
647
+ */
648
+ async *generateChatCompletion(content, tools, _toolFormat) {
649
+ // Determine best auth method
650
+ const authToken = await this.determineBestAuth();
651
+ // Import necessary modules
652
+ const { GoogleGenAI } = await import('@google/genai');
653
+ // Convert IContent directly to Gemini format
654
+ const contents = [];
655
+ for (const c of content) {
656
+ if (c.speaker === 'human') {
657
+ const parts = [];
658
+ for (const block of c.blocks) {
659
+ if (block.type === 'text') {
660
+ parts.push({ text: block.text });
661
+ }
662
+ }
663
+ if (parts.length > 0) {
664
+ contents.push({ role: 'user', parts });
665
+ }
666
+ }
667
+ else if (c.speaker === 'ai') {
668
+ const parts = [];
669
+ for (const block of c.blocks) {
670
+ if (block.type === 'text') {
671
+ parts.push({ text: block.text });
672
+ }
673
+ else if (block.type === 'tool_call') {
674
+ const tc = block;
675
+ parts.push({
676
+ functionCall: {
677
+ id: tc.id,
678
+ name: tc.name,
679
+ args: tc.parameters,
680
+ },
681
+ });
682
+ }
683
+ }
684
+ if (parts.length > 0) {
685
+ contents.push({ role: 'model', parts });
686
+ }
687
+ }
688
+ else if (c.speaker === 'tool') {
689
+ const toolResponseBlock = c.blocks.find((b) => b.type === 'tool_response');
690
+ if (!toolResponseBlock) {
691
+ throw new Error('Tool content must have a tool_response block');
692
+ }
693
+ contents.push({
694
+ role: 'user',
695
+ parts: [
696
+ {
697
+ functionResponse: {
698
+ id: toolResponseBlock.callId,
699
+ name: toolResponseBlock.toolName,
700
+ response: {
701
+ output: JSON.stringify(toolResponseBlock.result),
702
+ },
703
+ },
704
+ },
705
+ ],
706
+ });
707
+ }
708
+ }
709
+ // Ensure tools have proper type: 'object' for Gemini
710
+ const geminiTools = tools
711
+ ? tools.map((toolGroup) => ({
712
+ functionDeclarations: toolGroup.functionDeclarations.map((decl) => {
713
+ let parameters = decl.parametersJsonSchema;
714
+ if (parameters &&
715
+ typeof parameters === 'object' &&
716
+ !('type' in parameters)) {
717
+ parameters = { type: Type.OBJECT, ...parameters };
718
+ }
719
+ else if (!parameters) {
720
+ parameters = { type: Type.OBJECT, properties: {} };
721
+ }
722
+ return {
723
+ name: decl.name,
724
+ description: decl.description,
725
+ parameters: parameters,
726
+ };
727
+ }),
728
+ }))
729
+ : undefined;
730
+ // Create appropriate client and generate content
731
+ const httpOptions = {
732
+ headers: {
733
+ 'User-Agent': `LLxprt-Code/${process.env.CLI_VERSION || process.version} (${process.platform}; ${process.arch})`,
734
+ },
735
+ };
736
+ let stream;
737
+ if (this.authMode === 'oauth') {
738
+ // OAuth mode
739
+ const configForOAuth = this.globalConfig || {
740
+ getProxy: () => undefined,
741
+ };
742
+ const contentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, configForOAuth, this.getBaseURL());
743
+ const userMemory = this.globalConfig?.getUserMemory
744
+ ? this.globalConfig.getUserMemory()
745
+ : '';
746
+ const systemInstruction = await getCoreSystemPromptAsync(userMemory, this.currentModel);
747
+ const request = {
748
+ model: this.currentModel,
749
+ contents,
750
+ systemInstruction,
751
+ config: {
752
+ tools: geminiTools,
753
+ ...this.modelParams,
754
+ },
755
+ };
756
+ stream = await contentGenerator.generateContentStream(request, 'oauth-session');
757
+ }
758
+ else {
759
+ // API key mode
760
+ const genAI = new GoogleGenAI({
761
+ apiKey: authToken,
762
+ vertexai: this.authMode === 'vertex-ai',
763
+ httpOptions: this.getBaseURL()
764
+ ? { ...httpOptions, baseUrl: this.getBaseURL() }
765
+ : httpOptions,
766
+ });
767
+ const contentGenerator = genAI.models;
768
+ const userMemory = this.globalConfig?.getUserMemory
769
+ ? this.globalConfig.getUserMemory()
770
+ : '';
771
+ const systemInstruction = await getCoreSystemPromptAsync(userMemory, this.currentModel);
772
+ const request = {
773
+ model: this.currentModel,
774
+ contents,
775
+ systemInstruction,
776
+ config: {
777
+ tools: geminiTools,
778
+ ...this.modelParams,
779
+ },
780
+ };
781
+ stream = await contentGenerator.generateContentStream(request);
782
+ }
783
+ // Stream responses as IContent
784
+ for await (const response of stream) {
785
+ const text = response.candidates?.[0]?.content?.parts
786
+ ?.filter((part) => 'text' in part)
787
+ ?.map((part) => part.text)
788
+ ?.join('') || '';
789
+ const functionCalls = response.candidates?.[0]?.content?.parts
790
+ ?.filter((part) => 'functionCall' in part)
791
+ ?.map((part) => part.functionCall) || [];
792
+ // Yield text if present
793
+ if (text) {
794
+ yield {
795
+ speaker: 'ai',
796
+ blocks: [{ type: 'text', text }],
797
+ };
798
+ }
799
+ // Yield tool calls if present
800
+ if (functionCalls.length > 0) {
801
+ const blocks = functionCalls.map((call) => ({
802
+ type: 'tool_call',
803
+ id: call.id ||
804
+ `call_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
805
+ name: call.name || 'unknown_function',
806
+ parameters: call.args || {},
807
+ }));
808
+ yield {
809
+ speaker: 'ai',
810
+ blocks,
811
+ };
812
+ }
813
+ }
814
+ }
1099
815
  }
1100
816
  //# sourceMappingURL=GeminiProvider.js.map