@vybestack/llxprt-code-core 0.1.23 → 0.2.2-nightly.250908.fb8099b7

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 (149) hide show
  1. package/README.md +21 -17
  2. package/dist/src/adapters/IStreamAdapter.d.ts +3 -3
  3. package/dist/src/auth/oauth-errors.d.ts +173 -0
  4. package/dist/src/auth/oauth-errors.js +461 -0
  5. package/dist/src/auth/oauth-errors.js.map +1 -0
  6. package/dist/src/auth/precedence.d.ts +1 -5
  7. package/dist/src/auth/precedence.js +28 -48
  8. package/dist/src/auth/precedence.js.map +1 -1
  9. package/dist/src/auth/token-store.js +2 -2
  10. package/dist/src/auth/token-store.js.map +1 -1
  11. package/dist/src/auth/types.d.ts +4 -4
  12. package/dist/src/code_assist/codeAssist.js +19 -6
  13. package/dist/src/code_assist/codeAssist.js.map +1 -1
  14. package/dist/src/code_assist/oauth2.d.ts +7 -0
  15. package/dist/src/code_assist/oauth2.js +82 -32
  16. package/dist/src/code_assist/oauth2.js.map +1 -1
  17. package/dist/src/code_assist/server.js +15 -4
  18. package/dist/src/code_assist/server.js.map +1 -1
  19. package/dist/src/code_assist/setup.js +9 -0
  20. package/dist/src/code_assist/setup.js.map +1 -1
  21. package/dist/src/config/index.d.ts +7 -0
  22. package/dist/src/config/index.js +8 -0
  23. package/dist/src/config/index.js.map +1 -0
  24. package/dist/src/core/client.d.ts +15 -20
  25. package/dist/src/core/client.js +98 -124
  26. package/dist/src/core/client.js.map +1 -1
  27. package/dist/src/core/compression-config.d.ts +10 -0
  28. package/dist/src/core/compression-config.js +17 -0
  29. package/dist/src/core/compression-config.js.map +1 -0
  30. package/dist/src/core/coreToolScheduler.js +50 -15
  31. package/dist/src/core/coreToolScheduler.js.map +1 -1
  32. package/dist/src/core/geminiChat.d.ts +68 -9
  33. package/dist/src/core/geminiChat.js +940 -405
  34. package/dist/src/core/geminiChat.js.map +1 -1
  35. package/dist/src/core/nonInteractiveToolExecutor.js +70 -19
  36. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  37. package/dist/src/core/prompts.js +35 -25
  38. package/dist/src/core/prompts.js.map +1 -1
  39. package/dist/src/core/turn.d.ts +1 -0
  40. package/dist/src/core/turn.js +8 -6
  41. package/dist/src/core/turn.js.map +1 -1
  42. package/dist/src/ide/ide-client.d.ts +1 -1
  43. package/dist/src/ide/ide-client.js +12 -6
  44. package/dist/src/ide/ide-client.js.map +1 -1
  45. package/dist/src/index.d.ts +4 -2
  46. package/dist/src/index.js +5 -2
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/prompt-config/TemplateEngine.js +17 -0
  49. package/dist/src/prompt-config/TemplateEngine.js.map +1 -1
  50. package/dist/src/prompt-config/defaults/core-defaults.js +39 -32
  51. package/dist/src/prompt-config/defaults/core-defaults.js.map +1 -1
  52. package/dist/src/prompt-config/defaults/core.md +2 -0
  53. package/dist/src/prompt-config/defaults/provider-defaults.js +34 -27
  54. package/dist/src/prompt-config/defaults/provider-defaults.js.map +1 -1
  55. package/dist/src/prompt-config/defaults/providers/gemini/core.md +270 -0
  56. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/core.md +12 -0
  57. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/gemini-2-5-flash/core.md +12 -0
  58. package/dist/src/prompt-config/types.d.ts +2 -0
  59. package/dist/src/providers/BaseProvider.d.ts +39 -13
  60. package/dist/src/providers/BaseProvider.js +102 -28
  61. package/dist/src/providers/BaseProvider.js.map +1 -1
  62. package/dist/src/providers/IProvider.d.ts +17 -3
  63. package/dist/src/providers/LoggingProviderWrapper.d.ts +10 -3
  64. package/dist/src/providers/LoggingProviderWrapper.js +33 -27
  65. package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
  66. package/dist/src/providers/ProviderContentGenerator.d.ts +2 -2
  67. package/dist/src/providers/ProviderContentGenerator.js +9 -6
  68. package/dist/src/providers/ProviderContentGenerator.js.map +1 -1
  69. package/dist/src/providers/ProviderManager.d.ts +4 -0
  70. package/dist/src/providers/ProviderManager.js +6 -0
  71. package/dist/src/providers/ProviderManager.js.map +1 -1
  72. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +34 -21
  73. package/dist/src/providers/anthropic/AnthropicProvider.js +505 -492
  74. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  75. package/dist/src/providers/gemini/GeminiProvider.d.ts +23 -9
  76. package/dist/src/providers/gemini/GeminiProvider.js +344 -515
  77. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  78. package/dist/src/providers/openai/ConversationCache.d.ts +3 -3
  79. package/dist/src/providers/openai/IChatGenerateParams.d.ts +9 -4
  80. package/dist/src/providers/openai/OpenAIProvider.d.ts +46 -96
  81. package/dist/src/providers/openai/OpenAIProvider.js +532 -1393
  82. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  83. package/dist/src/providers/openai/buildResponsesRequest.d.ts +3 -3
  84. package/dist/src/providers/openai/buildResponsesRequest.js +67 -37
  85. package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
  86. package/dist/src/providers/openai/estimateRemoteTokens.d.ts +2 -2
  87. package/dist/src/providers/openai/estimateRemoteTokens.js +21 -8
  88. package/dist/src/providers/openai/estimateRemoteTokens.js.map +1 -1
  89. package/dist/src/providers/openai/parseResponsesStream.d.ts +6 -2
  90. package/dist/src/providers/openai/parseResponsesStream.js +99 -391
  91. package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
  92. package/dist/src/providers/openai/syntheticToolResponses.d.ts +5 -5
  93. package/dist/src/providers/openai/syntheticToolResponses.js +102 -91
  94. package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
  95. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +89 -0
  96. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +451 -0
  97. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -0
  98. package/dist/src/providers/openai-responses/index.d.ts +1 -0
  99. package/dist/src/providers/openai-responses/index.js +2 -0
  100. package/dist/src/providers/openai-responses/index.js.map +1 -0
  101. package/dist/src/providers/tokenizers/OpenAITokenizer.js +3 -3
  102. package/dist/src/providers/tokenizers/OpenAITokenizer.js.map +1 -1
  103. package/dist/src/providers/types.d.ts +1 -1
  104. package/dist/src/services/ClipboardService.d.ts +19 -0
  105. package/dist/src/services/ClipboardService.js +66 -0
  106. package/dist/src/services/ClipboardService.js.map +1 -0
  107. package/dist/src/services/history/ContentConverters.d.ts +43 -0
  108. package/dist/src/services/history/ContentConverters.js +325 -0
  109. package/dist/src/services/history/ContentConverters.js.map +1 -0
  110. package/dist/src/{providers/IMessage.d.ts → services/history/HistoryEvents.d.ts} +16 -22
  111. package/dist/src/{providers/IMessage.js → services/history/HistoryEvents.js} +1 -1
  112. package/dist/src/services/history/HistoryEvents.js.map +1 -0
  113. package/dist/src/services/history/HistoryService.d.ts +220 -0
  114. package/dist/src/services/history/HistoryService.js +673 -0
  115. package/dist/src/services/history/HistoryService.js.map +1 -0
  116. package/dist/src/services/history/IContent.d.ts +183 -0
  117. package/dist/src/services/history/IContent.js +104 -0
  118. package/dist/src/services/history/IContent.js.map +1 -0
  119. package/dist/src/services/index.d.ts +1 -0
  120. package/dist/src/services/index.js +1 -0
  121. package/dist/src/services/index.js.map +1 -1
  122. package/dist/src/telemetry/types.d.ts +16 -4
  123. package/dist/src/telemetry/types.js.map +1 -1
  124. package/dist/src/tools/IToolFormatter.d.ts +2 -2
  125. package/dist/src/tools/ToolFormatter.d.ts +42 -4
  126. package/dist/src/tools/ToolFormatter.js +159 -37
  127. package/dist/src/tools/ToolFormatter.js.map +1 -1
  128. package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
  129. package/dist/src/tools/doubleEscapeUtils.js +241 -0
  130. package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
  131. package/dist/src/tools/read-file.d.ts +6 -1
  132. package/dist/src/tools/read-file.js +25 -11
  133. package/dist/src/tools/read-file.js.map +1 -1
  134. package/dist/src/tools/todo-schemas.d.ts +4 -4
  135. package/dist/src/tools/tools.js +13 -0
  136. package/dist/src/tools/tools.js.map +1 -1
  137. package/dist/src/tools/write-file.d.ts +6 -1
  138. package/dist/src/tools/write-file.js +48 -26
  139. package/dist/src/tools/write-file.js.map +1 -1
  140. package/dist/src/types/modelParams.d.ts +8 -0
  141. package/dist/src/utils/bfsFileSearch.js +2 -6
  142. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  143. package/dist/src/utils/schemaValidator.js +16 -1
  144. package/dist/src/utils/schemaValidator.js.map +1 -1
  145. package/package.json +8 -7
  146. package/dist/src/providers/IMessage.js.map +0 -1
  147. package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
  148. package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
  149. 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 { 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
@@ -23,17 +25,13 @@ export class GeminiProvider extends BaseProvider {
23
25
  name: 'gemini',
24
26
  apiKey,
25
27
  baseURL,
26
- cliKey: apiKey, // CLI --key argument
27
28
  envKeyNames: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
28
29
  isOAuthEnabled: false, // OAuth enablement will be checked dynamically
29
30
  oauthProvider: 'gemini',
30
31
  oauthManager, // Keep the manager for checking enablement
31
32
  };
32
- super(baseConfig);
33
+ super(baseConfig, config, undefined);
33
34
  this.logger = new DebugLogger('llxprt:gemini:provider');
34
- // Store Gemini-specific configuration
35
- this.geminiConfig = config;
36
- this.baseURL = baseURL;
37
35
  this.geminiOAuthManager = oauthManager;
38
36
  // Do not determine auth mode on instantiation.
39
37
  // This will be done lazily when a chat completion is requested.
@@ -59,38 +57,76 @@ export class GeminiProvider extends BaseProvider {
59
57
  async determineBestAuth() {
60
58
  // Re-check OAuth enablement state before determining auth
61
59
  this.updateOAuthState();
62
- // Use the base provider's auth precedence resolution
60
+ // First check if we have Gemini-specific credentials
61
+ if (this.hasVertexAICredentials()) {
62
+ this.authMode = 'vertex-ai';
63
+ this.setupVertexAIAuth();
64
+ return 'USE_VERTEX_AI';
65
+ }
66
+ if (this.hasGeminiAPIKey()) {
67
+ this.authMode = 'gemini-api-key';
68
+ return process.env.GEMINI_API_KEY;
69
+ }
70
+ // No Gemini-specific credentials, check OAuth availability
63
71
  try {
64
72
  const token = await this.getAuthToken();
65
- // Check for special OAuth signal
66
- if (token === 'USE_LOGIN_WITH_GOOGLE') {
67
- this.authMode = 'oauth';
68
- return token; // Return the magic token
69
- }
70
- // Determine auth mode based on resolved authentication method
71
73
  const authMethodName = await this.getAuthMethodName();
72
- if (authMethodName?.startsWith('oauth-')) {
74
+ // Check if OAuth is configured for Gemini
75
+ const manager = this.geminiOAuthManager;
76
+ const isOAuthEnabled = manager?.isOAuthEnabled &&
77
+ typeof manager.isOAuthEnabled === 'function' &&
78
+ manager.isOAuthEnabled('gemini');
79
+ if (isOAuthEnabled &&
80
+ (authMethodName?.startsWith('oauth-') ||
81
+ (this.geminiOAuthManager && !token))) {
73
82
  this.authMode = 'oauth';
83
+ return 'USE_LOGIN_WITH_GOOGLE';
74
84
  }
75
- else if (this.hasVertexAICredentials()) {
76
- this.authMode = 'vertex-ai';
77
- this.setupVertexAIAuth();
78
- }
79
- else if (this.hasGeminiAPIKey() || authMethodName?.includes('key')) {
80
- this.authMode = 'gemini-api-key';
81
- }
82
- else {
83
- this.authMode = 'none';
85
+ // If we have a token but it's not for Gemini (e.g., from another provider),
86
+ // we should still fall back to OAuth for Gemini web search - BUT ONLY IF OAUTH IS ENABLED
87
+ if (!this.hasGeminiAPIKey() && !this.hasVertexAICredentials()) {
88
+ if (isOAuthEnabled) {
89
+ this.authMode = 'oauth';
90
+ return 'USE_LOGIN_WITH_GOOGLE';
91
+ }
92
+ else {
93
+ // OAuth is disabled and no other auth method available
94
+ throw new AuthenticationRequiredError('Web search requires Gemini authentication, but no API key is set and OAuth is disabled', 'none', ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
95
+ }
84
96
  }
85
- return token;
97
+ this.authMode = 'none';
98
+ return token || '';
86
99
  }
87
100
  catch (error) {
101
+ // CRITICAL FIX: Only fall back to LOGIN_WITH_GOOGLE if OAuth is actually enabled
102
+ // Don't use it when OAuth has been disabled
103
+ const manager = this.geminiOAuthManager;
104
+ const isOAuthEnabled = this.geminiOAuthManager &&
105
+ manager.isOAuthEnabled &&
106
+ typeof manager.isOAuthEnabled === 'function' &&
107
+ manager.isOAuthEnabled('gemini');
108
+ if (isOAuthEnabled) {
109
+ this.authMode = 'oauth';
110
+ return 'USE_LOGIN_WITH_GOOGLE';
111
+ }
88
112
  // Handle case where no auth is available
89
- const authType = this.geminiConfig?.getContentGeneratorConfig()?.authType;
113
+ const authType = this.globalConfig?.getContentGeneratorConfig()?.authType;
90
114
  if (authType === AuthType.USE_NONE) {
91
115
  this.authMode = 'none';
92
116
  throw new AuthenticationRequiredError('Authentication is set to USE_NONE but no credentials are available', this.authMode, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
93
117
  }
118
+ // When used as serverToolsProvider without API key, fall back to OAuth ONLY if enabled
119
+ // This handles the case where Gemini is used for server tools but not as main provider
120
+ if (!this.hasGeminiAPIKey() && !this.hasVertexAICredentials()) {
121
+ if (isOAuthEnabled) {
122
+ this.authMode = 'oauth';
123
+ return 'USE_LOGIN_WITH_GOOGLE';
124
+ }
125
+ else {
126
+ // OAuth is disabled and no other auth method available
127
+ throw new AuthenticationRequiredError('Web search requires Gemini authentication, but no API key is set and OAuth is disabled', 'none', ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
128
+ }
129
+ }
94
130
  throw error;
95
131
  }
96
132
  }
@@ -99,8 +135,15 @@ export class GeminiProvider extends BaseProvider {
99
135
  * Determines if this provider supports OAuth authentication
100
136
  */
101
137
  supportsOAuth() {
102
- // Gemini always supports Google OAuth
103
- return true;
138
+ // Check if OAuth is actually enabled for Gemini in the OAuth manager
139
+ const manager = this.geminiOAuthManager;
140
+ if (manager?.isOAuthEnabled &&
141
+ typeof manager.isOAuthEnabled === 'function') {
142
+ return manager.isOAuthEnabled('gemini');
143
+ }
144
+ // Default to false if OAuth manager is not available
145
+ // This ensures we don't fall back to LOGIN_WITH_GOOGLE when OAuth is disabled
146
+ return false;
104
147
  }
105
148
  /**
106
149
  * Checks if Vertex AI credentials are available
@@ -127,7 +170,6 @@ export class GeminiProvider extends BaseProvider {
127
170
  * Sets the config instance for reading OAuth credentials
128
171
  */
129
172
  setConfig(config) {
130
- this.geminiConfig = config;
131
173
  // Sync with config model if user hasn't explicitly set a model
132
174
  // This ensures consistency between config and provider state
133
175
  const configModel = config.getModel();
@@ -168,8 +210,9 @@ export class GeminiProvider extends BaseProvider {
168
210
  const apiKey = (await this.getAuthToken()) || process.env.GEMINI_API_KEY;
169
211
  if (apiKey) {
170
212
  try {
171
- const url = this.baseURL
172
- ? `${this.baseURL.replace(/\/$/, '')}/v1beta/models?key=${apiKey}`
213
+ const baseURL = this.getBaseURL();
214
+ const url = baseURL
215
+ ? `${baseURL.replace(/\/$/, '')}/v1beta/models?key=${apiKey}`
173
216
  : `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`;
174
217
  const response = await fetch(url, {
175
218
  method: 'GET',
@@ -216,464 +259,23 @@ export class GeminiProvider extends BaseProvider {
216
259
  },
217
260
  ];
218
261
  }
219
- async *generateChatCompletion(messages, tools, _toolFormat) {
220
- // Comprehensive debug logging
221
- this.logger.debug(() => `generateChatCompletion called with ${messages.length} messages`);
222
- this.logger.debug(() => `Messages: ${JSON.stringify(messages, null, 2)}`);
223
- this.logger.debug(() => `First message: ${messages[0] ? JSON.stringify(messages[0], null, 2) : 'NO FIRST MESSAGE'}`);
224
- this.logger.debug(() => `Tools: ${tools ? JSON.stringify(tools.map((t) => t.function.name)) : 'NO TOOLS'}`);
225
- // Lazily determine the best auth method now that it's needed.
226
- // This implements lazy OAuth triggering - OAuth is only triggered when making API calls
227
- let authToken;
228
- try {
229
- authToken = await this.determineBestAuth();
230
- }
231
- catch (error) {
232
- if (error instanceof AuthenticationRequiredError) {
233
- throw error;
234
- }
235
- throw new AuthenticationRequiredError('Failed to resolve authentication for Gemini provider', this.authMode, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
236
- }
237
- // Authentication has already been resolved by determineBestAuth()
238
- // No need for additional validation since the auth token is already obtained
239
- // Import the necessary modules dynamically to avoid circular dependencies
240
- const { GoogleGenAI } = await import('@google/genai');
241
- // Create the appropriate client based on auth mode
242
- let genAI;
243
- const httpOptions = {
244
- headers: {
245
- 'User-Agent': `LLxprt-Code/${process.env.CLI_VERSION || process.version} (${process.platform}; ${process.arch})`,
246
- },
247
- };
248
- switch (this.authMode) {
249
- case 'gemini-api-key':
250
- genAI = new GoogleGenAI({
251
- apiKey: authToken,
252
- httpOptions: this.baseURL
253
- ? {
254
- ...httpOptions,
255
- baseUrl: this.baseURL,
256
- }
257
- : httpOptions,
258
- });
259
- break;
260
- case 'vertex-ai':
261
- genAI = new GoogleGenAI({
262
- apiKey: authToken,
263
- vertexai: true,
264
- httpOptions: this.baseURL
265
- ? {
266
- ...httpOptions,
267
- baseUrl: this.baseURL,
268
- }
269
- : httpOptions,
270
- });
271
- break;
272
- case 'oauth': {
273
- // For OAuth, create a minimal config-like object if we don't have one
274
- const configForOAuth = this.geminiConfig || {
275
- getProxy: () => undefined, // OAuth only needs this from config
276
- };
277
- // For OAuth, we need to use the code assist server
278
- const contentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, configForOAuth, this.baseURL);
279
- // Convert messages to Gemini request format
280
- // Use config model in OAuth mode to ensure synchronization
281
- const oauthModel = this.modelExplicitlySet
282
- ? this.currentModel
283
- : this.geminiConfig?.getModel() || this.currentModel;
284
- // Generate systemInstruction using getCoreSystemPrompt
285
- // Get user memory from config if available
286
- const userMemory = this.geminiConfig?.getUserMemory
287
- ? this.geminiConfig.getUserMemory()
288
- : '';
289
- const systemInstruction = await getCoreSystemPromptAsync(userMemory, oauthModel);
290
- // Store tools if provided
291
- if (tools && tools.length > 0) {
292
- this.toolSchemas = this.convertToolsToGeminiFormat(tools);
293
- }
294
- // Use provided tools or stored tools
295
- let geminiTools = tools
296
- ? this.convertToolsToGeminiFormat(tools)
297
- : this.toolSchemas;
298
- // For Flash models, always include tools if available
299
- if (oauthModel.includes('flash') && !geminiTools && this.toolSchemas) {
300
- geminiTools = this.toolSchemas;
301
- }
302
- const request = {
303
- model: oauthModel,
304
- contents: this.convertMessagesToGeminiFormat(messages),
305
- systemInstruction,
306
- config: {
307
- tools: geminiTools,
308
- ...this.modelParams,
309
- },
310
- };
311
- // Use the content generator stream
312
- // PRIVACY FIX: Removed sessionId to prevent transmission to Google servers
313
- const streamResult = await contentGenerator.generateContentStream(request, 'oauth-session');
314
- // Convert the stream to our format
315
- for await (const response of streamResult) {
316
- // Extract text from the response
317
- const text = response.candidates?.[0]?.content?.parts
318
- ?.filter((part) => 'text' in part)
319
- ?.map((part) => part.text)
320
- ?.join('') || '';
321
- // Extract function calls from the response
322
- const functionCalls = response.candidates?.[0]?.content?.parts
323
- ?.filter((part) => 'functionCall' in part)
324
- ?.map((part) => part.functionCall) || [];
325
- // Build response message
326
- const message = {
327
- role: ContentGeneratorRole.ASSISTANT,
328
- content: text,
329
- };
330
- // Add function calls if any
331
- if (functionCalls && functionCalls.length > 0) {
332
- message.tool_calls = functionCalls.map((call) => ({
333
- id: call.id ||
334
- `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
335
- type: 'function',
336
- function: {
337
- name: call.name || 'unknown_function',
338
- arguments: JSON.stringify(call.args || {}),
339
- },
340
- }));
341
- }
342
- // Only yield if there's content or tool calls
343
- if (text || (functionCalls && functionCalls.length > 0)) {
344
- yield message;
345
- }
346
- }
347
- return;
348
- }
349
- case 'none':
350
- // For 'none' mode, use the resolved auth token
351
- genAI = new GoogleGenAI({
352
- apiKey: authToken,
353
- vertexai: this.hasVertexAICredentials(),
354
- httpOptions: this.baseURL
355
- ? {
356
- ...httpOptions,
357
- baseUrl: this.baseURL,
358
- }
359
- : httpOptions,
360
- });
361
- break;
362
- default:
363
- throw new Error(`Unsupported auth mode: ${this.authMode}`);
364
- }
365
- // Get the models interface (which is a ContentGenerator)
366
- const contentGenerator = genAI.models;
367
- // Store tools if provided
368
- if (tools && tools.length > 0) {
369
- this.toolSchemas = this.convertToolsToGeminiFormat(tools);
370
- }
371
- // Convert IMessage[] to Gemini format - do this after storing tools so priming can access them
372
- const contents = this.convertMessagesToGeminiFormat(messages);
373
- // Use provided tools or stored tools
374
- let geminiTools = tools
375
- ? this.convertToolsToGeminiFormat(tools)
376
- : this.toolSchemas;
377
- // Create the request - ContentGenerator expects model in the request
378
- // Use explicit model if set, otherwise fall back to config model
379
- const modelToUse = this.modelExplicitlySet
380
- ? this.currentModel
381
- : this.geminiConfig?.getModel() || this.currentModel;
382
- // For Flash models, always include tools if available
383
- if (modelToUse.includes('flash') && !geminiTools && this.toolSchemas) {
384
- geminiTools = this.toolSchemas;
385
- }
386
- // Generate systemInstruction using getCoreSystemPrompt
387
- // Get user memory from config if available
388
- const userMemory = this.geminiConfig?.getUserMemory
389
- ? this.geminiConfig.getUserMemory()
390
- : '';
391
- const systemInstruction = await getCoreSystemPromptAsync(userMemory, modelToUse);
392
- const request = {
393
- model: modelToUse,
394
- contents,
395
- systemInstruction,
396
- config: {
397
- tools: geminiTools,
398
- ...this.modelParams,
399
- },
400
- };
401
- // Generate content stream using the ContentGenerator interface
402
- const stream = await contentGenerator.generateContentStream(request);
403
- // Stream the response
404
- for await (const response of stream) {
405
- // Extract text from the response
406
- const text = response.candidates?.[0]?.content?.parts
407
- ?.filter((part) => 'text' in part)
408
- ?.map((part) => part.text)
409
- ?.join('') || '';
410
- // Extract function calls from the response
411
- const functionCalls = response.candidates?.[0]?.content?.parts
412
- ?.filter((part) => 'functionCall' in part)
413
- ?.map((part) => part.functionCall) || [];
414
- // Build response message
415
- const message = {
416
- role: ContentGeneratorRole.ASSISTANT,
417
- content: text || '',
418
- };
419
- // Add function calls if any
420
- if (functionCalls && functionCalls.length > 0) {
421
- message.tool_calls = functionCalls.map((call) => ({
422
- id: call.id ||
423
- `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
424
- type: 'function',
425
- function: {
426
- name: call.name || 'unknown_function',
427
- arguments: JSON.stringify(call.args || {}),
428
- },
429
- }));
430
- }
431
- // Only yield if there's content or tool calls
432
- if (text || (functionCalls && functionCalls.length > 0)) {
433
- yield message;
434
- }
435
- }
436
- }
437
- convertMessagesToGeminiFormat(messages) {
438
- const contents = [];
439
- // Enhanced tracking with more details
440
- const functionCalls = new Map();
441
- const functionResponses = new Map();
442
- for (let i = 0; i < messages.length; i++) {
443
- const msg = messages[i];
444
- // Handle tool responses - each in its own Content object
445
- if (msg.role === ContentGeneratorRole.TOOL) {
446
- if (!msg.tool_call_id) {
447
- this.logger.debug(() => `Tool response at index ${i} missing tool_call_id, skipping: ${JSON.stringify(msg)}`);
448
- continue;
449
- }
450
- functionResponses.set(msg.tool_call_id, {
451
- name: msg.tool_name || 'unknown_function',
452
- contentIndex: contents.length,
453
- messageIndex: i,
454
- });
455
- // Add each tool response as a separate content immediately
456
- contents.push({
457
- role: 'user',
458
- parts: [
459
- {
460
- functionResponse: {
461
- id: msg.tool_call_id,
462
- name: msg.tool_name || 'unknown_function',
463
- response: {
464
- output: msg.content || '',
465
- },
466
- },
467
- },
468
- ],
469
- });
470
- continue;
471
- }
472
- // For non-tool messages, convert normally
473
- const parts = [];
474
- // Check for parts first (for messages with PDF/image parts but no text content)
475
- if (msg.parts && msg.parts.length > 0) {
476
- parts.push(...msg.parts);
477
- }
478
- else if (msg.content) {
479
- // Handle PartListUnion: string | Part | Part[]
480
- // In practice, content can be PartListUnion even though IMessage types it as string
481
- const content = msg.content;
482
- if (typeof content === 'string') {
483
- // Try to parse string in case it's a stringified Part or Part[]
484
- if ((content.startsWith('{') && content.endsWith('}')) ||
485
- (content.startsWith('[') && content.endsWith(']'))) {
486
- try {
487
- const parsed = JSON.parse(content);
488
- if (Array.isArray(parsed)) {
489
- parts.push(...parsed);
490
- }
491
- else {
492
- parts.push(parsed);
493
- }
494
- }
495
- catch (_e) {
496
- // Not valid JSON, treat as text
497
- parts.push({ text: content });
498
- }
499
- }
500
- else {
501
- parts.push({ text: content });
502
- }
503
- }
504
- else if (Array.isArray(content)) {
505
- // Content is Part[]
506
- parts.push(...content);
507
- }
508
- else {
509
- // Content is a single Part
510
- parts.push(content);
511
- }
512
- }
513
- // Handle tool calls
514
- if (msg.tool_calls && msg.tool_calls.length > 0) {
515
- // Check if function calls were already added via parts
516
- const existingFunctionCallIds = new Set();
517
- if (msg.parts && msg.parts.length > 0) {
518
- for (const part of parts) {
519
- if ('functionCall' in part) {
520
- const fc = part;
521
- if (fc.functionCall.id) {
522
- existingFunctionCallIds.add(fc.functionCall.id);
523
- }
524
- }
525
- }
526
- }
527
- for (const toolCall of msg.tool_calls) {
528
- // Skip if this function call was already added via parts
529
- if (toolCall.id && existingFunctionCallIds.has(toolCall.id)) {
530
- continue;
531
- }
532
- // Ensure tool call has an ID
533
- if (!toolCall.id) {
534
- this.logger.debug(() => `Tool call at message ${i} missing ID, generating one: ${JSON.stringify(toolCall)}`);
535
- // Generate a unique ID for the function call
536
- toolCall.id = `generated_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
537
- }
538
- const partIndex = parts.length;
539
- parts.push({
540
- functionCall: {
541
- id: toolCall.id,
542
- name: toolCall.function.name,
543
- args: JSON.parse(toolCall.function.arguments),
544
- },
545
- });
546
- // Track this function call with its position
547
- functionCalls.set(toolCall.id, {
548
- name: toolCall.function.name,
549
- contentIndex: contents.length,
550
- partIndex,
551
- messageIndex: i,
552
- });
553
- }
554
- }
555
- // Map roles
556
- let role = 'user';
557
- if (msg.role === ContentGeneratorRole.ASSISTANT) {
558
- role = 'model';
559
- }
560
- else if (msg.role === ContentGeneratorRole.USER) {
561
- role = 'user';
562
- }
563
- else if (msg.role === 'system') {
564
- // Gemini doesn't have system role in contents, handle separately
565
- role = 'user';
566
- }
567
- if (parts.length > 0) {
568
- contents.push({
569
- role,
570
- parts,
571
- });
572
- }
573
- }
574
- // Validate and add missing function responses
575
- for (const [callId, callInfo] of Array.from(functionCalls.entries())) {
576
- if (!functionResponses.has(callId)) {
577
- // Create a placeholder response for missing function response
578
- this.logger.debug(() => `Function call ${callInfo.name} (id: ${callId}) has no matching response, adding placeholder`);
579
- // Add each function response as a separate content object (same as regular tool responses)
580
- contents.push({
581
- role: 'user',
582
- parts: [
583
- {
584
- functionResponse: {
585
- id: callId,
586
- name: callInfo.name,
587
- response: {
588
- output: JSON.stringify({
589
- error: 'Function call was interrupted or no response received',
590
- 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.`,
591
- callId,
592
- functionName: callInfo.name,
593
- }),
594
- },
595
- },
596
- },
597
- ],
598
- });
599
- // Mark this response as added
600
- functionResponses.set(callId, {
601
- name: callInfo.name,
602
- contentIndex: contents.length - 1,
603
- messageIndex: -1, // Placeholder response doesn't have original message index
604
- });
605
- }
606
- }
607
- // Final validation - count function calls and responses
608
- let totalFunctionCalls = 0;
609
- let totalFunctionResponses = 0;
610
- const callsDetail = [];
611
- const responsesDetail = [];
612
- const unmatchedCalls = new Set();
613
- const unmatchedResponses = new Set();
614
- // First pass: collect all function calls and responses with their IDs
615
- for (let i = 0; i < contents.length; i++) {
616
- const content = contents[i];
617
- for (const part of content.parts) {
618
- if ('functionCall' in part) {
619
- totalFunctionCalls++;
620
- const fc = part;
621
- callsDetail.push(`${i}: ${fc.functionCall.name} (${fc.functionCall.id})`);
622
- if (fc.functionCall.id) {
623
- unmatchedCalls.add(fc.functionCall.id);
624
- }
625
- }
626
- else if ('functionResponse' in part) {
627
- totalFunctionResponses++;
628
- const fr = part;
629
- responsesDetail.push(`${i}: ${fr.functionResponse.name} (${fr.functionResponse.id})`);
630
- if (fr.functionResponse.id) {
631
- unmatchedResponses.add(fr.functionResponse.id);
632
- }
633
- }
634
- }
635
- }
636
- // Second pass: match calls with responses
637
- for (const id of unmatchedCalls) {
638
- if (unmatchedResponses.has(id)) {
639
- unmatchedCalls.delete(id);
640
- unmatchedResponses.delete(id);
641
- }
642
- }
643
- if (totalFunctionCalls !== totalFunctionResponses) {
644
- this.logger.debug(() => `Function parts count mismatch: ${totalFunctionCalls} calls vs ${totalFunctionResponses} responses`);
645
- this.logger.debug(() => `Function calls: ${JSON.stringify(callsDetail)}`);
646
- this.logger.debug(() => `Function responses: ${JSON.stringify(responsesDetail)}`);
647
- this.logger.debug(() => `Unmatched call IDs: ${JSON.stringify(Array.from(unmatchedCalls))}`);
648
- this.logger.debug(() => `Unmatched response IDs: ${JSON.stringify(Array.from(unmatchedResponses))}`);
649
- // This is now just a warning, not an error, since we've added placeholders
650
- // The Gemini API should handle this gracefully
651
- }
652
- return contents;
653
- }
654
- convertToolsToGeminiFormat(tools) {
655
- const result = [
656
- {
657
- functionDeclarations: tools.map((tool) => ({
658
- name: tool.function.name,
659
- description: tool.function.description,
660
- parameters: tool.function.parameters,
661
- })),
662
- },
663
- ];
664
- this.logger.debug(() => `Converted tools to Gemini format: ${JSON.stringify(result, null, 2)}`);
665
- return result;
666
- }
667
262
  setApiKey(apiKey) {
668
263
  // Call base provider implementation
669
- super.setApiKey?.(apiKey);
264
+ super.setApiKey(apiKey);
670
265
  // Set the API key as an environment variable so it can be used by the core library
671
- process.env.GEMINI_API_KEY = apiKey;
266
+ // CRITICAL FIX: When clearing the key (empty string), delete the env var instead of setting to empty
267
+ if (apiKey && apiKey.trim() !== '') {
268
+ process.env.GEMINI_API_KEY = apiKey;
269
+ }
270
+ else {
271
+ delete process.env.GEMINI_API_KEY;
272
+ }
672
273
  // Clear auth cache when API key changes
274
+ this.clearAuthCache();
673
275
  }
674
276
  setBaseUrl(baseUrl) {
675
- // If no baseUrl is provided or it's an empty string, clear to undefined
676
- this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
277
+ // Call base provider implementation which stores in ephemeral settings
278
+ super.setBaseUrl?.(baseUrl);
677
279
  }
678
280
  /**
679
281
  * Gets the current authentication mode
@@ -737,8 +339,8 @@ export class GeminiProvider extends BaseProvider {
737
339
  this.modelExplicitlySet = true;
738
340
  // Always update config if available, not just in OAuth mode
739
341
  // This ensures the model is properly synchronized
740
- if (this.geminiConfig) {
741
- this.geminiConfig.setModel(modelId);
342
+ if (this.globalConfig) {
343
+ this.globalConfig.setModel(modelId);
742
344
  }
743
345
  }
744
346
  /**
@@ -775,13 +377,25 @@ export class GeminiProvider extends BaseProvider {
775
377
  this.currentModel = 'gemini-2.5-pro';
776
378
  }
777
379
  // Note: We don't clear config or apiKey as they might be needed
380
+ // Clear auth cache
381
+ this.clearAuthCache();
382
+ }
383
+ /**
384
+ * Clear all authentication including environment variable
385
+ */
386
+ clearAuth() {
387
+ // Call base implementation to clear SettingsService
388
+ super.clearAuth?.();
389
+ // CRITICAL: Also clear the environment variable that setApiKey sets
390
+ delete process.env.GEMINI_API_KEY;
778
391
  }
779
392
  /**
780
393
  * Forces re-determination of auth method
781
394
  */
782
395
  clearAuthCache() {
783
- // Don't clear the auth mode itself, just the determination flag
784
- // This allows for smoother transitions
396
+ // Call the base implementation to clear the cached token
397
+ super.clearAuthCache();
398
+ // Don't clear the auth mode itself, just allow re-determination next time
785
399
  }
786
400
  /**
787
401
  * Get the list of server tools supported by this provider
@@ -794,6 +408,9 @@ export class GeminiProvider extends BaseProvider {
794
408
  */
795
409
  async invokeServerTool(toolName, params, _config) {
796
410
  if (toolName === 'web_search') {
411
+ this.logger.debug(() => `invokeServerTool: web_search called with params: ${JSON.stringify(params)}`);
412
+ this.logger.debug(() => `invokeServerTool: globalConfig is ${this.globalConfig ? 'set' : 'null/undefined'}`);
413
+ this.logger.debug(() => `invokeServerTool: current authMode is ${this.authMode}`);
797
414
  // Import the necessary modules dynamically
798
415
  const { GoogleGenAI } = await import('@google/genai');
799
416
  // Create the appropriate client based on auth mode
@@ -804,15 +421,28 @@ export class GeminiProvider extends BaseProvider {
804
421
  };
805
422
  let genAI;
806
423
  // Get authentication token lazily
424
+ this.logger.debug(() => `invokeServerTool: about to call determineBestAuth()`);
807
425
  const authToken = await this.determineBestAuth();
426
+ this.logger.debug(() => `invokeServerTool: determineBestAuth returned, authMode is now ${this.authMode}`);
427
+ // Re-evaluate auth mode if we got a signal to use OAuth
428
+ if (authToken === 'USE_LOGIN_WITH_GOOGLE') {
429
+ this.authMode = 'oauth';
430
+ }
808
431
  switch (this.authMode) {
809
432
  case 'gemini-api-key': {
433
+ // This case should never happen if determineBestAuth worked correctly
434
+ // but add safety check
435
+ if (!authToken ||
436
+ authToken === 'USE_LOGIN_WITH_GOOGLE' ||
437
+ authToken === '') {
438
+ throw new Error('No valid Gemini API key available for web search');
439
+ }
810
440
  genAI = new GoogleGenAI({
811
441
  apiKey: authToken,
812
- httpOptions: this.baseURL
442
+ httpOptions: this.getBaseURL()
813
443
  ? {
814
444
  ...httpOptions,
815
- baseUrl: this.baseURL,
445
+ baseUrl: this.getBaseURL(),
816
446
  }
817
447
  : httpOptions,
818
448
  });
@@ -837,10 +467,10 @@ export class GeminiProvider extends BaseProvider {
837
467
  genAI = new GoogleGenAI({
838
468
  apiKey: authToken,
839
469
  vertexai: true,
840
- httpOptions: this.baseURL
470
+ httpOptions: this.getBaseURL()
841
471
  ? {
842
472
  ...httpOptions,
843
- baseUrl: this.baseURL,
473
+ baseUrl: this.getBaseURL(),
844
474
  }
845
475
  : httpOptions,
846
476
  });
@@ -862,24 +492,52 @@ export class GeminiProvider extends BaseProvider {
862
492
  return vertexResult;
863
493
  }
864
494
  case 'oauth': {
865
- // For OAuth, use the code assist content generator
866
- const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, this.geminiConfig);
867
- // For web search, always use gemini-2.5-flash regardless of the active model
868
- const oauthRequest = {
869
- model: 'gemini-2.5-flash',
870
- contents: [
871
- {
872
- role: 'user',
873
- parts: [{ text: params.query }],
495
+ try {
496
+ this.logger.debug(() => `invokeServerTool: OAuth case - creating content generator`);
497
+ // If globalConfig is not set (e.g., when using non-Gemini provider),
498
+ // create a minimal config for OAuth
499
+ let configForOAuth = this.globalConfig;
500
+ if (!configForOAuth) {
501
+ this.logger.debug(() => `invokeServerTool: globalConfig is null, creating minimal config for OAuth`);
502
+ // Use crypto for UUID generation
503
+ const { randomUUID } = await import('crypto');
504
+ configForOAuth = new Config({
505
+ sessionId: randomUUID(),
506
+ targetDir: process.cwd(),
507
+ debugMode: false,
508
+ cwd: process.cwd(),
509
+ model: 'gemini-2.5-flash',
510
+ });
511
+ // The OAuth flow will handle authentication
512
+ }
513
+ // For OAuth, use the code assist content generator
514
+ // Note: Detailed logging is now handled by DebugLogger in codeAssist.ts with namespace llxprt:code:assist
515
+ const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, configForOAuth);
516
+ this.logger.debug(() => `invokeServerTool: OAuth content generator created successfully`);
517
+ // For web search, always use gemini-2.5-flash regardless of the active model
518
+ const oauthRequest = {
519
+ model: 'gemini-2.5-flash',
520
+ contents: [
521
+ {
522
+ role: 'user',
523
+ parts: [{ text: params.query }],
524
+ },
525
+ ],
526
+ config: {
527
+ tools: [{ googleSearch: {} }],
874
528
  },
875
- ],
876
- config: {
877
- tools: [{ googleSearch: {} }],
878
- },
879
- };
880
- // PRIVACY FIX: Removed sessionId to prevent transmission to Google servers
881
- const result = await oauthContentGenerator.generateContent(oauthRequest, 'web-search-oauth');
882
- return result;
529
+ };
530
+ this.logger.debug(() => `invokeServerTool: making OAuth generateContent request with query: ${params.query}`);
531
+ // PRIVACY FIX: Removed sessionId to prevent transmission to Google servers
532
+ const result = await oauthContentGenerator.generateContent(oauthRequest, 'web-search-oauth');
533
+ this.logger.debug(() => `invokeServerTool: OAuth generateContent completed successfully`);
534
+ return result;
535
+ }
536
+ catch (error) {
537
+ this.logger.debug(() => `invokeServerTool: ERROR in OAuth case: ${error}`);
538
+ this.logger.debug(() => `invokeServerTool: Error details:`, error);
539
+ throw error;
540
+ }
883
541
  }
884
542
  default:
885
543
  throw new Error(`Web search not supported in auth mode: ${this.authMode}`);
@@ -903,10 +561,10 @@ export class GeminiProvider extends BaseProvider {
903
561
  case 'gemini-api-key': {
904
562
  genAI = new GoogleGenAI({
905
563
  apiKey: authToken,
906
- httpOptions: this.baseURL
564
+ httpOptions: this.getBaseURL()
907
565
  ? {
908
566
  ...httpOptions,
909
- baseUrl: this.baseURL,
567
+ baseUrl: this.getBaseURL(),
910
568
  }
911
569
  : httpOptions,
912
570
  });
@@ -931,10 +589,10 @@ export class GeminiProvider extends BaseProvider {
931
589
  genAI = new GoogleGenAI({
932
590
  apiKey: authToken,
933
591
  vertexai: true,
934
- httpOptions: this.baseURL
592
+ httpOptions: this.getBaseURL()
935
593
  ? {
936
594
  ...httpOptions,
937
- baseUrl: this.baseURL,
595
+ baseUrl: this.getBaseURL(),
938
596
  }
939
597
  : httpOptions,
940
598
  });
@@ -957,7 +615,7 @@ export class GeminiProvider extends BaseProvider {
957
615
  }
958
616
  case 'oauth': {
959
617
  // For OAuth, use the code assist content generator
960
- const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, this.geminiConfig);
618
+ const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, this.globalConfig);
961
619
  // For web fetch, always use gemini-2.5-flash regardless of the active model
962
620
  const oauthRequest = {
963
621
  model: 'gemini-2.5-flash',
@@ -983,5 +641,176 @@ export class GeminiProvider extends BaseProvider {
983
641
  throw new Error(`Unknown server tool: ${toolName}`);
984
642
  }
985
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
+ }
986
815
  }
987
816
  //# sourceMappingURL=GeminiProvider.js.map