@vybestack/llxprt-code-core 0.1.23 → 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/README.md +21 -17
- package/dist/src/adapters/IStreamAdapter.d.ts +3 -3
- package/dist/src/auth/oauth-errors.d.ts +173 -0
- package/dist/src/auth/oauth-errors.js +461 -0
- package/dist/src/auth/oauth-errors.js.map +1 -0
- package/dist/src/auth/precedence.d.ts +1 -5
- package/dist/src/auth/precedence.js +28 -48
- package/dist/src/auth/precedence.js.map +1 -1
- package/dist/src/auth/token-store.js +2 -2
- package/dist/src/auth/token-store.js.map +1 -1
- package/dist/src/auth/types.d.ts +4 -4
- package/dist/src/code_assist/codeAssist.js +19 -6
- package/dist/src/code_assist/codeAssist.js.map +1 -1
- package/dist/src/code_assist/oauth2.d.ts +7 -0
- package/dist/src/code_assist/oauth2.js +82 -32
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/server.js +15 -4
- package/dist/src/code_assist/server.js.map +1 -1
- package/dist/src/code_assist/setup.js +9 -0
- 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 +15 -20
- package/dist/src/core/client.js +98 -124
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/compression-config.d.ts +10 -0
- package/dist/src/core/compression-config.js +17 -0
- package/dist/src/core/compression-config.js.map +1 -0
- package/dist/src/core/coreToolScheduler.js +50 -15
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +68 -9
- package/dist/src/core/geminiChat.js +940 -405
- 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 +35 -25
- 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/ide/ide-client.d.ts +1 -1
- package/dist/src/ide/ide-client.js +12 -6
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +5 -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 +270 -0
- 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 +39 -13
- package/dist/src/providers/BaseProvider.js +102 -28
- package/dist/src/providers/BaseProvider.js.map +1 -1
- package/dist/src/providers/IProvider.d.ts +17 -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/ProviderManager.d.ts +4 -0
- package/dist/src/providers/ProviderManager.js +6 -0
- package/dist/src/providers/ProviderManager.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.d.ts +34 -21
- package/dist/src/providers/anthropic/AnthropicProvider.js +505 -492
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/gemini/GeminiProvider.d.ts +23 -9
- package/dist/src/providers/gemini/GeminiProvider.js +344 -515
- 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 +46 -96
- package/dist/src/providers/openai/OpenAIProvider.js +532 -1393
- 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 +89 -0
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +451 -0
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -0
- package/dist/src/providers/openai-responses/index.d.ts +1 -0
- package/dist/src/providers/openai-responses/index.js +2 -0
- package/dist/src/providers/openai-responses/index.js.map +1 -0
- 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/ClipboardService.d.ts +19 -0
- package/dist/src/services/ClipboardService.js +66 -0
- package/dist/src/services/ClipboardService.js.map +1 -0
- package/dist/src/services/history/ContentConverters.d.ts +43 -0
- package/dist/src/services/history/ContentConverters.js +325 -0
- package/dist/src/services/history/ContentConverters.js.map +1 -0
- package/dist/src/{providers/IMessage.d.ts → services/history/HistoryEvents.d.ts} +16 -22
- package/dist/src/{providers/IMessage.js → services/history/HistoryEvents.js} +1 -1
- package/dist/src/services/history/HistoryEvents.js.map +1 -0
- package/dist/src/services/history/HistoryService.d.ts +220 -0
- package/dist/src/services/history/HistoryService.js +673 -0
- package/dist/src/services/history/HistoryService.js.map +1 -0
- package/dist/src/services/history/IContent.d.ts +183 -0
- package/dist/src/services/history/IContent.js +104 -0
- package/dist/src/services/history/IContent.js.map +1 -0
- package/dist/src/services/index.d.ts +1 -0
- package/dist/src/services/index.js +1 -0
- package/dist/src/services/index.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.d.ts +6 -1
- package/dist/src/tools/read-file.js +25 -11
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/todo-schemas.d.ts +4 -4
- package/dist/src/tools/tools.js +13 -0
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/write-file.d.ts +6 -1
- package/dist/src/tools/write-file.js +48 -26
- 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/dist/src/utils/schemaValidator.js +16 -1
- package/dist/src/utils/schemaValidator.js.map +1 -1
- package/package.json +8 -7
- 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
@@ -4,18 +4,20 @@
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
5
5
|
*/
|
6
6
|
import { DebugLogger } from '../../debug/index.js';
|
7
|
-
import {
|
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
|
-
//
|
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
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
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.
|
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
|
103
|
-
|
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
|
172
|
-
|
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
|
264
|
+
super.setApiKey(apiKey);
|
670
265
|
// Set the API key as an environment variable so it can be used by the core library
|
671
|
-
|
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
|
-
//
|
676
|
-
|
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.
|
741
|
-
this.
|
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
|
-
//
|
784
|
-
|
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.
|
442
|
+
httpOptions: this.getBaseURL()
|
813
443
|
? {
|
814
444
|
...httpOptions,
|
815
|
-
baseUrl: this.
|
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.
|
470
|
+
httpOptions: this.getBaseURL()
|
841
471
|
? {
|
842
472
|
...httpOptions,
|
843
|
-
baseUrl: this.
|
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
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
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
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
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.
|
564
|
+
httpOptions: this.getBaseURL()
|
907
565
|
? {
|
908
566
|
...httpOptions,
|
909
|
-
baseUrl: this.
|
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.
|
592
|
+
httpOptions: this.getBaseURL()
|
935
593
|
? {
|
936
594
|
...httpOptions,
|
937
|
-
baseUrl: this.
|
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.
|
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
|