@vybestack/llxprt-code-core 0.4.8 → 0.5.0-nightly.251102.e5b51aa3

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 (213) hide show
  1. package/dist/prompt-config/defaults/default-prompts.json +4 -17
  2. package/dist/src/auth/precedence.d.ts +69 -9
  3. package/dist/src/auth/precedence.js +467 -69
  4. package/dist/src/auth/precedence.js.map +1 -1
  5. package/dist/src/auth/types.d.ts +2 -2
  6. package/dist/src/config/config.d.ts +15 -1
  7. package/dist/src/config/config.js +118 -6
  8. package/dist/src/config/config.js.map +1 -1
  9. package/dist/src/config/index.d.ts +6 -0
  10. package/dist/src/config/index.js +5 -0
  11. package/dist/src/config/index.js.map +1 -1
  12. package/dist/src/config/profileManager.d.ts +23 -3
  13. package/dist/src/config/profileManager.js +54 -7
  14. package/dist/src/config/profileManager.js.map +1 -1
  15. package/dist/src/config/subagentManager.d.ts +96 -0
  16. package/dist/src/config/subagentManager.js +371 -0
  17. package/dist/src/config/subagentManager.js.map +1 -0
  18. package/dist/src/config/types.d.ts +18 -0
  19. package/dist/src/config/types.js +3 -0
  20. package/dist/src/config/types.js.map +1 -0
  21. package/dist/src/core/client.d.ts +27 -7
  22. package/dist/src/core/client.js +231 -55
  23. package/dist/src/core/client.js.map +1 -1
  24. package/dist/src/core/contentGenerator.d.ts +3 -1
  25. package/dist/src/core/contentGenerator.js +3 -0
  26. package/dist/src/core/contentGenerator.js.map +1 -1
  27. package/dist/src/core/coreToolScheduler.d.ts +1 -5
  28. package/dist/src/core/coreToolScheduler.js +95 -23
  29. package/dist/src/core/coreToolScheduler.js.map +1 -1
  30. package/dist/src/core/geminiChat.d.ts +42 -12
  31. package/dist/src/core/geminiChat.js +405 -205
  32. package/dist/src/core/geminiChat.js.map +1 -1
  33. package/dist/src/core/nonInteractiveToolExecutor.d.ts +3 -2
  34. package/dist/src/core/nonInteractiveToolExecutor.js +94 -10
  35. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  36. package/dist/src/core/subagent.d.ts +86 -7
  37. package/dist/src/core/subagent.js +809 -79
  38. package/dist/src/core/subagent.js.map +1 -1
  39. package/dist/src/core/subagentOrchestrator.d.ts +73 -0
  40. package/dist/src/core/subagentOrchestrator.js +387 -0
  41. package/dist/src/core/subagentOrchestrator.js.map +1 -0
  42. package/dist/src/core/subagentScheduler.d.ts +16 -0
  43. package/dist/src/core/subagentScheduler.js +7 -0
  44. package/dist/src/core/subagentScheduler.js.map +1 -0
  45. package/dist/src/core/turn.d.ts +5 -1
  46. package/dist/src/core/turn.js +5 -1
  47. package/dist/src/core/turn.js.map +1 -1
  48. package/dist/src/hooks/tool-render-suppression-hook.js +6 -1
  49. package/dist/src/hooks/tool-render-suppression-hook.js.map +1 -1
  50. package/dist/src/ide/ideContext.d.ts +32 -32
  51. package/dist/src/index.d.ts +19 -1
  52. package/dist/src/index.js +15 -2
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/interfaces/index.d.ts +1 -0
  55. package/dist/src/interfaces/index.js +4 -0
  56. package/dist/src/interfaces/index.js.map +1 -0
  57. package/dist/src/interfaces/nodejs-error.interface.d.ts +4 -0
  58. package/dist/src/interfaces/nodejs-error.interface.js +2 -0
  59. package/dist/src/interfaces/nodejs-error.interface.js.map +1 -0
  60. package/dist/src/parsers/TextToolCallParser.d.ts +17 -1
  61. package/dist/src/parsers/TextToolCallParser.js +542 -148
  62. package/dist/src/parsers/TextToolCallParser.js.map +1 -1
  63. package/dist/src/prompt-config/defaults/core.md +15 -0
  64. package/dist/src/prompt-config/defaults/providers/gemini/core.md +203 -119
  65. package/dist/src/prompt-config/defaults/tool-defaults.js +2 -0
  66. package/dist/src/prompt-config/defaults/tool-defaults.js.map +1 -1
  67. package/dist/src/prompt-config/defaults/tools/list-subagents.md +7 -0
  68. package/dist/src/prompt-config/defaults/tools/task.md +8 -0
  69. package/dist/src/providers/BaseProvider.d.ts +115 -30
  70. package/dist/src/providers/BaseProvider.js +445 -109
  71. package/dist/src/providers/BaseProvider.js.map +1 -1
  72. package/dist/src/providers/IProvider.d.ts +50 -18
  73. package/dist/src/providers/LoggingProviderWrapper.d.ts +60 -16
  74. package/dist/src/providers/LoggingProviderWrapper.js +213 -60
  75. package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
  76. package/dist/src/providers/ProviderManager.d.ts +73 -2
  77. package/dist/src/providers/ProviderManager.js +492 -40
  78. package/dist/src/providers/ProviderManager.js.map +1 -1
  79. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +35 -38
  80. package/dist/src/providers/anthropic/AnthropicProvider.js +222 -227
  81. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  82. package/dist/src/providers/errors.d.ts +86 -0
  83. package/dist/src/providers/errors.js +89 -0
  84. package/dist/src/providers/errors.js.map +1 -1
  85. package/dist/src/providers/gemini/GeminiProvider.d.ts +101 -41
  86. package/dist/src/providers/gemini/GeminiProvider.js +386 -311
  87. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  88. package/dist/src/providers/openai/ConversationCache.d.ts +5 -3
  89. package/dist/src/providers/openai/ConversationCache.js +93 -32
  90. package/dist/src/providers/openai/ConversationCache.js.map +1 -1
  91. package/dist/src/providers/openai/OpenAIProvider.d.ts +82 -42
  92. package/dist/src/providers/openai/OpenAIProvider.js +391 -536
  93. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  94. package/dist/src/providers/openai/getOpenAIProviderInfo.d.ts +1 -1
  95. package/dist/src/providers/openai/getOpenAIProviderInfo.js +52 -22
  96. package/dist/src/providers/openai/getOpenAIProviderInfo.js.map +1 -1
  97. package/dist/src/providers/openai/openaiRequestParams.d.ts +7 -0
  98. package/dist/src/providers/openai/openaiRequestParams.js +66 -0
  99. package/dist/src/providers/openai/openaiRequestParams.js.map +1 -0
  100. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +6 -33
  101. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +84 -183
  102. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
  103. package/dist/src/providers/types/providerRuntime.d.ts +17 -0
  104. package/dist/src/providers/types/providerRuntime.js +7 -0
  105. package/dist/src/providers/types/providerRuntime.js.map +1 -0
  106. package/dist/src/providers/utils/authToken.d.ts +12 -0
  107. package/dist/src/providers/utils/authToken.js +17 -0
  108. package/dist/src/providers/utils/authToken.js.map +1 -0
  109. package/dist/src/providers/utils/userMemory.d.ts +8 -0
  110. package/dist/src/providers/utils/userMemory.js +34 -0
  111. package/dist/src/providers/utils/userMemory.js.map +1 -0
  112. package/dist/src/runtime/AgentRuntimeContext.d.ts +213 -0
  113. package/dist/src/runtime/AgentRuntimeContext.js +17 -0
  114. package/dist/src/runtime/AgentRuntimeContext.js.map +1 -0
  115. package/dist/src/runtime/AgentRuntimeLoader.d.ts +47 -0
  116. package/dist/src/runtime/AgentRuntimeLoader.js +122 -0
  117. package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -0
  118. package/dist/src/runtime/AgentRuntimeState.d.ts +232 -0
  119. package/dist/src/runtime/AgentRuntimeState.js +439 -0
  120. package/dist/src/runtime/AgentRuntimeState.js.map +1 -0
  121. package/dist/src/runtime/RuntimeInvocationContext.d.ts +51 -0
  122. package/dist/src/runtime/RuntimeInvocationContext.js +52 -0
  123. package/dist/src/runtime/RuntimeInvocationContext.js.map +1 -0
  124. package/dist/src/runtime/createAgentRuntimeContext.d.ts +7 -0
  125. package/dist/src/runtime/createAgentRuntimeContext.js +65 -0
  126. package/dist/src/runtime/createAgentRuntimeContext.js.map +1 -0
  127. package/dist/src/runtime/index.d.ts +13 -0
  128. package/dist/src/runtime/index.js +14 -0
  129. package/dist/src/runtime/index.js.map +1 -0
  130. package/dist/src/runtime/providerRuntimeContext.d.ts +30 -0
  131. package/dist/src/runtime/providerRuntimeContext.js +70 -0
  132. package/dist/src/runtime/providerRuntimeContext.js.map +1 -0
  133. package/dist/src/runtime/runtimeAdapters.d.ts +22 -0
  134. package/dist/src/runtime/runtimeAdapters.js +81 -0
  135. package/dist/src/runtime/runtimeAdapters.js.map +1 -0
  136. package/dist/src/runtime/runtimeStateFactory.d.ts +21 -0
  137. package/dist/src/runtime/runtimeStateFactory.js +104 -0
  138. package/dist/src/runtime/runtimeStateFactory.js.map +1 -0
  139. package/dist/src/services/todo-context-tracker.d.ts +10 -8
  140. package/dist/src/services/todo-context-tracker.js +26 -10
  141. package/dist/src/services/todo-context-tracker.js.map +1 -1
  142. package/dist/src/services/tool-call-tracker-service.d.ts +11 -7
  143. package/dist/src/services/tool-call-tracker-service.js +89 -29
  144. package/dist/src/services/tool-call-tracker-service.js.map +1 -1
  145. package/dist/src/settings/SettingsService.d.ts +4 -0
  146. package/dist/src/settings/SettingsService.js +65 -2
  147. package/dist/src/settings/SettingsService.js.map +1 -1
  148. package/dist/src/settings/settingsServiceInstance.d.ts +6 -1
  149. package/dist/src/settings/settingsServiceInstance.js +28 -8
  150. package/dist/src/settings/settingsServiceInstance.js.map +1 -1
  151. package/dist/src/telemetry/loggers.d.ts +5 -1
  152. package/dist/src/telemetry/loggers.js.map +1 -1
  153. package/dist/src/telemetry/loggers.test.circular.js +4 -0
  154. package/dist/src/telemetry/loggers.test.circular.js.map +1 -1
  155. package/dist/src/telemetry/metrics.d.ts +3 -1
  156. package/dist/src/telemetry/metrics.js.map +1 -1
  157. package/dist/src/telemetry/types.d.ts +1 -0
  158. package/dist/src/telemetry/types.js +3 -0
  159. package/dist/src/telemetry/types.js.map +1 -1
  160. package/dist/src/test-utils/index.d.ts +2 -0
  161. package/dist/src/test-utils/index.js +2 -0
  162. package/dist/src/test-utils/index.js.map +1 -1
  163. package/dist/src/test-utils/mockWorkspaceContext.d.ts +0 -3
  164. package/dist/src/test-utils/mockWorkspaceContext.js +3 -4
  165. package/dist/src/test-utils/mockWorkspaceContext.js.map +1 -1
  166. package/dist/src/test-utils/providerCallOptions.d.ts +43 -0
  167. package/dist/src/test-utils/providerCallOptions.js +137 -0
  168. package/dist/src/test-utils/providerCallOptions.js.map +1 -0
  169. package/dist/src/test-utils/runtime.d.ts +92 -0
  170. package/dist/src/test-utils/runtime.js +226 -0
  171. package/dist/src/test-utils/runtime.js.map +1 -0
  172. package/dist/src/test-utils/tools.d.ts +4 -4
  173. package/dist/src/test-utils/tools.js +20 -10
  174. package/dist/src/test-utils/tools.js.map +1 -1
  175. package/dist/src/tools/list-subagents.d.ts +31 -0
  176. package/dist/src/tools/list-subagents.js +109 -0
  177. package/dist/src/tools/list-subagents.js.map +1 -0
  178. package/dist/src/tools/task.d.ts +87 -0
  179. package/dist/src/tools/task.js +427 -0
  180. package/dist/src/tools/task.js.map +1 -0
  181. package/dist/src/tools/todo-read.js +1 -1
  182. package/dist/src/tools/todo-read.js.map +1 -1
  183. package/dist/src/tools/todo-store.js +4 -2
  184. package/dist/src/tools/todo-store.js.map +1 -1
  185. package/dist/src/tools/todo-write.js +4 -2
  186. package/dist/src/tools/todo-write.js.map +1 -1
  187. package/dist/src/tools/tool-error.d.ts +1 -0
  188. package/dist/src/tools/tool-error.js +1 -0
  189. package/dist/src/tools/tool-error.js.map +1 -1
  190. package/dist/src/tools/tool-registry.d.ts +2 -0
  191. package/dist/src/tools/tool-registry.js +46 -21
  192. package/dist/src/tools/tool-registry.js.map +1 -1
  193. package/dist/src/types/modelParams.d.ts +4 -0
  194. package/dist/src/utils/editor.js +10 -8
  195. package/dist/src/utils/editor.js.map +1 -1
  196. package/dist/src/utils/gitIgnoreParser.js +15 -3
  197. package/dist/src/utils/gitIgnoreParser.js.map +1 -1
  198. package/package.json +1 -1
  199. package/dist/src/prompt-config/defaults/providers/anthropic/core.md +0 -97
  200. package/dist/src/prompt-config/defaults/providers/anthropic/tools/glob.md +0 -34
  201. package/dist/src/prompt-config/defaults/providers/anthropic/tools/list-directory.md +0 -11
  202. package/dist/src/prompt-config/defaults/providers/anthropic/tools/read-file.md +0 -14
  203. package/dist/src/prompt-config/defaults/providers/anthropic/tools/read-many-files.md +0 -31
  204. package/dist/src/prompt-config/defaults/providers/anthropic/tools/replace.md +0 -41
  205. package/dist/src/prompt-config/defaults/providers/anthropic/tools/run-shell-command.md +0 -32
  206. package/dist/src/prompt-config/defaults/providers/anthropic/tools/save-memory.md +0 -35
  207. package/dist/src/prompt-config/defaults/providers/anthropic/tools/search-file-content.md +0 -44
  208. package/dist/src/prompt-config/defaults/providers/anthropic/tools/todo-write.md +0 -45
  209. package/dist/src/prompt-config/defaults/providers/anthropic/tools/write-file.md +0 -11
  210. package/dist/src/prompt-config/defaults/providers/openai/core.md +0 -97
  211. package/dist/src/prompt-config/defaults/providers/openai/tools/todo-pause.md +0 -28
  212. package/dist/src/prompt-config/defaults/providers/openai/tools/todo-read.md +0 -5
  213. package/dist/src/prompt-config/defaults/providers/openai/tools/todo-write.md +0 -45
@@ -3,17 +3,49 @@
3
3
  * Copyright 2025 Vybestack LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ // @plan:PLAN-20251023-STATELESS-HARDENING.P08 @requirement:REQ-SP4-002
7
+ // createHash import removed - no longer needed without client caching
6
8
  import { DebugLogger } from '../../debug/index.js';
7
9
  import { Config } from '../../config/config.js';
8
10
  import { AuthType } from '../../core/contentGenerator.js';
9
11
  import { AuthenticationRequiredError } from '../errors.js';
10
12
  import { getCoreSystemPromptAsync } from '../../core/prompts.js';
11
- import { createCodeAssistContentGenerator } from '../../code_assist/codeAssist.js';
12
13
  import { Type, } from '@google/genai';
13
- import { BaseProvider } from '../BaseProvider.js';
14
- import { getSettingsService } from '../../settings/settingsServiceInstance.js';
15
- import { retryWithBackoff, getErrorStatus, isNetworkTransientError, } from '../../utils/retry.js';
14
+ import { BaseProvider, } from '../BaseProvider.js';
16
15
  export class GeminiProvider extends BaseProvider {
16
+ /**
17
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
18
+ * @requirement:REQ-SP4-002
19
+ * @pseudocode lines 10-14
20
+ *
21
+ * Removed cached state variables to ensure stateless behavior.
22
+ * Provider now resolves model, auth, and parameters per call.
23
+ * No instance state: authMode, model overrides, modelExplicitlySet, currentModel removed.
24
+ * @requirement:REQ-SP4-003: Auth/model/params come from NormalizedGenerateChatOptions
25
+ */
26
+ geminiOAuthManager;
27
+ /**
28
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
29
+ * @requirement:REQ-SP4-002, REQ-SP4-003
30
+ * @pseudocode lines 10-14
31
+ *
32
+ * Simplified constructor that only stores readonly OAuth manager.
33
+ * All other state is now resolved per call from NormalizedGenerateChatOptions.
34
+ * No loggers cached - created on demand like AnthropicProvider.
35
+ */
36
+ constructor(apiKey, baseURL, config, oauthManager) {
37
+ const baseConfig = {
38
+ name: 'gemini',
39
+ apiKey,
40
+ baseURL,
41
+ envKeyNames: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
42
+ isOAuthEnabled: !!oauthManager,
43
+ oauthProvider: oauthManager ? 'gemini' : undefined,
44
+ oauthManager,
45
+ };
46
+ super(baseConfig, config);
47
+ this.geminiOAuthManager = oauthManager;
48
+ }
17
49
  /**
18
50
  * @description Cleans a JSON Schema object to ensure it strictly conforms to the Gemini API's supported Schema definition.
19
51
  * This function acts as a whitelist, removing any properties not explicitly defined in the OpenAPI 3.03 Schema Object
@@ -85,28 +117,42 @@ export class GeminiProvider extends BaseProvider {
85
117
  }
86
118
  return cleanedSchema;
87
119
  }
88
- logger;
89
- authMode = 'none';
90
- currentModel = 'gemini-2.5-pro';
91
- modelExplicitlySet = false;
92
- modelParams;
93
- geminiOAuthManager;
94
- constructor(apiKey, baseURL, config, oauthManager) {
95
- // Initialize base provider with auth configuration
96
- const baseConfig = {
97
- name: 'gemini',
98
- apiKey,
99
- baseURL,
100
- envKeyNames: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
101
- isOAuthEnabled: false, // OAuth enablement will be checked dynamically
102
- oauthProvider: 'gemini',
103
- oauthManager, // Keep the manager for checking enablement
104
- };
105
- super(baseConfig, config, undefined);
106
- this.logger = new DebugLogger('llxprt:gemini:provider');
107
- this.geminiOAuthManager = oauthManager;
108
- // Do not determine auth mode on instantiation.
109
- // This will be done lazily when a chat completion is requested.
120
+ /**
121
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
122
+ * @requirement:REQ-SP4-002
123
+ * Create loggers on-demand to avoid instance state, like AnthropicProvider
124
+ */
125
+ getLogger() {
126
+ return new DebugLogger('llxprt:gemini:provider');
127
+ }
128
+ getToolsLogger() {
129
+ return new DebugLogger('llxprt:gemini:tools');
130
+ }
131
+ /**
132
+ * @plan PLAN-20251018-STATELESSPROVIDER2.P12
133
+ * @requirement REQ-SP2-001
134
+ * @pseudocode anthropic-gemini-stateless.md lines 1-2
135
+ */
136
+ /**
137
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
138
+ * @requirement:REQ-SP4-003
139
+ * Determine streaming preference from config settings per call
140
+ */
141
+ getStreamingPreference(_options) {
142
+ const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
143
+ return streamingSetting !== 'disabled';
144
+ }
145
+ async createOAuthContentGenerator(httpOptions, config, baseURL) {
146
+ const { createCodeAssistContentGenerator } = await import('../../code_assist/codeAssist.js');
147
+ return createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, config, baseURL);
148
+ }
149
+ /**
150
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
151
+ * @requirement:REQ-SP4-002
152
+ * No operation - stateless provider has no cache to clear
153
+ */
154
+ clearClientCache(_runtimeKey) {
155
+ this.getLogger().debug(() => 'Cache clear called on stateless provider - no operation');
110
156
  }
111
157
  /**
112
158
  * Updates OAuth configuration based on current OAuth manager state
@@ -123,21 +169,21 @@ export class GeminiProvider extends BaseProvider {
123
169
  }
124
170
  }
125
171
  /**
126
- * Determines the best available authentication method based on environment variables
127
- * and existing configuration. Now uses lazy evaluation with proper precedence chain.
172
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
173
+ * @requirement:REQ-SP4-002, REQ-SP4-003
174
+ * Determines the best available authentication method per call without storing state.
175
+ * Returns both authMode and token - caller must handle both values.
128
176
  */
129
177
  async determineBestAuth() {
130
178
  // Re-check OAuth enablement state before determining auth
131
179
  this.updateOAuthState();
132
180
  // First check if we have Gemini-specific credentials
133
181
  if (this.hasVertexAICredentials()) {
134
- this.authMode = 'vertex-ai';
135
182
  this.setupVertexAIAuth();
136
- return 'USE_VERTEX_AI';
183
+ return { authMode: 'vertex-ai', token: 'USE_VERTEX_AI' };
137
184
  }
138
185
  if (this.hasGeminiAPIKey()) {
139
- this.authMode = 'gemini-api-key';
140
- return process.env.GEMINI_API_KEY;
186
+ return { authMode: 'gemini-api-key', token: process.env.GEMINI_API_KEY };
141
187
  }
142
188
  // No Gemini-specific credentials, check OAuth availability
143
189
  try {
@@ -151,51 +197,42 @@ export class GeminiProvider extends BaseProvider {
151
197
  if (isOAuthEnabled &&
152
198
  (authMethodName?.startsWith('oauth-') ||
153
199
  (this.geminiOAuthManager && !token))) {
154
- this.authMode = 'oauth';
155
- return 'USE_LOGIN_WITH_GOOGLE';
200
+ return { authMode: 'oauth', token: 'USE_LOGIN_WITH_GOOGLE' };
156
201
  }
157
202
  // If we have a token but it's not for Gemini (e.g., from another provider),
158
203
  // we should still fall back to OAuth for Gemini web search - BUT ONLY IF OAUTH IS ENABLED
159
204
  if (!this.hasGeminiAPIKey() && !this.hasVertexAICredentials()) {
160
205
  if (isOAuthEnabled) {
161
- this.authMode = 'oauth';
162
- return 'USE_LOGIN_WITH_GOOGLE';
206
+ return { authMode: 'oauth', token: 'USE_LOGIN_WITH_GOOGLE' };
163
207
  }
164
208
  else {
165
209
  // OAuth is disabled and no other auth method available
166
210
  throw new AuthenticationRequiredError('Web search requires Gemini authentication, but no API key is set and OAuth is disabled. Hint: call /auth gemini enable', 'none', ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
167
211
  }
168
212
  }
169
- this.authMode = 'none';
170
- return token || '';
213
+ return { authMode: 'none', token: token || '' };
171
214
  }
172
215
  catch (error) {
173
216
  // CRITICAL FIX: Only fall back to LOGIN_WITH_GOOGLE if OAuth is actually enabled
174
- // Don't use it when OAuth has been disabled
175
217
  const manager = this.geminiOAuthManager;
176
218
  const isOAuthEnabled = this.geminiOAuthManager &&
177
219
  manager.isOAuthEnabled &&
178
220
  typeof manager.isOAuthEnabled === 'function' &&
179
221
  manager.isOAuthEnabled('gemini');
180
222
  if (isOAuthEnabled) {
181
- this.authMode = 'oauth';
182
- return 'USE_LOGIN_WITH_GOOGLE';
223
+ return { authMode: 'oauth', token: 'USE_LOGIN_WITH_GOOGLE' };
183
224
  }
184
225
  // Handle case where no auth is available
185
226
  const authType = this.globalConfig?.getContentGeneratorConfig()?.authType;
186
227
  if (authType === AuthType.USE_NONE) {
187
- this.authMode = 'none';
188
- throw new AuthenticationRequiredError('Authentication is set to USE_NONE but no credentials are available', this.authMode, ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
228
+ throw new AuthenticationRequiredError('Authentication is set to USE_NONE but no credentials are available', 'none', ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
189
229
  }
190
230
  // When used as serverToolsProvider without API key, fall back to OAuth ONLY if enabled
191
- // This handles the case where Gemini is used for server tools but not as main provider
192
231
  if (!this.hasGeminiAPIKey() && !this.hasVertexAICredentials()) {
193
232
  if (isOAuthEnabled) {
194
- this.authMode = 'oauth';
195
- return 'USE_LOGIN_WITH_GOOGLE';
233
+ return { authMode: 'oauth', token: 'USE_LOGIN_WITH_GOOGLE' };
196
234
  }
197
235
  else {
198
- // OAuth is disabled and no other auth method available
199
236
  throw new AuthenticationRequiredError('Web search requires Gemini authentication, but no API key is set and OAuth is disabled. Hint: call /auth gemini enable', 'none', ['GEMINI_API_KEY', 'GOOGLE_API_KEY']);
200
237
  }
201
238
  }
@@ -253,20 +290,30 @@ export class GeminiProvider extends BaseProvider {
253
290
  setConfig(config) {
254
291
  // Call base provider implementation
255
292
  super.setConfig?.(config);
293
+ this.refreshCachedSettings();
256
294
  // Sync with config model if user hasn't explicitly set a model
257
295
  // This ensures consistency between config and provider state
258
- const configModel = config.getModel();
259
- if (!this.modelExplicitlySet && configModel) {
260
- this.currentModel = configModel;
261
- }
296
+ // @plan:PLAN-20251023-STATELESS-HARDENING.P08 @requirement:REQ-SP4-002
297
+ // Removed model caching in stateless implementation
298
+ // const configModel = config.getModel();
299
+ // if (!this.modelExplicitlySet && configModel) {
300
+ // this.currentModel = configModel;
301
+ // }
262
302
  // Update OAuth configuration based on OAuth manager state, not config authType
263
303
  // This ensures that if OAuth is disabled via /auth gemini disable, it stays disabled
264
304
  this.updateOAuthState();
265
305
  // Clear auth cache when config changes to allow re-determination
266
306
  }
307
+ /**
308
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
309
+ * @requirement:REQ-SP4-002
310
+ * Determine auth mode per call instead of using cached state
311
+ */
267
312
  async getModels() {
313
+ // Determine auth mode for this call
314
+ const { authMode } = await this.determineBestAuth();
268
315
  // For OAuth mode, return fixed list of models
269
- if (this.authMode === 'oauth') {
316
+ if (authMode === 'oauth') {
270
317
  return [
271
318
  {
272
319
  id: 'gemini-2.5-pro',
@@ -289,7 +336,7 @@ export class GeminiProvider extends BaseProvider {
289
336
  ];
290
337
  }
291
338
  // For API key modes (gemini-api-key or vertex-ai), try to fetch real models
292
- if (this.authMode === 'gemini-api-key' || this.authMode === 'vertex-ai') {
339
+ if (authMode === 'gemini-api-key' || authMode === 'vertex-ai') {
293
340
  const apiKey = (await this.getAuthToken()) || process.env.GEMINI_API_KEY;
294
341
  if (apiKey) {
295
342
  try {
@@ -342,35 +389,23 @@ export class GeminiProvider extends BaseProvider {
342
389
  },
343
390
  ];
344
391
  }
345
- setApiKey(apiKey) {
346
- // Call base provider implementation
347
- super.setApiKey(apiKey);
348
- // Set the API key as an environment variable so it can be used by the core library
349
- // CRITICAL FIX: When clearing the key (empty string), delete the env var instead of setting to empty
350
- if (apiKey && apiKey.trim() !== '') {
351
- process.env.GEMINI_API_KEY = apiKey;
352
- }
353
- else {
354
- delete process.env.GEMINI_API_KEY;
355
- }
356
- // Clear auth cache when API key changes
357
- this.clearAuthCache();
358
- }
359
- setBaseUrl(baseUrl) {
360
- // Call base provider implementation which stores in ephemeral settings
361
- super.setBaseUrl?.(baseUrl);
362
- }
363
392
  /**
364
- * Gets the current authentication mode
393
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
394
+ * @requirement:REQ-SP4-002
395
+ * Gets the current authentication mode per call without caching
365
396
  */
366
- getAuthMode() {
367
- return this.authMode;
397
+ async getAuthMode() {
398
+ const { authMode } = await this.determineBestAuth();
399
+ return authMode;
368
400
  }
369
401
  /**
370
- * Gets the appropriate AuthType for the core library
402
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
403
+ * @requirement:REQ-SP4-002
404
+ * Gets the appropriate AuthType for the core library per call
371
405
  */
372
- getCoreAuthType() {
373
- switch (this.authMode) {
406
+ async getCoreAuthType() {
407
+ const { authMode } = await this.determineBestAuth();
408
+ switch (authMode) {
374
409
  case 'oauth':
375
410
  return AuthType.LOGIN_WITH_GOOGLE;
376
411
  case 'gemini-api-key':
@@ -382,22 +417,32 @@ export class GeminiProvider extends BaseProvider {
382
417
  }
383
418
  }
384
419
  /**
385
- * Gets the current model ID
420
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
421
+ * @requirement:REQ-SP4-002
422
+ * No caching - this method is now a no-op. Settings read on demand.
423
+ */
424
+ refreshCachedSettings() {
425
+ // No operation - stateless provider doesn't cache settings
426
+ }
427
+ /**
428
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
429
+ * @requirement:REQ-SP4-003
430
+ * Gets the current model ID from SettingsService per call
386
431
  */
387
432
  getCurrentModel() {
388
433
  // Try to get from SettingsService first (source of truth)
389
434
  try {
390
- const settingsService = getSettingsService();
435
+ const settingsService = this.resolveSettingsService();
391
436
  const providerSettings = settingsService.getProviderSettings(this.name);
392
437
  if (providerSettings.model) {
393
438
  return providerSettings.model;
394
439
  }
395
440
  }
396
441
  catch (error) {
397
- this.logger.debug(() => `Failed to get model from SettingsService: ${error}`);
442
+ this.getLogger().debug(() => `Failed to get model from SettingsService: ${error}`);
398
443
  }
399
- // Fall back to cached value or default
400
- return this.currentModel || this.getDefaultModel();
444
+ // In stateless mode, always return default since model should come from options
445
+ return this.getDefaultModel();
401
446
  }
402
447
  /**
403
448
  * Gets the default model for Gemini
@@ -406,61 +451,61 @@ export class GeminiProvider extends BaseProvider {
406
451
  return 'gemini-2.5-pro';
407
452
  }
408
453
  /**
409
- * Sets the current model ID
454
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
455
+ * @requirement:REQ-SP4-003
456
+ * Gets model parameters from SettingsService per call
410
457
  */
411
- setModel(modelId) {
412
- // Update SettingsService as the source of truth
458
+ getModelParams() {
413
459
  try {
414
- const settingsService = getSettingsService();
415
- settingsService.setProviderSetting(this.name, 'model', modelId);
460
+ const settingsService = this.resolveSettingsService();
461
+ const providerSettings = settingsService.getProviderSettings(this.name);
462
+ const reservedKeys = new Set([
463
+ 'enabled',
464
+ 'apiKey',
465
+ 'api-key',
466
+ 'apiKeyfile',
467
+ 'api-keyfile',
468
+ 'baseUrl',
469
+ 'base-url',
470
+ 'model',
471
+ 'toolFormat',
472
+ 'tool-format',
473
+ 'toolFormatOverride',
474
+ 'tool-format-override',
475
+ 'defaultModel',
476
+ ]);
477
+ const params = {};
478
+ if (providerSettings) {
479
+ for (const [key, value] of Object.entries(providerSettings)) {
480
+ if (reservedKeys.has(key) || value === undefined || value === null) {
481
+ continue;
482
+ }
483
+ params[key] = value;
484
+ }
485
+ }
486
+ return Object.keys(params).length > 0 ? params : undefined;
416
487
  }
417
488
  catch (error) {
418
- this.logger.debug(() => `Failed to persist model to SettingsService: ${error}`);
419
- }
420
- // Keep local cache for performance
421
- this.currentModel = modelId;
422
- this.modelExplicitlySet = true;
423
- // Always update config if available, not just in OAuth mode
424
- // This ensures the model is properly synchronized
425
- if (this.globalConfig) {
426
- this.globalConfig.setModel(modelId);
427
- }
428
- }
429
- /**
430
- * Sets additional model parameters to include in requests
431
- */
432
- setModelParams(params) {
433
- if (params === undefined) {
434
- this.modelParams = undefined;
435
- }
436
- else {
437
- this.modelParams = { ...this.modelParams, ...params };
489
+ this.getLogger().debug(() => `Failed to get Gemini provider settings from SettingsService: ${error}`);
490
+ return undefined;
438
491
  }
439
492
  }
440
493
  /**
441
- * Gets the current model parameters
442
- */
443
- getModelParams() {
444
- return this.modelParams;
445
- }
446
- /**
447
- * Checks if the current auth mode requires payment
494
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
495
+ * @requirement:REQ-SP4-002
496
+ * Checks if using paid mode synchronously via env vars (stateless check)
448
497
  */
449
498
  isPaidMode() {
450
- return this.authMode === 'gemini-api-key' || this.authMode === 'vertex-ai';
499
+ // Synchronous check based on environment variables only
500
+ return this.hasGeminiAPIKey() || this.hasVertexAICredentials();
451
501
  }
452
502
  /**
453
- * Clears provider state but preserves explicitly set model
503
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
504
+ * @requirement:REQ-SP4-002
505
+ * No state to clear in stateless implementation
454
506
  */
455
507
  clearState() {
456
- // Clear auth-related state
457
- this.authMode = 'none';
458
- // Only reset model if it wasn't explicitly set by user
459
- if (!this.modelExplicitlySet) {
460
- this.currentModel = 'gemini-2.5-pro';
461
- }
462
- // Note: We don't clear config or apiKey as they might be needed
463
- // Clear auth cache
508
+ // No operation - stateless provider has no state to clear
464
509
  this.clearAuthCache();
465
510
  }
466
511
  /**
@@ -478,6 +523,7 @@ export class GeminiProvider extends BaseProvider {
478
523
  clearAuthCache() {
479
524
  // Call the base implementation to clear the cached token
480
525
  super.clearAuthCache();
526
+ this.clearClientCache();
481
527
  // Don't clear the auth mode itself, just allow re-determination next time
482
528
  }
483
529
  /**
@@ -487,30 +533,39 @@ export class GeminiProvider extends BaseProvider {
487
533
  return ['web_search', 'web_fetch'];
488
534
  }
489
535
  /**
490
- * Invoke a server tool (native provider tool)
536
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
537
+ * @requirement:REQ-SP4-002
538
+ * Invoke a server tool using per-call auth resolution
491
539
  */
492
- async invokeServerTool(toolName, params, _config) {
540
+ async invokeServerTool(toolName, params, _config, _signal) {
493
541
  if (toolName === 'web_search') {
494
- this.logger.debug(() => `invokeServerTool: web_search called with params: ${JSON.stringify(params)}`);
495
- this.logger.debug(() => `invokeServerTool: globalConfig is ${this.globalConfig ? 'set' : 'null/undefined'}`);
496
- this.logger.debug(() => `invokeServerTool: current authMode is ${this.authMode}`);
542
+ const logger = this.getToolsLogger();
543
+ logger.debug(() => `invokeServerTool: web_search called with params: ${JSON.stringify(params)}`);
544
+ logger.debug(() => `invokeServerTool: globalConfig is ${this.globalConfig ? 'set' : 'null/undefined'}`);
545
+ // Check for abort before auth determination
546
+ if (_signal?.aborted) {
547
+ const error = new Error('Operation was aborted');
548
+ error.name = 'AbortError';
549
+ throw error;
550
+ }
497
551
  // Import the necessary modules dynamically
498
552
  const { GoogleGenAI } = await import('@google/genai');
499
553
  // Create the appropriate client based on auth mode
500
554
  const httpOptions = this.createHttpOptions();
501
555
  let genAI;
502
- // Get authentication token lazily
503
- this.logger.debug(() => `invokeServerTool: about to call determineBestAuth()`);
504
- const authToken = await this.determineBestAuth();
505
- this.logger.debug(() => `invokeServerTool: determineBestAuth returned, authMode is now ${this.authMode}`);
506
- // Re-evaluate auth mode if we got a signal to use OAuth
507
- if (authToken === 'USE_LOGIN_WITH_GOOGLE') {
508
- this.authMode = 'oauth';
556
+ // Get authentication token and mode lazily per call
557
+ logger.debug(() => `invokeServerTool: about to call determineBestAuth()`);
558
+ const { authMode, token: authToken } = await this.determineBestAuth();
559
+ // Check for abort after auth determination
560
+ if (_signal?.aborted) {
561
+ const error = new Error('Operation was aborted');
562
+ error.name = 'AbortError';
563
+ throw error;
509
564
  }
510
- switch (this.authMode) {
565
+ logger.debug(() => `invokeServerTool: determineBestAuth returned authMode=${authMode}`);
566
+ switch (authMode) {
511
567
  case 'gemini-api-key': {
512
- // This case should never happen if determineBestAuth worked correctly
513
- // but add safety check
568
+ // Validate auth token
514
569
  if (!authToken ||
515
570
  authToken === 'USE_LOGIN_WITH_GOOGLE' ||
516
571
  authToken === '') {
@@ -572,12 +627,12 @@ export class GeminiProvider extends BaseProvider {
572
627
  }
573
628
  case 'oauth': {
574
629
  try {
575
- this.logger.debug(() => `invokeServerTool: OAuth case - creating content generator`);
630
+ logger.debug(() => `invokeServerTool: OAuth case - creating content generator`);
576
631
  // If globalConfig is not set (e.g., when using non-Gemini provider),
577
632
  // create a minimal config for OAuth
578
633
  let configForOAuth = this.globalConfig;
579
634
  if (!configForOAuth) {
580
- this.logger.debug(() => `invokeServerTool: globalConfig is null, creating minimal config for OAuth`);
635
+ logger.debug(() => `invokeServerTool: globalConfig is null, creating minimal config for OAuth`);
581
636
  // Use crypto for UUID generation
582
637
  const { randomUUID } = await import('crypto');
583
638
  configForOAuth = new Config({
@@ -591,8 +646,8 @@ export class GeminiProvider extends BaseProvider {
591
646
  }
592
647
  // For OAuth, use the code assist content generator
593
648
  // Note: Detailed logging is now handled by DebugLogger in codeAssist.ts with namespace llxprt:code:assist
594
- const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, configForOAuth);
595
- this.logger.debug(() => `invokeServerTool: OAuth content generator created successfully`);
649
+ const oauthContentGenerator = await this.createOAuthContentGenerator(httpOptions, configForOAuth);
650
+ logger.debug(() => `invokeServerTool: OAuth content generator created successfully`);
596
651
  // For web search, always use gemini-2.5-flash regardless of the active model
597
652
  const oauthRequest = {
598
653
  model: 'gemini-2.5-flash',
@@ -606,23 +661,29 @@ export class GeminiProvider extends BaseProvider {
606
661
  tools: [{ googleSearch: {} }],
607
662
  },
608
663
  };
609
- this.logger.debug(() => `invokeServerTool: making OAuth generateContent request with query: ${params.query}`);
664
+ logger.debug(() => `invokeServerTool: making OAuth generateContent request with query: ${params.query}`);
610
665
  // PRIVACY FIX: Removed sessionId to prevent transmission to Google servers
611
666
  const result = await oauthContentGenerator.generateContent(oauthRequest, 'web-search-oauth');
612
- this.logger.debug(() => `invokeServerTool: OAuth generateContent completed successfully`);
667
+ logger.debug(() => `invokeServerTool: OAuth generateContent completed successfully`);
613
668
  return result;
614
669
  }
615
670
  catch (error) {
616
- this.logger.debug(() => `invokeServerTool: ERROR in OAuth case: ${error}`);
617
- this.logger.debug(() => `invokeServerTool: Error details:`, error);
671
+ logger.debug(() => `invokeServerTool: ERROR in OAuth case: ${error}`);
672
+ logger.debug(() => `invokeServerTool: Error details:`, error);
618
673
  throw error;
619
674
  }
620
675
  }
621
676
  default:
622
- throw new Error(`Web search not supported in auth mode: ${this.authMode}`);
677
+ throw new Error(`Web search not supported in auth mode: ${authMode}`);
623
678
  }
624
679
  }
625
680
  else if (toolName === 'web_fetch') {
681
+ // Check for abort before auth determination
682
+ if (_signal?.aborted) {
683
+ const error = new Error('Operation was aborted');
684
+ error.name = 'AbortError';
685
+ throw error;
686
+ }
626
687
  // Import the necessary modules dynamically
627
688
  const { GoogleGenAI } = await import('@google/genai');
628
689
  // Get the prompt directly without any processing
@@ -630,9 +691,15 @@ export class GeminiProvider extends BaseProvider {
630
691
  // Create the appropriate client based on auth mode
631
692
  const httpOptions = this.createHttpOptions();
632
693
  let genAI;
633
- // Get authentication token lazily
634
- const authToken = await this.determineBestAuth();
635
- switch (this.authMode) {
694
+ // Get authentication token and mode lazily per call
695
+ const { authMode, token: authToken } = await this.determineBestAuth();
696
+ // Check for abort after auth determination
697
+ if (_signal?.aborted) {
698
+ const error = new Error('Operation was aborted');
699
+ error.name = 'AbortError';
700
+ throw error;
701
+ }
702
+ switch (authMode) {
636
703
  case 'gemini-api-key': {
637
704
  genAI = new GoogleGenAI({
638
705
  apiKey: authToken,
@@ -690,7 +757,7 @@ export class GeminiProvider extends BaseProvider {
690
757
  }
691
758
  case 'oauth': {
692
759
  // For OAuth, use the code assist content generator
693
- const oauthContentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, this.globalConfig);
760
+ const oauthContentGenerator = await this.createOAuthContentGenerator(httpOptions, this.globalConfig, this.getBaseURL());
694
761
  // For web fetch, always use gemini-2.5-flash regardless of the active model
695
762
  const oauthRequest = {
696
763
  model: 'gemini-2.5-flash',
@@ -709,7 +776,7 @@ export class GeminiProvider extends BaseProvider {
709
776
  return result;
710
777
  }
711
778
  default:
712
- throw new Error(`Web fetch not supported in auth mode: ${this.authMode}`);
779
+ throw new Error(`Web fetch not supported in auth mode: ${authMode}`);
713
780
  }
714
781
  }
715
782
  else {
@@ -717,32 +784,18 @@ export class GeminiProvider extends BaseProvider {
717
784
  }
718
785
  }
719
786
  /**
720
- * Generate chat completion with IContent interface
721
- * Direct implementation for Gemini API with IContent interface
787
+ * @plan:PLAN-20251023-STATELESS-HARDENING.P08
788
+ * @requirement:REQ-SP4-002, REQ-SP4-003
789
+ * Generate chat completion using per-call resolution of auth, model, and params
790
+ * All state comes from NormalizedGenerateChatOptions, no instance caching
722
791
  */
723
- async *generateChatCompletion(content, tools, signal, _toolFormat) {
724
- // First critical AbortSignal check for generateChatCompletion
725
- if (signal?.aborted) {
726
- const error = new Error('Operation was aborted');
727
- error.name = 'AbortError';
728
- throw error;
729
- }
730
- // AbortSignal check before auth
731
- if (signal?.aborted) {
732
- const error = new Error('Operation was aborted');
733
- error.name = 'AbortError';
734
- throw error;
735
- }
736
- // Determine best auth method
737
- const authToken = await this.determineBestAuth();
738
- // AbortSignal check after auth
739
- if (signal?.aborted) {
740
- const error = new Error('Operation was aborted');
741
- error.name = 'AbortError';
742
- throw error;
743
- }
744
- // Import necessary modules
745
- const { GoogleGenAI } = await import('@google/genai');
792
+ async *generateChatCompletionWithOptions(options) {
793
+ const streamingEnabled = this.getStreamingPreference(options);
794
+ const { contents: content, tools } = options;
795
+ // Determine best auth method per call - no state storage
796
+ const { authMode, token: authToken } = await this.determineBestAuth();
797
+ // Model is resolved from options.resolved.model - no instance caching
798
+ const currentModel = options.resolved.model;
746
799
  // Convert IContent directly to Gemini format
747
800
  const contents = [];
748
801
  for (const c of content) {
@@ -824,116 +877,27 @@ export class GeminiProvider extends BaseProvider {
824
877
  }),
825
878
  }))
826
879
  : undefined;
827
- const flattenedToolNames = tools?.flatMap((group) => group.functionDeclarations
828
- .map((decl) => decl.name)
829
- .filter((name) => !!name)) ?? [];
830
- const toolNamesArg = tools === undefined ? undefined : Array.from(new Set(flattenedToolNames));
880
+ const toolNamesForPrompt = tools === undefined
881
+ ? undefined
882
+ : Array.from(new Set(tools.flatMap((group) => group.functionDeclarations
883
+ .map((decl) => decl.name)
884
+ .filter((name) => Boolean(name)))));
885
+ const serverTools = ['web_search', 'web_fetch'];
886
+ // @plan:PLAN-20251023-STATELESS-HARDENING.P08 @requirement:REQ-SP4-003
887
+ // Get model params per call from ephemeral settings, not cached instance state
888
+ const requestOverrides = options.invocation?.ephemerals ?? {};
889
+ const requestConfig = {
890
+ serverTools,
891
+ ...(requestOverrides ?? {}),
892
+ };
893
+ if (geminiTools) {
894
+ requestConfig.tools = geminiTools;
895
+ }
831
896
  // Create appropriate client and generate content
897
+ const baseURL = options.resolved.baseURL ?? this.getBaseURL();
832
898
  const httpOptions = this.createHttpOptions();
833
- const { maxAttempts, initialDelayMs } = this.getRetryConfig();
834
- const stream = await retryWithBackoff(async () => {
835
- // AbortSignal check before client creation
836
- if (signal?.aborted) {
837
- const error = new Error('Operation was aborted');
838
- error.name = 'AbortError';
839
- throw error;
840
- }
841
- if (this.authMode === 'oauth') {
842
- // OAuth mode
843
- const configForOAuth = this.globalConfig || {
844
- getProxy: () => undefined,
845
- isBrowserLaunchSuppressed: () => false,
846
- getNoBrowser: () => false,
847
- getUserMemory: () => '',
848
- };
849
- const contentGenerator = await createCodeAssistContentGenerator(httpOptions, AuthType.LOGIN_WITH_GOOGLE, configForOAuth, this.getBaseURL());
850
- const userMemory = this.globalConfig?.getUserMemory
851
- ? this.globalConfig.getUserMemory()
852
- : '';
853
- const systemInstruction = await getCoreSystemPromptAsync(userMemory, this.currentModel, toolNamesArg);
854
- // For OAuth/CodeAssist mode, inject system prompt as first user message
855
- // This ensures the CodeAssist endpoint receives the full context
856
- // Similar to how AnthropicProvider handles OAuth mode
857
- const contentsWithSystemPrompt = [
858
- {
859
- role: 'user',
860
- parts: [
861
- {
862
- text: `<system>\n${systemInstruction}\n</system>\n\nUser provided conversation begins here:`,
863
- },
864
- ],
865
- },
866
- ...contents,
867
- ];
868
- // Streaming request abort check
869
- if (signal?.aborted) {
870
- const error = new Error('Operation was aborted');
871
- error.name = 'AbortError';
872
- throw error;
873
- }
874
- const request = {
875
- model: this.currentModel,
876
- contents: contentsWithSystemPrompt,
877
- // Still pass systemInstruction for SDK compatibility
878
- systemInstruction,
879
- config: {
880
- tools: geminiTools,
881
- ...this.modelParams,
882
- },
883
- };
884
- return await contentGenerator.generateContentStream(request, 'oauth-session');
885
- }
886
- else {
887
- // API key mode
888
- // AbortSignal check before client creation
889
- if (signal?.aborted) {
890
- const error = new Error('Operation was aborted');
891
- error.name = 'AbortError';
892
- throw error;
893
- }
894
- const genAI = new GoogleGenAI({
895
- apiKey: authToken,
896
- vertexai: this.authMode === 'vertex-ai',
897
- httpOptions: this.getBaseURL()
898
- ? { ...httpOptions, baseUrl: this.getBaseURL() }
899
- : httpOptions,
900
- });
901
- const contentGenerator = genAI.models;
902
- const userMemory = this.globalConfig?.getUserMemory
903
- ? this.globalConfig.getUserMemory()
904
- : '';
905
- const systemInstruction = await getCoreSystemPromptAsync(userMemory, this.currentModel, toolNamesArg);
906
- // Streaming request abort check
907
- if (signal?.aborted) {
908
- const error = new Error('Operation was aborted');
909
- error.name = 'AbortError';
910
- throw error;
911
- }
912
- const request = {
913
- model: this.currentModel,
914
- contents,
915
- systemInstruction,
916
- config: {
917
- tools: geminiTools,
918
- ...this.modelParams,
919
- },
920
- };
921
- return await contentGenerator.generateContentStream(request);
922
- }
923
- }, {
924
- maxAttempts,
925
- initialDelayMs,
926
- shouldRetry: this.shouldRetryGeminiResponse.bind(this),
927
- trackThrottleWaitTime: this.throttleTracker,
928
- });
929
- // Stream responses as IContent
930
- for await (const response of stream) {
931
- // Critical streaming loop abort check
932
- if (signal?.aborted) {
933
- const error = new Error('Operation was aborted');
934
- error.name = 'AbortError';
935
- throw error;
936
- }
899
+ const mapResponseToChunks = (response) => {
900
+ const chunks = [];
937
901
  const text = response.candidates?.[0]?.content?.parts
938
902
  ?.filter((part) => 'text' in part)
939
903
  ?.map((part) => part.text)
@@ -941,15 +905,12 @@ export class GeminiProvider extends BaseProvider {
941
905
  const functionCalls = response.candidates?.[0]?.content?.parts
942
906
  ?.filter((part) => 'functionCall' in part)
943
907
  ?.map((part) => part.functionCall) || [];
944
- // Extract usage metadata from response
945
908
  const usageMetadata = response.usageMetadata;
946
- // Yield text if present
947
909
  if (text) {
948
910
  const textContent = {
949
911
  speaker: 'ai',
950
912
  blocks: [{ type: 'text', text }],
951
913
  };
952
- // Add usage metadata if present
953
914
  if (usageMetadata) {
954
915
  textContent.metadata = {
955
916
  usage: {
@@ -961,9 +922,8 @@ export class GeminiProvider extends BaseProvider {
961
922
  },
962
923
  };
963
924
  }
964
- yield textContent;
925
+ chunks.push(textContent);
965
926
  }
966
- // Yield tool calls if present
967
927
  if (functionCalls.length > 0) {
968
928
  const blocks = functionCalls.map((call) => ({
969
929
  type: 'tool_call',
@@ -976,7 +936,6 @@ export class GeminiProvider extends BaseProvider {
976
936
  speaker: 'ai',
977
937
  blocks,
978
938
  };
979
- // Add usage metadata if present
980
939
  if (usageMetadata) {
981
940
  toolCallContent.metadata = {
982
941
  usage: {
@@ -988,11 +947,10 @@ export class GeminiProvider extends BaseProvider {
988
947
  },
989
948
  };
990
949
  }
991
- yield toolCallContent;
950
+ chunks.push(toolCallContent);
992
951
  }
993
- // If we have usage metadata but no content blocks, emit a metadata-only response
994
952
  if (usageMetadata && !text && functionCalls.length === 0) {
995
- yield {
953
+ chunks.push({
996
954
  speaker: 'ai',
997
955
  blocks: [],
998
956
  metadata: {
@@ -1004,27 +962,144 @@ export class GeminiProvider extends BaseProvider {
1004
962
  (usageMetadata.candidatesTokenCount || 0),
1005
963
  },
1006
964
  },
965
+ });
966
+ }
967
+ if (!usageMetadata && !text && functionCalls.length === 0) {
968
+ chunks.push({
969
+ speaker: 'ai',
970
+ blocks: [],
971
+ });
972
+ }
973
+ return chunks;
974
+ };
975
+ let stream = null;
976
+ let emitted = false;
977
+ // @plan:PLAN-20251023-STATELESS-HARDENING.P08 @requirement:REQ-SP4-002
978
+ // No caching - create client per call based on resolved authMode
979
+ if (authMode === 'oauth') {
980
+ const configForOAuth = this.globalConfig || {
981
+ getProxy: () => undefined,
982
+ isBrowserLaunchSuppressed: () => false,
983
+ getNoBrowser: () => false,
984
+ getUserMemory: () => '',
985
+ };
986
+ // Create OAuth content generator per call - no caching
987
+ const contentGenerator = await this.createOAuthContentGenerator(httpOptions, configForOAuth, baseURL);
988
+ const userMemory = this.globalConfig?.getUserMemory
989
+ ? this.globalConfig.getUserMemory()
990
+ : '';
991
+ const systemInstruction = await getCoreSystemPromptAsync(userMemory, currentModel, toolNamesForPrompt);
992
+ const contentsWithSystemPrompt = [
993
+ {
994
+ role: 'user',
995
+ parts: [
996
+ {
997
+ text: `<system>\n${systemInstruction}\n</system>\n\nUser provided conversation begins here:`,
998
+ },
999
+ ],
1000
+ },
1001
+ ...contents,
1002
+ ];
1003
+ const oauthConfig = { ...requestConfig };
1004
+ const oauthRequest = {
1005
+ model: currentModel,
1006
+ contents: contentsWithSystemPrompt,
1007
+ systemInstruction,
1008
+ config: oauthConfig,
1009
+ };
1010
+ // Use runtime metadata from options for session ID
1011
+ const runtimeId = options.runtime?.runtimeId || 'default';
1012
+ const sessionId = `oauth-session:${runtimeId}:${Math.random()
1013
+ .toString(36)
1014
+ .slice(2)}`;
1015
+ const generatorWithStream = contentGenerator;
1016
+ if (!streamingEnabled && generatorWithStream.generateContent) {
1017
+ const response = await generatorWithStream.generateContent(oauthRequest, sessionId);
1018
+ let yielded = false;
1019
+ for (const chunk of mapResponseToChunks(response)) {
1020
+ yielded = true;
1021
+ yield chunk;
1022
+ }
1023
+ if (!yielded) {
1024
+ yield { speaker: 'ai', blocks: [] };
1025
+ }
1026
+ return;
1027
+ }
1028
+ const oauthStream = await generatorWithStream.generateContentStream(oauthRequest, sessionId);
1029
+ stream = oauthStream;
1030
+ if (streamingEnabled) {
1031
+ emitted = true;
1032
+ yield {
1033
+ speaker: 'ai',
1034
+ blocks: [],
1035
+ metadata: {
1036
+ session: sessionId,
1037
+ runtime: runtimeId,
1038
+ authMode: 'oauth',
1039
+ },
1007
1040
  };
1008
1041
  }
1009
1042
  }
1010
- }
1011
- getRetryConfig() {
1012
- const ephemeralSettings = this.providerConfig?.getEphemeralSettings?.() || {};
1013
- const maxAttempts = ephemeralSettings['retries'] ?? 6;
1014
- const initialDelayMs = ephemeralSettings['retrywait'] ?? 4000;
1015
- return { maxAttempts, initialDelayMs };
1016
- }
1017
- shouldRetryGeminiResponse(error) {
1018
- const status = getErrorStatus(error);
1019
- if (status === 429 || (status && status >= 500 && status < 600)) {
1020
- this.logger.debug(() => `Will retry Gemini request due to status ${status}`);
1021
- return true;
1043
+ else {
1044
+ // @plan:PLAN-20251023-STATELESS-HARDENING.P08 @requirement:REQ-SP4-002
1045
+ // Create Google GenAI client per call - no caching
1046
+ const { GoogleGenAI } = await import('@google/genai');
1047
+ const genAI = new GoogleGenAI({
1048
+ apiKey: authToken,
1049
+ vertexai: authMode === 'vertex-ai',
1050
+ httpOptions: baseURL
1051
+ ? { ...httpOptions, baseUrl: baseURL }
1052
+ : httpOptions,
1053
+ });
1054
+ const contentGenerator = genAI.models;
1055
+ const userMemory = this.globalConfig?.getUserMemory
1056
+ ? this.globalConfig.getUserMemory()
1057
+ : '';
1058
+ const systemInstruction = await getCoreSystemPromptAsync(userMemory, currentModel, toolNamesForPrompt);
1059
+ const apiRequest = {
1060
+ model: currentModel,
1061
+ contents,
1062
+ systemInstruction,
1063
+ config: { ...requestConfig },
1064
+ };
1065
+ if (streamingEnabled) {
1066
+ stream = await contentGenerator.generateContentStream(apiRequest);
1067
+ }
1068
+ else {
1069
+ const response = await contentGenerator.generateContent(apiRequest);
1070
+ let yielded = false;
1071
+ for (const chunk of mapResponseToChunks(response)) {
1072
+ yielded = true;
1073
+ yield chunk;
1074
+ }
1075
+ if (!yielded) {
1076
+ yield { speaker: 'ai', blocks: [] };
1077
+ }
1078
+ return;
1079
+ }
1022
1080
  }
1023
- if (isNetworkTransientError(error)) {
1024
- this.logger.debug(() => 'Will retry Gemini request due to transient network error signature.');
1025
- return true;
1081
+ if (stream) {
1082
+ const iterator = typeof stream[Symbol.asyncIterator] === 'function'
1083
+ ? stream[Symbol.asyncIterator]()
1084
+ : stream;
1085
+ while (true) {
1086
+ const { value, done } = await iterator.next();
1087
+ if (done) {
1088
+ break;
1089
+ }
1090
+ const mapped = mapResponseToChunks(value);
1091
+ if (mapped.length === 0) {
1092
+ continue;
1093
+ }
1094
+ emitted = true;
1095
+ for (const chunk of mapped) {
1096
+ yield chunk;
1097
+ }
1098
+ }
1099
+ }
1100
+ if (!emitted) {
1101
+ yield { speaker: 'ai', blocks: [] };
1026
1102
  }
1027
- return false;
1028
1103
  }
1029
1104
  }
1030
1105
  //# sourceMappingURL=GeminiProvider.js.map