@vybestack/llxprt-code-core 0.1.21 → 0.1.23

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 (199) hide show
  1. package/dist/src/auth/types.d.ts +4 -4
  2. package/dist/src/code_assist/codeAssist.d.ts +1 -1
  3. package/dist/src/code_assist/codeAssist.js +5 -2
  4. package/dist/src/code_assist/codeAssist.js.map +1 -1
  5. package/dist/src/code_assist/converter.d.ts +2 -1
  6. package/dist/src/code_assist/converter.js +6 -4
  7. package/dist/src/code_assist/converter.js.map +1 -1
  8. package/dist/src/code_assist/oauth2.d.ts +1 -0
  9. package/dist/src/code_assist/oauth2.js +32 -15
  10. package/dist/src/code_assist/oauth2.js.map +1 -1
  11. package/dist/src/code_assist/server.d.ts +1 -2
  12. package/dist/src/code_assist/server.js +10 -5
  13. package/dist/src/code_assist/server.js.map +1 -1
  14. package/dist/src/code_assist/setup.js +2 -1
  15. package/dist/src/code_assist/setup.js.map +1 -1
  16. package/dist/src/config/config.d.ts +14 -1
  17. package/dist/src/config/config.js +28 -3
  18. package/dist/src/config/config.js.map +1 -1
  19. package/dist/src/core/client.js +13 -4
  20. package/dist/src/core/client.js.map +1 -1
  21. package/dist/src/core/coreToolScheduler.d.ts +2 -1
  22. package/dist/src/core/coreToolScheduler.js +23 -0
  23. package/dist/src/core/coreToolScheduler.js.map +1 -1
  24. package/dist/src/core/geminiChat.js +73 -17
  25. package/dist/src/core/geminiChat.js.map +1 -1
  26. package/dist/src/core/loggingContentGenerator.d.ts +1 -5
  27. package/dist/src/core/loggingContentGenerator.js +4 -6
  28. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  29. package/dist/src/core/nonInteractiveToolExecutor.d.ts +2 -2
  30. package/dist/src/core/nonInteractiveToolExecutor.js +13 -9
  31. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  32. package/dist/src/core/subagent.js +5 -5
  33. package/dist/src/core/subagent.js.map +1 -1
  34. package/dist/src/debug/ConfigurationManager.d.ts +28 -0
  35. package/dist/src/debug/ConfigurationManager.js +211 -0
  36. package/dist/src/debug/ConfigurationManager.js.map +1 -0
  37. package/dist/src/debug/DebugLogger.d.ts +26 -0
  38. package/dist/src/debug/DebugLogger.js +230 -0
  39. package/dist/src/debug/DebugLogger.js.map +1 -0
  40. package/dist/src/debug/FileOutput.d.ts +23 -0
  41. package/dist/src/debug/FileOutput.js +144 -0
  42. package/dist/src/debug/FileOutput.js.map +1 -0
  43. package/dist/src/debug/MockConfigurationManager.d.ts +28 -0
  44. package/dist/src/debug/MockConfigurationManager.js +45 -0
  45. package/dist/src/debug/MockConfigurationManager.js.map +1 -0
  46. package/dist/src/debug/MockFileOutput.d.ts +20 -0
  47. package/dist/src/debug/MockFileOutput.js +25 -0
  48. package/dist/src/debug/MockFileOutput.js.map +1 -0
  49. package/dist/src/debug/index.d.ts +8 -0
  50. package/dist/src/debug/index.js +9 -0
  51. package/dist/src/debug/index.js.map +1 -0
  52. package/dist/src/debug/types.d.ts +23 -0
  53. package/dist/src/debug/types.js +2 -0
  54. package/dist/src/debug/types.js.map +1 -0
  55. package/dist/src/ide/ide-client.d.ts +4 -0
  56. package/dist/src/ide/ide-client.js +41 -2
  57. package/dist/src/ide/ide-client.js.map +1 -1
  58. package/dist/src/ide/ide-installer.js +17 -4
  59. package/dist/src/ide/ide-installer.js.map +1 -1
  60. package/dist/src/ide/process-utils.d.ts +10 -5
  61. package/dist/src/ide/process-utils.js +113 -30
  62. package/dist/src/ide/process-utils.js.map +1 -1
  63. package/dist/src/index.d.ts +5 -0
  64. package/dist/src/index.js +4 -0
  65. package/dist/src/index.js.map +1 -1
  66. package/dist/src/mcp/oauth-provider.js +176 -58
  67. package/dist/src/mcp/oauth-provider.js.map +1 -1
  68. package/dist/src/mcp/oauth-utils.d.ts +3 -1
  69. package/dist/src/mcp/oauth-utils.js +52 -22
  70. package/dist/src/mcp/oauth-utils.js.map +1 -1
  71. package/dist/src/prompt-config/defaults/core.md +34 -230
  72. package/dist/src/prompt-config/defaults/tools/read-file.md +1 -1
  73. package/dist/src/providers/BaseProvider.js +13 -2
  74. package/dist/src/providers/BaseProvider.js.map +1 -1
  75. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +1 -0
  76. package/dist/src/providers/anthropic/AnthropicProvider.js +165 -127
  77. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  78. package/dist/src/providers/gemini/GeminiProvider.d.ts +1 -0
  79. package/dist/src/providers/gemini/GeminiProvider.js +29 -53
  80. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  81. package/dist/src/providers/openai/OpenAIProvider.d.ts +9 -0
  82. package/dist/src/providers/openai/OpenAIProvider.js +671 -128
  83. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  84. package/dist/src/providers/openai/buildResponsesRequest.js +9 -6
  85. package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
  86. package/dist/src/providers/openai/getOpenAIProviderInfo.js +8 -3
  87. package/dist/src/providers/openai/getOpenAIProviderInfo.js.map +1 -1
  88. package/dist/src/providers/openai/parseResponsesStream.js +13 -4
  89. package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
  90. package/dist/src/providers/openai/syntheticToolResponses.d.ts +0 -9
  91. package/dist/src/providers/openai/syntheticToolResponses.js +87 -16
  92. package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
  93. package/dist/src/services/fileSystemService.d.ts +31 -0
  94. package/dist/src/services/fileSystemService.js +18 -0
  95. package/dist/src/services/fileSystemService.js.map +1 -0
  96. package/dist/src/services/loopDetectionService.d.ts +5 -0
  97. package/dist/src/services/loopDetectionService.js +89 -1
  98. package/dist/src/services/loopDetectionService.js.map +1 -1
  99. package/dist/src/services/shellExecutionService.d.ts +8 -6
  100. package/dist/src/services/shellExecutionService.js +297 -133
  101. package/dist/src/services/shellExecutionService.js.map +1 -1
  102. package/dist/src/telemetry/index.d.ts +2 -2
  103. package/dist/src/telemetry/index.js +2 -2
  104. package/dist/src/telemetry/index.js.map +1 -1
  105. package/dist/src/telemetry/loggers.d.ts +2 -1
  106. package/dist/src/telemetry/loggers.js +15 -1
  107. package/dist/src/telemetry/loggers.js.map +1 -1
  108. package/dist/src/telemetry/metrics.d.ts +1 -1
  109. package/dist/src/telemetry/metrics.js +2 -1
  110. package/dist/src/telemetry/metrics.js.map +1 -1
  111. package/dist/src/telemetry/sdk.js +3 -3
  112. package/dist/src/telemetry/sdk.js.map +1 -1
  113. package/dist/src/telemetry/types.d.ts +9 -1
  114. package/dist/src/telemetry/types.js +32 -0
  115. package/dist/src/telemetry/types.js.map +1 -1
  116. package/dist/src/telemetry/uiTelemetry.d.ts +4 -0
  117. package/dist/src/telemetry/uiTelemetry.js +16 -1
  118. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  119. package/dist/src/test-utils/tools.d.ts +29 -8
  120. package/dist/src/test-utils/tools.js +79 -15
  121. package/dist/src/test-utils/tools.js.map +1 -1
  122. package/dist/src/tools/ToolFormatter.d.ts +1 -0
  123. package/dist/src/tools/ToolFormatter.js +90 -9
  124. package/dist/src/tools/ToolFormatter.js.map +1 -1
  125. package/dist/src/tools/edit.d.ts +2 -2
  126. package/dist/src/tools/edit.js +60 -28
  127. package/dist/src/tools/edit.js.map +1 -1
  128. package/dist/src/tools/glob.d.ts +1 -1
  129. package/dist/src/tools/glob.js +8 -8
  130. package/dist/src/tools/glob.js.map +1 -1
  131. package/dist/src/tools/grep.d.ts +1 -1
  132. package/dist/src/tools/grep.js +1 -6
  133. package/dist/src/tools/grep.js.map +1 -1
  134. package/dist/src/tools/ls.d.ts +1 -1
  135. package/dist/src/tools/ls.js +1 -6
  136. package/dist/src/tools/ls.js.map +1 -1
  137. package/dist/src/tools/mcp-client-manager.d.ts +38 -0
  138. package/dist/src/tools/mcp-client-manager.js +74 -0
  139. package/dist/src/tools/mcp-client-manager.js.map +1 -0
  140. package/dist/src/tools/mcp-client.d.ts +43 -0
  141. package/dist/src/tools/mcp-client.js +163 -32
  142. package/dist/src/tools/mcp-client.js.map +1 -1
  143. package/dist/src/tools/memoryTool.d.ts +1 -1
  144. package/dist/src/tools/memoryTool.js +1 -6
  145. package/dist/src/tools/memoryTool.js.map +1 -1
  146. package/dist/src/tools/read-file.d.ts +1 -1
  147. package/dist/src/tools/read-file.js +5 -7
  148. package/dist/src/tools/read-file.js.map +1 -1
  149. package/dist/src/tools/read-many-files.d.ts +0 -1
  150. package/dist/src/tools/read-many-files.js +16 -11
  151. package/dist/src/tools/read-many-files.js.map +1 -1
  152. package/dist/src/tools/shell.d.ts +1 -1
  153. package/dist/src/tools/shell.js +57 -36
  154. package/dist/src/tools/shell.js.map +1 -1
  155. package/dist/src/tools/tool-error.d.ts +1 -0
  156. package/dist/src/tools/tool-error.js +1 -0
  157. package/dist/src/tools/tool-error.js.map +1 -1
  158. package/dist/src/tools/tool-registry.d.ts +5 -0
  159. package/dist/src/tools/tool-registry.js +13 -4
  160. package/dist/src/tools/tool-registry.js.map +1 -1
  161. package/dist/src/tools/tools.d.ts +56 -76
  162. package/dist/src/tools/tools.js +119 -106
  163. package/dist/src/tools/tools.js.map +1 -1
  164. package/dist/src/tools/web-fetch.d.ts +1 -1
  165. package/dist/src/tools/web-fetch.js +1 -6
  166. package/dist/src/tools/web-fetch.js.map +1 -1
  167. package/dist/src/tools/web-search.d.ts +6 -1
  168. package/dist/src/tools/web-search.js +6 -6
  169. package/dist/src/tools/web-search.js.map +1 -1
  170. package/dist/src/tools/write-file.d.ts +1 -1
  171. package/dist/src/tools/write-file.js +7 -8
  172. package/dist/src/tools/write-file.js.map +1 -1
  173. package/dist/src/utils/environmentContext.js +2 -2
  174. package/dist/src/utils/environmentContext.js.map +1 -1
  175. package/dist/src/utils/errorReporting.js +1 -1
  176. package/dist/src/utils/fileUtils.d.ts +2 -1
  177. package/dist/src/utils/fileUtils.js +3 -3
  178. package/dist/src/utils/fileUtils.js.map +1 -1
  179. package/dist/src/utils/filesearch/crawler.d.ts +15 -0
  180. package/dist/src/utils/filesearch/crawler.js +50 -0
  181. package/dist/src/utils/filesearch/crawler.js.map +1 -0
  182. package/dist/src/utils/filesearch/fileSearch.d.ts +9 -53
  183. package/dist/src/utils/filesearch/fileSearch.js +62 -117
  184. package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
  185. package/dist/src/utils/filesearch/ignore.d.ts +7 -0
  186. package/dist/src/utils/filesearch/ignore.js +25 -0
  187. package/dist/src/utils/filesearch/ignore.js.map +1 -1
  188. package/dist/src/utils/getPty.d.ts +19 -0
  189. package/dist/src/utils/getPty.js +23 -0
  190. package/dist/src/utils/getPty.js.map +1 -0
  191. package/dist/src/utils/shell-utils.d.ts +39 -0
  192. package/dist/src/utils/shell-utils.js +68 -0
  193. package/dist/src/utils/shell-utils.js.map +1 -1
  194. package/dist/src/utils/user_account.js +58 -53
  195. package/dist/src/utils/user_account.js.map +1 -1
  196. package/dist/src/utils/workspaceContext.d.ts +9 -0
  197. package/dist/src/utils/workspaceContext.js +37 -3
  198. package/dist/src/utils/workspaceContext.js.map +1 -1
  199. package/package.json +22 -7
@@ -13,6 +13,11 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
+ /**
17
+ * @plan PLAN-20250120-DEBUGLOGGING.P15
18
+ * @requirement REQ-INT-001.1
19
+ */
20
+ import { DebugLogger } from '../../debug/index.js';
16
21
  import { ContentGeneratorRole } from '../ContentGeneratorRole.js';
17
22
  import { GemmaToolCallParser } from '../../parsers/TextToolCallParser.js';
18
23
  import { ToolFormatter } from '../../tools/ToolFormatter.js';
@@ -27,8 +32,9 @@ import { BaseProvider } from '../BaseProvider.js';
27
32
  import { isQwenEndpoint, generateOAuthEndpointMismatchError, } from '../../config/endpoints.js';
28
33
  import { getSettingsService } from '../../settings/settingsServiceInstance.js';
29
34
  export class OpenAIProvider extends BaseProvider {
35
+ logger;
30
36
  openai;
31
- currentModel = process.env.LLXPRT_DEFAULT_MODEL || 'llama3-70b-8192';
37
+ currentModel = process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
32
38
  baseURL;
33
39
  providerConfig;
34
40
  toolFormatter;
@@ -40,13 +46,22 @@ export class OpenAIProvider extends BaseProvider {
40
46
  constructor(apiKey, baseURL, config, oauthManager) {
41
47
  // Initialize base provider with auth configuration
42
48
  // Check if we should enable OAuth for Qwen
43
- // Enable OAuth if: 1) we have an oauth manager, and 2) either the baseURL is a Qwen endpoint OR no baseURL/apiKey is provided
44
- const shouldEnableQwenOAuth = !!oauthManager &&
45
- (isQwenEndpoint(baseURL || '') ||
46
- (!baseURL && (!apiKey || apiKey === '')) ||
47
- baseURL === 'https://portal.qwen.ai/v1');
48
- if (process.env.DEBUG) {
49
- console.log(`[OpenAI Constructor] baseURL: ${baseURL}, apiKey: ${apiKey?.substring(0, 10) || 'none'}, oauthManager: ${!!oauthManager}, shouldEnableQwenOAuth: ${shouldEnableQwenOAuth}`);
49
+ // Check OAuth enablement from OAuth manager if available
50
+ let shouldEnableQwenOAuth = false;
51
+ if (oauthManager) {
52
+ // Check if OAuth is enabled for qwen in the OAuth manager (from settings)
53
+ const manager = oauthManager;
54
+ if (manager.isOAuthEnabled &&
55
+ typeof manager.isOAuthEnabled === 'function') {
56
+ shouldEnableQwenOAuth = manager.isOAuthEnabled('qwen');
57
+ }
58
+ // Also enable if this looks like a Qwen endpoint
59
+ if (!shouldEnableQwenOAuth) {
60
+ shouldEnableQwenOAuth =
61
+ isQwenEndpoint(baseURL || '') ||
62
+ (!baseURL && (!apiKey || apiKey === '')) ||
63
+ baseURL === 'https://portal.qwen.ai/v1';
64
+ }
50
65
  }
51
66
  const baseConfig = {
52
67
  name: 'openai',
@@ -59,15 +74,15 @@ export class OpenAIProvider extends BaseProvider {
59
74
  oauthManager,
60
75
  };
61
76
  super(baseConfig);
77
+ this.logger = new DebugLogger('llxprt:providers:openai');
78
+ this.logger.debug(() => `Constructor - baseURL: ${baseURL}, apiKey: ${apiKey?.substring(0, 10) || 'none'}, oauthManager: ${!!oauthManager}, shouldEnableQwenOAuth: ${shouldEnableQwenOAuth}`);
62
79
  this.baseURL = baseURL;
63
80
  this.providerConfig = config;
64
81
  this.toolFormatter = new ToolFormatter();
65
82
  this.conversationCache = new ConversationCache();
66
83
  // Initialize from SettingsService
67
84
  this.initializeFromSettings().catch((error) => {
68
- if (process.env.DEBUG) {
69
- console.warn('Failed to initialize OpenAI provider from SettingsService:', error);
70
- }
85
+ this.logger.debug(() => `Failed to initialize from SettingsService: ${error}`);
71
86
  });
72
87
  // Set appropriate default model based on the provider
73
88
  if (shouldEnableQwenOAuth || isQwenEndpoint(baseURL || '')) {
@@ -127,25 +142,18 @@ export class OpenAIProvider extends BaseProvider {
127
142
  }
128
143
  // Check if we're using Qwen OAuth and need to update the baseURL
129
144
  let effectiveBaseURL = this.baseURL;
130
- // Debug logging
131
- if (process.env.DEBUG) {
132
- console.log(`[OpenAI] updateClientWithResolvedAuth - OAuth enabled: ${this.isOAuthEnabled()}, OAuth provider: ${this.baseProviderConfig.oauthProvider}, baseURL: ${this.baseURL}, resolvedKey: ${resolvedKey?.substring(0, 10)}...`);
133
- }
145
+ this.logger.debug(() => `updateClientWithResolvedAuth - OAuth enabled: ${this.isOAuthEnabled()}, OAuth provider: ${this.baseProviderConfig.oauthProvider}, baseURL: ${this.baseURL}, resolvedKey: ${resolvedKey?.substring(0, 10)}...`);
134
146
  if (this.isOAuthEnabled() &&
135
147
  this.baseProviderConfig.oauthProvider === 'qwen') {
136
148
  // Get the OAuth token to check for resource_url
137
149
  const oauthManager = this.baseProviderConfig.oauthManager;
138
150
  if (oauthManager?.getOAuthToken) {
139
151
  const oauthToken = await oauthManager.getOAuthToken('qwen');
140
- if (process.env.DEBUG) {
141
- console.log(`[OpenAI] OAuth token retrieved, resource_url: ${oauthToken?.resource_url}, access_token: ${oauthToken?.access_token?.substring(0, 10)}...`);
142
- }
152
+ this.logger.debug(() => `OAuth token retrieved, resource_url: ${oauthToken?.resource_url}, access_token: ${oauthToken?.access_token?.substring(0, 10)}...`);
143
153
  if (oauthToken?.resource_url) {
144
154
  // Use the resource_url from the OAuth token
145
155
  effectiveBaseURL = `https://${oauthToken.resource_url}/v1`;
146
- if (process.env.DEBUG) {
147
- console.log(`[OpenAI] Using Qwen OAuth endpoint: ${effectiveBaseURL}`);
148
- }
156
+ this.logger.debug(() => `Using Qwen OAuth endpoint: ${effectiveBaseURL}`);
149
157
  }
150
158
  }
151
159
  }
@@ -236,11 +244,7 @@ export class OpenAIProvider extends BaseProvider {
236
244
  const contextInfo = this.estimateContextUsage(options.conversationId, options.parentId, messages);
237
245
  // Warn if less than 4k tokens remaining
238
246
  if (contextInfo.tokensRemaining < 4000) {
239
- if (process.env.DEBUG) {
240
- console.warn(`[OpenAI] Warning: Only ${contextInfo.tokensRemaining} tokens remaining ` +
241
- `(${contextInfo.contextUsedPercent.toFixed(1)}% context used). ` +
242
- `Consider starting a new conversation.`);
243
- }
247
+ this.logger.debug(() => `Warning: Only ${contextInfo.tokensRemaining} tokens remaining (${contextInfo.contextUsedPercent.toFixed(1)}% context used). Consider starting a new conversation.`);
244
248
  }
245
249
  }
246
250
  // Check cache for existing conversation
@@ -295,9 +299,7 @@ export class OpenAIProvider extends BaseProvider {
295
299
  // Handle 422 context_length_exceeded error
296
300
  if (response.status === 422 &&
297
301
  errorBody.includes('context_length_exceeded')) {
298
- if (process.env.DEBUG) {
299
- console.warn('[OpenAI] Context length exceeded, invalidating cache and retrying stateless...');
300
- }
302
+ this.logger.debug(() => 'Context length exceeded, invalidating cache and retrying stateless...');
301
303
  // Invalidate the cache for this conversation
302
304
  if (options?.conversationId && options?.parentId) {
303
305
  this.conversationCache.invalidate(options.conversationId, options.parentId);
@@ -377,6 +379,49 @@ export class OpenAIProvider extends BaseProvider {
377
379
  }
378
380
  const data = (await response.json());
379
381
  const resultMessages = [];
382
+ // DEFENSIVE FIX: Handle potential array response from providers that violate OpenAI spec
383
+ // Some providers (like Cerebras) may return an array of responses instead of a single response
384
+ if (Array.isArray(data)) {
385
+ this.logger.error(() => '[Cerebras Corruption] Detected malformed array response from provider, aggregating...', {
386
+ provider: this.baseURL,
387
+ arrayLength: data.length,
388
+ });
389
+ const aggregatedContent = [];
390
+ let aggregatedToolCalls = [];
391
+ let aggregatedUsage = undefined;
392
+ for (const item of data) {
393
+ if (item.choices?.[0]?.message?.content) {
394
+ aggregatedContent.push(item.choices[0].message.content);
395
+ }
396
+ if (item.choices?.[0]?.message?.tool_calls) {
397
+ aggregatedToolCalls = item.choices[0].message.tool_calls;
398
+ }
399
+ if (item.usage) {
400
+ aggregatedUsage = item.usage;
401
+ }
402
+ }
403
+ const message = {
404
+ role: ContentGeneratorRole.ASSISTANT,
405
+ content: aggregatedContent.join(''),
406
+ };
407
+ if (aggregatedToolCalls.length > 0) {
408
+ message.tool_calls = aggregatedToolCalls;
409
+ }
410
+ if (aggregatedUsage) {
411
+ message.usage = {
412
+ prompt_tokens: aggregatedUsage.prompt_tokens || 0,
413
+ completion_tokens: aggregatedUsage.completion_tokens || 0,
414
+ total_tokens: aggregatedUsage.total_tokens || 0,
415
+ };
416
+ }
417
+ resultMessages.push(message);
418
+ // Convert to async iterator for consistent return type
419
+ return (async function* () {
420
+ for (const msg of resultMessages) {
421
+ yield msg;
422
+ }
423
+ })();
424
+ }
380
425
  if (data.choices && data.choices.length > 0) {
381
426
  const choice = data.choices[0];
382
427
  const message = {
@@ -441,9 +486,7 @@ export class OpenAIProvider extends BaseProvider {
441
486
  return models;
442
487
  }
443
488
  catch (error) {
444
- if (process.env.DEBUG) {
445
- console.error('Error fetching models from OpenAI:', error);
446
- }
489
+ this.logger.debug(() => `Error fetching models from OpenAI: ${error}`);
447
490
  // Return a hardcoded list as fallback
448
491
  // Check if this is a Qwen endpoint
449
492
  if (isQwenEndpoint(this.baseURL || '')) {
@@ -509,15 +552,32 @@ export class OpenAIProvider extends BaseProvider {
509
552
  });
510
553
  return;
511
554
  }
555
+ // Fix messages in place like AnthropicProvider does - this ensures synthetic responses persist
556
+ // This is critical for preventing 400 errors on subsequent calls with Qwen/Cerebras
557
+ const missingToolIds = this.identifyAndFixMissingToolResponses(messages);
558
+ if (missingToolIds.length > 0) {
559
+ this.logger.debug(() => `[Synthetic] Added ${missingToolIds.length} synthetic responses to conversation history: ${JSON.stringify(missingToolIds)}`);
560
+ // Log the actual tool calls and their IDs for debugging
561
+ const assistantMessagesWithTools = messages.filter((m) => m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0);
562
+ const lastAssistantWithTools = assistantMessagesWithTools[assistantMessagesWithTools.length - 1];
563
+ if (lastAssistantWithTools?.tool_calls) {
564
+ this.logger.debug(() => `[Synthetic] Last assistant tool calls: ${JSON.stringify(lastAssistantWithTools.tool_calls?.map((tc) => ({ id: tc.id, name: tc.function.name })) ?? [])}`);
565
+ }
566
+ }
567
+ // Now messages array has been modified in place with synthetic responses
568
+ const patchedMessages = messages;
512
569
  // Validate tool messages have required tool_call_id
513
- const toolMessages = messages.filter((msg) => msg.role === 'tool');
570
+ const toolMessages = patchedMessages.filter((msg) => msg.role === 'tool');
514
571
  const missingIds = toolMessages.filter((msg) => !msg.tool_call_id);
515
572
  if (missingIds.length > 0) {
516
- if (process.env.DEBUG) {
517
- console.error('[OpenAIProvider] FATAL: Tool messages missing tool_call_id:', missingIds);
518
- }
573
+ this.logger.error(() => `FATAL: Tool messages missing tool_call_id: ${JSON.stringify(missingIds)}`);
519
574
  throw new Error(`OpenAI API requires tool_call_id for all tool messages. Found ${missingIds.length} tool message(s) without IDs.`);
520
575
  }
576
+ // Log synthetic responses for debugging
577
+ const syntheticMessages = patchedMessages.filter((msg) => msg._synthetic);
578
+ if (syntheticMessages.length > 0) {
579
+ this.logger.debug(() => `[Synthetic] Added ${syntheticMessages.length} synthetic tool responses`);
580
+ }
521
581
  const parser = this.requiresTextToolCallParsing()
522
582
  ? new GemmaToolCallParser()
523
583
  : null;
@@ -529,94 +589,389 @@ export class OpenAIProvider extends BaseProvider {
529
589
  const streamOptions = this.providerConfig?.getEphemeralSettings?.()?.['stream-options'];
530
590
  // Default stream_options to { include_usage: true } unless explicitly set
531
591
  const finalStreamOptions = streamOptions !== undefined ? streamOptions : { include_usage: true };
592
+ // Get streaming setting from ephemeral settings (default: enabled)
593
+ const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
594
+ let streamingEnabled = streamingSetting !== 'disabled';
532
595
  // Get resolved authentication and update client if needed
533
596
  await this.updateClientWithResolvedAuth();
534
- if (process.env.DEBUG) {
535
- console.log(`[OpenAI] About to make API call with model: ${this.currentModel}, baseURL: ${this.openai.baseURL}, apiKey: ${this.openai.apiKey?.substring(0, 10)}...`);
536
- }
537
- // Build request params with exact order from original
538
- const stream = await this.openai.chat.completions.create({
539
- model: this.currentModel,
540
- messages: messages,
541
- stream: true,
542
- ...(finalStreamOptions !== null
543
- ? { stream_options: finalStreamOptions }
544
- : {}),
545
- tools: formattedTools,
546
- tool_choice: this.getToolChoiceForFormat(tools),
547
- ...this.modelParams,
597
+ // Strip internal tracking fields that some APIs don't accept
598
+ // We keep the synthetic responses but remove the metadata fields
599
+ const cleanedMessages = patchedMessages.map((msg) => {
600
+ // Create a shallow copy and remove internal fields
601
+ const { _synthetic, _cancelled, ...cleanMsg } = msg;
602
+ // Log synthetic tool responses for debugging
603
+ if (msg._synthetic) {
604
+ this.logger.debug(() => `[Synthetic Tool Response] ${JSON.stringify(cleanMsg)}`);
605
+ }
606
+ return cleanMsg;
548
607
  });
608
+ this.logger.debug(() => `About to make API call with model: ${this.currentModel}, baseURL: ${this.openai.baseURL}, apiKey: ${this.openai.apiKey?.substring(0, 10)}..., streaming: ${streamingEnabled}`);
609
+ // Debug: Log message roles being sent
610
+ this.logger.debug(() => `Messages being sent to OpenAI (${cleanedMessages.length} total): ${cleanedMessages
611
+ .map((m) => `${m.role}${m.role === 'system' ? ` (length: ${m.content?.length})` : ''}`)
612
+ .join(', ')}`);
613
+ let response;
614
+ try {
615
+ // Build request params with exact order from original
616
+ response = await this.openai.chat.completions.create({
617
+ model: this.currentModel,
618
+ messages: cleanedMessages,
619
+ stream: streamingEnabled,
620
+ ...(streamingEnabled && finalStreamOptions !== null
621
+ ? { stream_options: finalStreamOptions }
622
+ : {}),
623
+ tools: formattedTools,
624
+ tool_choice: this.getToolChoiceForFormat(tools),
625
+ ...this.modelParams,
626
+ });
627
+ }
628
+ catch (error) {
629
+ // Debug the error
630
+ this.logger.error(() => `[Cancellation 400] Error caught in API call: ${error}`);
631
+ this.logger.error(() => `[Cancellation 400] Error type: ${error?.constructor?.name}`);
632
+ this.logger.error(() => `[Cancellation 400] Error status: ${error?.status || error?.response?.status}`);
633
+ this.logger.error(() => `[Cancellation 400] Error response data: ${JSON.stringify(error?.response?.data, null, 2)}`);
634
+ // Log the last few messages to understand what's being sent
635
+ if (error?.status === 400 ||
636
+ error?.response?.status === 400) {
637
+ this.logger.error(() => `[Cancellation 400] Last 5 messages being sent:`);
638
+ const lastMessages = cleanedMessages.slice(-5);
639
+ lastMessages.forEach((msg, idx) => {
640
+ this.logger.error(() => ` [${cleanedMessages.length - 5 + idx}] ${msg.role}${msg.tool_call_id ? ` (tool response for ${msg.tool_call_id})` : ''}${msg.tool_calls ? ` (${msg.tool_calls.length} tool calls)` : ''}`);
641
+ if (msg.tool_calls) {
642
+ msg.tool_calls.forEach((tc) => {
643
+ this.logger.error(() => ` - Tool call: ${tc.id} -> ${tc.function.name}`);
644
+ });
645
+ }
646
+ });
647
+ }
648
+ // Check for JSONResponse mutation errors
649
+ const errorMessage = error instanceof Error ? error.message : String(error);
650
+ if (errorMessage?.includes('JSONResponse') &&
651
+ errorMessage?.includes('does not support item assignment')) {
652
+ this.logger.debug(() => '[Cancellation 400] Detected JSONResponse mutation error, retrying without streaming');
653
+ this.logger.error(() => '[Cerebras Corruption] JSONResponse mutation error detected. This typically occurs with certain providers like Cerebras. Falling back to non-streaming mode.', {
654
+ errorMessage,
655
+ provider: this.baseURL,
656
+ streamingEnabled,
657
+ });
658
+ // Retry with streaming disabled
659
+ response = await this.openai.chat.completions.create({
660
+ model: this.currentModel,
661
+ messages: cleanedMessages,
662
+ stream: false, // Force non-streaming
663
+ tools: formattedTools,
664
+ tool_choice: this.getToolChoiceForFormat(tools),
665
+ ...this.modelParams,
666
+ });
667
+ // Override streamingEnabled for the rest of this function
668
+ streamingEnabled = false;
669
+ }
670
+ else {
671
+ this.logger.debug(() => '[Cancellation 400] Re-throwing error (not a JSONResponse mutation)');
672
+ // Re-throw other errors
673
+ throw error;
674
+ }
675
+ }
549
676
  let fullContent = '';
550
677
  const accumulatedToolCalls = [];
551
678
  let hasStreamedContent = false;
552
679
  let usageData;
553
680
  // For Qwen streaming, buffer whitespace-only chunks to preserve spacing across chunk boundaries
554
681
  let pendingWhitespace = null;
555
- for await (const chunk of stream) {
556
- const delta = chunk.choices[0]?.delta;
557
- if (delta?.content) {
558
- // Enhanced debug logging to understand streaming behavior
559
- if (process.env.DEBUG && this.isUsingQwen()) {
560
- console.log(`[OpenAIProvider/${this.currentModel}] Chunk:`, {
561
- content: delta.content,
562
- contentLength: delta.content.length,
563
- isWhitespaceOnly: delta.content.trim() === '',
564
- chunkIndex: chunk.choices[0]?.index,
682
+ // Handle streaming vs non-streaming response
683
+ if (streamingEnabled) {
684
+ // We need to buffer all chunks to detect and handle malformed streams
685
+ // Some providers (like Cerebras) send message format instead of delta
686
+ const allChunks = [];
687
+ this.logger.debug(() => '[Stream Detection] Starting to buffer chunks for corruption detection', {
688
+ provider: this.baseURL,
689
+ streamingEnabled,
690
+ });
691
+ try {
692
+ for await (const chunk of response) {
693
+ // CRITICAL: Create a deep copy to avoid JSONResponse mutation issues
694
+ // Cerebras and other providers may return immutable JSONResponse objects
695
+ // Cast to unknown first to bypass type checking, then to our extended type
696
+ const extendedChunk = chunk;
697
+ const safeChunk = {
698
+ choices: extendedChunk.choices?.map((choice) => ({
699
+ delta: choice.delta
700
+ ? {
701
+ content: choice.delta.content ?? undefined,
702
+ role: choice.delta.role,
703
+ tool_calls: choice.delta.tool_calls?.map((tc, idx) => ({
704
+ id: tc.id,
705
+ type: tc.type,
706
+ function: tc.function
707
+ ? {
708
+ name: tc.function.name,
709
+ arguments: tc.function.arguments,
710
+ }
711
+ : undefined,
712
+ index: tc.index !== undefined ? tc.index : idx,
713
+ })),
714
+ }
715
+ : undefined,
716
+ message: choice.message
717
+ ? {
718
+ content: choice.message.content ?? undefined,
719
+ role: choice.message.role,
720
+ tool_calls: choice.message.tool_calls?.map((tc) => ({
721
+ id: tc.id,
722
+ type: tc.type,
723
+ function: tc.function
724
+ ? {
725
+ name: tc.function.name,
726
+ arguments: tc.function.arguments,
727
+ }
728
+ : undefined,
729
+ })),
730
+ }
731
+ : undefined,
732
+ index: choice.index,
733
+ finish_reason: choice.finish_reason,
734
+ })),
735
+ usage: extendedChunk.usage
736
+ ? {
737
+ prompt_tokens: extendedChunk.usage.prompt_tokens,
738
+ completion_tokens: extendedChunk.usage.completion_tokens,
739
+ total_tokens: extendedChunk.usage.total_tokens,
740
+ }
741
+ : undefined,
742
+ };
743
+ allChunks.push(safeChunk);
744
+ }
745
+ }
746
+ catch (error) {
747
+ // Handle JSONResponse mutation errors that occur during iteration
748
+ const errorMessage = error instanceof Error ? error.message : String(error);
749
+ if (errorMessage?.includes('JSONResponse') &&
750
+ errorMessage?.includes('does not support item assignment')) {
751
+ this.logger.error(() => '[Cerebras Corruption] JSONResponse mutation error during stream iteration. This is a known issue with Cerebras. The OpenAI client library is trying to mutate immutable response objects. Falling back to non-streaming mode.', {
752
+ error: errorMessage,
753
+ provider: this.baseURL,
754
+ chunksCollected: allChunks.length,
755
+ });
756
+ // Retry the entire request with streaming disabled
757
+ // This is the nuclear option but ensures we get a response
758
+ const nonStreamingResponse = await this.openai.chat.completions.create({
759
+ model: this.currentModel,
760
+ messages: cleanedMessages,
761
+ stream: false, // Force non-streaming
762
+ tools: formattedTools,
763
+ tool_choice: this.getToolChoiceForFormat(tools),
764
+ ...this.modelParams,
565
765
  });
766
+ // Handle as non-streaming response
767
+ const completionResponse = nonStreamingResponse;
768
+ const choice = completionResponse.choices[0];
769
+ if (choice?.message.content) {
770
+ fullContent = choice.message.content;
771
+ }
772
+ if (choice?.message.tool_calls) {
773
+ for (const toolCall of choice.message.tool_calls) {
774
+ if (toolCall.type === 'function' && toolCall.function) {
775
+ accumulatedToolCalls.push({
776
+ id: toolCall.id,
777
+ type: 'function',
778
+ function: toolCall.function,
779
+ });
780
+ }
781
+ }
782
+ }
783
+ if (completionResponse.usage) {
784
+ usageData = {
785
+ prompt_tokens: completionResponse.usage.prompt_tokens,
786
+ completion_tokens: completionResponse.usage.completion_tokens,
787
+ total_tokens: completionResponse.usage.total_tokens,
788
+ };
789
+ }
790
+ // Yield the complete response
791
+ yield {
792
+ role: ContentGeneratorRole.ASSISTANT,
793
+ content: fullContent || '',
794
+ tool_calls: accumulatedToolCalls.length > 0
795
+ ? accumulatedToolCalls
796
+ : undefined,
797
+ usage: usageData,
798
+ };
799
+ return;
566
800
  }
567
- // For text-based models, don't yield content chunks yet
568
- if (!parser) {
801
+ // Re-throw other errors
802
+ throw error;
803
+ }
804
+ // Check first chunk to see if we have malformed stream
805
+ let detectedMalformedStream = false;
806
+ if (allChunks.length > 0) {
807
+ const firstChunk = allChunks[0];
808
+ if (firstChunk.choices?.[0]?.message &&
809
+ !firstChunk.choices?.[0]?.delta) {
810
+ detectedMalformedStream = true;
811
+ this.logger.debug(() => 'Detected malformed stream (message instead of delta), using aggregation mode');
812
+ }
813
+ }
814
+ // If we detected issues, aggregate everything
815
+ if (detectedMalformedStream) {
816
+ const contentParts = [];
817
+ let aggregatedToolCalls = [];
818
+ let finalUsageData = undefined;
819
+ // Process all buffered chunks
820
+ for (const chunk of allChunks) {
821
+ const message = chunk.choices?.[0]?.message || chunk.choices?.[0]?.delta;
822
+ if (message?.content) {
823
+ contentParts.push(message.content);
824
+ }
825
+ if (message?.tool_calls) {
826
+ // Ensure tool_calls match the expected format
827
+ aggregatedToolCalls = message.tool_calls.map((tc) => ({
828
+ id: tc.id || `call_${Date.now()}`,
829
+ type: (tc.type || 'function'),
830
+ function: {
831
+ name: tc.function?.name || '',
832
+ arguments: tc.function?.arguments || '',
833
+ },
834
+ }));
835
+ }
836
+ if (chunk.usage) {
837
+ finalUsageData = {
838
+ prompt_tokens: chunk.usage.prompt_tokens || 0,
839
+ completion_tokens: chunk.usage.completion_tokens || 0,
840
+ total_tokens: chunk.usage.total_tokens || 0,
841
+ };
842
+ }
843
+ }
844
+ // Yield single reconstructed message
845
+ yield {
846
+ role: ContentGeneratorRole.ASSISTANT,
847
+ content: contentParts.join(''),
848
+ tool_calls: aggregatedToolCalls.length > 0 ? aggregatedToolCalls : undefined,
849
+ usage: finalUsageData,
850
+ };
851
+ return;
852
+ }
853
+ // Process chunks normally - stream them as they come
854
+ for (const chunk of allChunks) {
855
+ // Since we created safe copies during buffering, chunks are now mutable
856
+ // Check if this chunk has message format instead of delta (malformed stream)
857
+ let processedChunk = chunk;
858
+ if (chunk.choices?.[0]?.message && !chunk.choices?.[0]?.delta) {
859
+ this.logger.error(() => '[Cerebras Corruption] Converting malformed chunk from message to delta format', {
860
+ provider: this.baseURL,
861
+ hasMessage: true,
862
+ hasDelta: false,
863
+ messageContent: chunk.choices[0].message?.content?.substring(0, 100),
864
+ });
865
+ // Convert message format to delta format for consistent processing
866
+ const message = chunk.choices[0].message;
867
+ processedChunk = {
868
+ choices: [
869
+ {
870
+ delta: {
871
+ content: message?.content ?? undefined,
872
+ role: message?.role,
873
+ tool_calls: message?.tool_calls,
874
+ },
875
+ },
876
+ ],
877
+ usage: chunk.usage,
878
+ };
879
+ }
880
+ const delta = processedChunk.choices?.[0]?.delta;
881
+ if (delta?.content) {
882
+ // Enhanced debug logging to understand streaming behavior
569
883
  if (this.isUsingQwen()) {
570
- const isWhitespaceOnly = delta.content.trim() === '';
571
- if (isWhitespaceOnly) {
572
- // Buffer whitespace-only chunk
573
- pendingWhitespace = (pendingWhitespace || '') + delta.content;
574
- if (process.env.DEBUG) {
575
- console.log(`[OpenAIProvider/${this.currentModel}] Buffered whitespace-only chunk (len=${delta.content.length}). pendingWhitespace now len=${pendingWhitespace.length}`);
884
+ this.logger.debug(() => `Chunk: ${JSON.stringify({
885
+ content: delta.content,
886
+ contentLength: delta.content?.length ?? 0,
887
+ isWhitespaceOnly: delta.content?.trim() === '',
888
+ chunkIndex: 0,
889
+ })}`);
890
+ }
891
+ // For text-based models, don't yield content chunks yet
892
+ if (!parser && delta.content) {
893
+ if (this.isUsingQwen()) {
894
+ const isWhitespaceOnly = delta.content.trim() === '';
895
+ if (isWhitespaceOnly) {
896
+ // Buffer whitespace-only chunk
897
+ pendingWhitespace = (pendingWhitespace || '') + delta.content;
898
+ this.logger.debug(() => `Buffered whitespace-only chunk (len=${delta.content?.length ?? 0}). pendingWhitespace now len=${pendingWhitespace?.length ?? 0}`);
899
+ continue;
576
900
  }
577
- continue;
578
- }
579
- else if (pendingWhitespace) {
580
- // Flush buffered whitespace before non-empty chunk to preserve spacing
581
- if (process.env.DEBUG) {
582
- console.log(`[OpenAIProvider/${this.currentModel}] Flushing pending whitespace (len=${pendingWhitespace.length}) before non-empty chunk`);
901
+ else if (pendingWhitespace) {
902
+ // Flush buffered whitespace before non-empty chunk to preserve spacing
903
+ this.logger.debug(() => `Flushing pending whitespace (len=${pendingWhitespace?.length ?? 0}) before non-empty chunk`);
904
+ yield {
905
+ role: ContentGeneratorRole.ASSISTANT,
906
+ content: pendingWhitespace,
907
+ };
908
+ hasStreamedContent = true;
909
+ fullContent += pendingWhitespace;
910
+ pendingWhitespace = null;
583
911
  }
584
- yield {
585
- role: ContentGeneratorRole.ASSISTANT,
586
- content: pendingWhitespace,
587
- };
588
- hasStreamedContent = true;
589
- fullContent += pendingWhitespace;
590
- pendingWhitespace = null;
591
912
  }
913
+ yield {
914
+ role: ContentGeneratorRole.ASSISTANT,
915
+ content: delta.content,
916
+ };
917
+ hasStreamedContent = true;
592
918
  }
593
- yield {
594
- role: ContentGeneratorRole.ASSISTANT,
595
- content: delta.content,
919
+ fullContent += delta.content;
920
+ }
921
+ if (delta?.tool_calls) {
922
+ for (const toolCall of delta.tool_calls) {
923
+ this.toolFormatter.accumulateStreamingToolCall(toolCall, accumulatedToolCalls, currentToolFormat);
924
+ }
925
+ }
926
+ // Check for usage data in the chunk
927
+ if (processedChunk.usage) {
928
+ usageData = {
929
+ prompt_tokens: processedChunk.usage.prompt_tokens || 0,
930
+ completion_tokens: processedChunk.usage.completion_tokens || 0,
931
+ total_tokens: processedChunk.usage.total_tokens || 0,
596
932
  };
597
- hasStreamedContent = true;
598
933
  }
599
- fullContent += delta.content;
600
934
  }
601
- if (delta?.tool_calls) {
602
- for (const toolCall of delta.tool_calls) {
603
- this.toolFormatter.accumulateStreamingToolCall(toolCall, accumulatedToolCalls, currentToolFormat);
935
+ }
936
+ else {
937
+ // Non-streaming response - handle as a single completion
938
+ const completionResponse = response;
939
+ const choice = completionResponse.choices[0];
940
+ if (choice?.message.content) {
941
+ fullContent = choice.message.content;
942
+ }
943
+ if (choice?.message.tool_calls) {
944
+ // Convert tool calls to the standard format
945
+ for (const toolCall of choice.message.tool_calls) {
946
+ if (toolCall.type === 'function' && toolCall.function) {
947
+ // Don't fix double stringification here - it's handled later in the final processing
948
+ accumulatedToolCalls.push({
949
+ id: toolCall.id,
950
+ type: 'function',
951
+ function: toolCall.function,
952
+ });
953
+ }
604
954
  }
605
955
  }
606
- // Check for usage data in the chunk
607
- if (chunk.usage) {
956
+ if (completionResponse.usage) {
608
957
  usageData = {
609
- prompt_tokens: chunk.usage.prompt_tokens,
610
- completion_tokens: chunk.usage.completion_tokens,
611
- total_tokens: chunk.usage.total_tokens,
958
+ prompt_tokens: completionResponse.usage.prompt_tokens,
959
+ completion_tokens: completionResponse.usage.completion_tokens,
960
+ total_tokens: completionResponse.usage.total_tokens,
961
+ };
962
+ }
963
+ // For non-streaming, we yield the full content at once if there's no parser
964
+ if (!parser && fullContent) {
965
+ yield {
966
+ role: ContentGeneratorRole.ASSISTANT,
967
+ content: fullContent,
612
968
  };
969
+ hasStreamedContent = true;
613
970
  }
614
971
  }
615
972
  // Flush any remaining pending whitespace for Qwen
616
973
  if (pendingWhitespace && this.isUsingQwen() && !parser) {
617
- if (process.env.DEBUG) {
618
- console.log(`[OpenAIProvider/${this.currentModel}] Flushing trailing pending whitespace (len=${pendingWhitespace.length}) at stream end`);
619
- }
974
+ this.logger.debug(() => `Flushing trailing pending whitespace (len=${pendingWhitespace?.length ?? 0}) at stream end`);
620
975
  yield {
621
976
  role: ContentGeneratorRole.ASSISTANT,
622
977
  content: pendingWhitespace,
@@ -657,23 +1012,144 @@ export class OpenAIProvider extends BaseProvider {
657
1012
  else {
658
1013
  // Standard OpenAI tool call handling
659
1014
  if (accumulatedToolCalls.length > 0) {
660
- if (process.env.DEBUG && this.isUsingQwen()) {
661
- console.log(`[OpenAIProvider/${this.currentModel}] Final message with tool calls:`, {
1015
+ // Fix double stringification for Qwen tool calls
1016
+ // Qwen models pre-stringify arguments values, but later in the process
1017
+ // they are being JSON.stringify'd again
1018
+ let fixedToolCalls = accumulatedToolCalls;
1019
+ if (this.isUsingQwen()) {
1020
+ this.logger.debug(() => `[Qwen Fix] Processing ${accumulatedToolCalls.length} tool calls for double-stringification fix`);
1021
+ fixedToolCalls = accumulatedToolCalls.map((toolCall, index) => {
1022
+ this.logger.debug(() => `[Qwen Fix] Tool call ${index}: ${JSON.stringify({
1023
+ name: toolCall.function.name,
1024
+ argumentsType: typeof toolCall.function.arguments,
1025
+ argumentsLength: toolCall.function.arguments?.length,
1026
+ argumentsSample: toolCall.function.arguments?.substring(0, 100),
1027
+ })}`);
1028
+ // For Qwen, check for nested double-stringification
1029
+ // Qwen models stringify array/object values WITHIN the JSON arguments
1030
+ if (toolCall.function.arguments &&
1031
+ typeof toolCall.function.arguments === 'string') {
1032
+ try {
1033
+ // First, parse the arguments to get the JSON object
1034
+ const parsedArgs = JSON.parse(toolCall.function.arguments);
1035
+ let hasNestedStringification = false;
1036
+ // Check each property to see if it's a stringified array/object
1037
+ const fixedArgs = {};
1038
+ for (const [key, value] of Object.entries(parsedArgs)) {
1039
+ if (typeof value === 'string') {
1040
+ const trimmed = value.trim();
1041
+ // Check if it looks like a stringified array or object
1042
+ // Also check for Python-style dictionaries with single quotes
1043
+ if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
1044
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
1045
+ try {
1046
+ // Try to parse it as JSON
1047
+ const nestedParsed = JSON.parse(value);
1048
+ fixedArgs[key] = nestedParsed;
1049
+ hasNestedStringification = true;
1050
+ this.logger.debug(() => `[Qwen Fix] Fixed nested stringification in property '${key}' for ${toolCall.function.name}`);
1051
+ }
1052
+ catch {
1053
+ // Try to convert Python-style to JSON (single quotes to double quotes)
1054
+ try {
1055
+ const jsonified = value
1056
+ .replace(/'/g, '"')
1057
+ .replace(/: True/g, ': true')
1058
+ .replace(/: False/g, ': false')
1059
+ .replace(/: None/g, ': null');
1060
+ const nestedParsed = JSON.parse(jsonified);
1061
+ fixedArgs[key] = nestedParsed;
1062
+ hasNestedStringification = true;
1063
+ this.logger.debug(() => `[Qwen Fix] Fixed Python-style nested stringification in property '${key}' for ${toolCall.function.name}`);
1064
+ }
1065
+ catch {
1066
+ // Not valid JSON even after conversion, keep as string
1067
+ fixedArgs[key] = value;
1068
+ }
1069
+ }
1070
+ }
1071
+ else {
1072
+ fixedArgs[key] = value;
1073
+ }
1074
+ }
1075
+ else {
1076
+ fixedArgs[key] = value;
1077
+ }
1078
+ }
1079
+ if (hasNestedStringification) {
1080
+ this.logger.debug(() => `[Qwen Fix] Fixed nested double-stringification for ${toolCall.function.name}`);
1081
+ return {
1082
+ ...toolCall,
1083
+ function: {
1084
+ ...toolCall.function,
1085
+ arguments: JSON.stringify(fixedArgs),
1086
+ },
1087
+ };
1088
+ }
1089
+ }
1090
+ catch (_e) {
1091
+ // If parsing fails, check for old-style double-stringification
1092
+ if (toolCall.function.arguments.startsWith('"') &&
1093
+ toolCall.function.arguments.endsWith('"')) {
1094
+ try {
1095
+ // Old fix: entire arguments were double-stringified
1096
+ const parsedArgs = JSON.parse(toolCall.function.arguments);
1097
+ this.logger.debug(() => `[Qwen Fix] Fixed whole-argument double-stringification for ${toolCall.function.name}`);
1098
+ return {
1099
+ ...toolCall,
1100
+ function: {
1101
+ ...toolCall.function,
1102
+ arguments: JSON.stringify(parsedArgs),
1103
+ },
1104
+ };
1105
+ }
1106
+ catch {
1107
+ // Leave as-is if we can't parse
1108
+ }
1109
+ }
1110
+ }
1111
+ }
1112
+ // No fix needed
1113
+ this.logger.debug(() => `[Qwen Fix] No double-stringification detected for ${toolCall.function.name}, keeping original`);
1114
+ return toolCall;
1115
+ });
1116
+ }
1117
+ if (this.isUsingQwen()) {
1118
+ this.logger.debug(() => `Final message with tool calls: ${JSON.stringify({
662
1119
  contentLength: fullContent.length,
663
1120
  content: fullContent.substring(0, 200) +
664
1121
  (fullContent.length > 200 ? '...' : ''),
665
1122
  toolCallCount: accumulatedToolCalls.length,
666
1123
  hasStreamedContent,
667
- });
1124
+ })}`);
668
1125
  }
669
1126
  // For Qwen models, don't duplicate content if we've already streamed it
670
- const shouldOmitContent = hasStreamedContent && this.isUsingQwen();
1127
+ // BUT Cerebras needs at least a space to continue after tool responses
1128
+ const isCerebras = this.baseURL?.toLowerCase().includes('cerebras.ai');
1129
+ if (isCerebras) {
1130
+ this.logger.debug(() => '[Cerebras] Special handling for Cerebras provider after tool responses', {
1131
+ hasStreamedContent,
1132
+ willSendSpace: hasStreamedContent,
1133
+ });
1134
+ }
1135
+ const shouldOmitContent = hasStreamedContent && this.isUsingQwen() && !isCerebras;
671
1136
  if (shouldOmitContent) {
672
1137
  // Only yield tool calls with empty content to avoid duplication
673
1138
  yield {
674
1139
  role: ContentGeneratorRole.ASSISTANT,
675
1140
  content: '',
676
- tool_calls: accumulatedToolCalls,
1141
+ tool_calls: fixedToolCalls,
1142
+ usage: usageData,
1143
+ };
1144
+ }
1145
+ else if (isCerebras && hasStreamedContent) {
1146
+ // Cerebras: Send just a space to prevent duplication but allow continuation
1147
+ // This prevents the repeated "Let me search..." text
1148
+ this.logger.debug(() => '[Cerebras] Sending minimal space content to prevent duplication');
1149
+ yield {
1150
+ role: ContentGeneratorRole.ASSISTANT,
1151
+ content: ' ', // Single space instead of full content
1152
+ tool_calls: fixedToolCalls,
677
1153
  usage: usageData,
678
1154
  };
679
1155
  }
@@ -682,7 +1158,7 @@ export class OpenAIProvider extends BaseProvider {
682
1158
  yield {
683
1159
  role: ContentGeneratorRole.ASSISTANT,
684
1160
  content: fullContent || '',
685
- tool_calls: accumulatedToolCalls,
1161
+ tool_calls: fixedToolCalls,
686
1162
  usage: usageData,
687
1163
  };
688
1164
  }
@@ -700,9 +1176,7 @@ export class OpenAIProvider extends BaseProvider {
700
1176
  setModel(modelId) {
701
1177
  // Update SettingsService as the source of truth
702
1178
  this.setModelInSettings(modelId).catch((error) => {
703
- if (process.env.DEBUG) {
704
- console.warn('Failed to persist model to SettingsService:', error);
705
- }
1179
+ this.logger.debug(() => `Failed to persist model to SettingsService: ${error}`);
706
1180
  });
707
1181
  // Keep local cache for performance
708
1182
  this.currentModel = modelId;
@@ -717,9 +1191,7 @@ export class OpenAIProvider extends BaseProvider {
717
1191
  }
718
1192
  }
719
1193
  catch (error) {
720
- if (process.env.DEBUG) {
721
- console.warn('Failed to get model from SettingsService:', error);
722
- }
1194
+ this.logger.debug(() => `Failed to get model from SettingsService: ${error}`);
723
1195
  }
724
1196
  // Fall back to cached value or default
725
1197
  return this.currentModel || this.getDefaultModel();
@@ -730,16 +1202,14 @@ export class OpenAIProvider extends BaseProvider {
730
1202
  if (this.isUsingQwen()) {
731
1203
  return 'qwen3-coder-plus';
732
1204
  }
733
- return process.env.LLXPRT_DEFAULT_MODEL || 'llama3-70b-8192';
1205
+ return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
734
1206
  }
735
1207
  setApiKey(apiKey) {
736
1208
  // Call base provider implementation
737
1209
  super.setApiKey?.(apiKey);
738
1210
  // Persist to SettingsService if available
739
1211
  this.setApiKeyInSettings(apiKey).catch((error) => {
740
- if (process.env.DEBUG) {
741
- console.warn('Failed to persist API key to SettingsService:', error);
742
- }
1212
+ this.logger.debug(() => `Failed to persist API key to SettingsService: ${error}`);
743
1213
  });
744
1214
  // Create a new OpenAI client with the updated API key
745
1215
  const clientOptions = {
@@ -758,9 +1228,7 @@ export class OpenAIProvider extends BaseProvider {
758
1228
  this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
759
1229
  // Persist to SettingsService if available
760
1230
  this.setBaseUrlInSettings(this.baseURL).catch((error) => {
761
- if (process.env.DEBUG) {
762
- console.warn('Failed to persist base URL to SettingsService:', error);
763
- }
1231
+ this.logger.debug(() => `Failed to persist base URL to SettingsService: ${error}`);
764
1232
  });
765
1233
  // Update OAuth configuration based on endpoint validation
766
1234
  // Enable OAuth for Qwen endpoints if we have an OAuth manager
@@ -808,6 +1276,89 @@ export class OpenAIProvider extends BaseProvider {
808
1276
  getConversationCache() {
809
1277
  return this.conversationCache;
810
1278
  }
1279
+ /**
1280
+ * Identifies and fixes missing tool responses by adding synthetic responses in place.
1281
+ * Similar to AnthropicProvider's validateAndFixMessages approach.
1282
+ * This ensures synthetic responses persist in the conversation history.
1283
+ * @param messages The message array to fix in place
1284
+ * @returns Array of tool call IDs that were fixed
1285
+ */
1286
+ identifyAndFixMissingToolResponses(messages) {
1287
+ const fixedIds = [];
1288
+ const pendingToolCalls = [];
1289
+ // Process messages in order, tracking tool calls and responses
1290
+ for (let i = 0; i < messages.length; i++) {
1291
+ const msg = messages[i];
1292
+ if (msg.role === 'assistant' && msg.tool_calls) {
1293
+ // If we have pending tool calls from a previous assistant message,
1294
+ // add synthetic responses for them before processing this new assistant message
1295
+ if (pendingToolCalls.length > 0) {
1296
+ const syntheticResponses = pendingToolCalls.map((tc) => ({
1297
+ role: 'tool',
1298
+ tool_call_id: tc.id,
1299
+ content: 'Tool execution cancelled by user',
1300
+ _synthetic: true,
1301
+ _cancelled: true,
1302
+ }));
1303
+ // Insert synthetic responses before the current assistant message
1304
+ messages.splice(i, 0, ...syntheticResponses);
1305
+ // Track what we fixed
1306
+ fixedIds.push(...pendingToolCalls.map((tc) => tc.id));
1307
+ // Adjust index to account for inserted messages
1308
+ i += syntheticResponses.length;
1309
+ // Clear pending tool calls
1310
+ pendingToolCalls.length = 0;
1311
+ }
1312
+ // Now track the new tool calls from this assistant message
1313
+ msg.tool_calls.forEach((toolCall) => {
1314
+ if (toolCall.id) {
1315
+ pendingToolCalls.push({
1316
+ id: toolCall.id,
1317
+ name: toolCall.function.name,
1318
+ });
1319
+ }
1320
+ });
1321
+ }
1322
+ else if (msg.role === 'tool' && pendingToolCalls.length > 0) {
1323
+ // Match tool responses with pending tool calls
1324
+ pendingToolCalls.splice(pendingToolCalls.findIndex((tc) => tc.id === msg.tool_call_id), 1);
1325
+ }
1326
+ else if ((msg.role === 'assistant' || msg.role === 'user') &&
1327
+ pendingToolCalls.length > 0) {
1328
+ // We hit a non-tool message with pending tool calls - need to add synthetic responses
1329
+ const syntheticResponses = pendingToolCalls.map((tc) => ({
1330
+ role: 'tool',
1331
+ tool_call_id: tc.id,
1332
+ content: 'Tool execution cancelled by user',
1333
+ _synthetic: true,
1334
+ _cancelled: true,
1335
+ }));
1336
+ // Insert synthetic responses before the current message
1337
+ messages.splice(i, 0, ...syntheticResponses);
1338
+ // Track what we fixed
1339
+ fixedIds.push(...pendingToolCalls.map((tc) => tc.id));
1340
+ // Adjust index to account for inserted messages
1341
+ i += syntheticResponses.length;
1342
+ // Clear pending tool calls
1343
+ pendingToolCalls.length = 0;
1344
+ }
1345
+ }
1346
+ // Handle any remaining pending tool calls at the end
1347
+ if (pendingToolCalls.length > 0) {
1348
+ const syntheticResponses = pendingToolCalls.map((tc) => ({
1349
+ role: 'tool',
1350
+ tool_call_id: tc.id,
1351
+ content: 'Tool execution cancelled by user',
1352
+ _synthetic: true,
1353
+ _cancelled: true,
1354
+ }));
1355
+ // Add to the end of messages
1356
+ messages.push(...syntheticResponses);
1357
+ // Track what we fixed
1358
+ fixedIds.push(...pendingToolCalls.map((tc) => tc.id));
1359
+ }
1360
+ return fixedIds;
1361
+ }
811
1362
  /**
812
1363
  * OpenAI always requires payment (API key)
813
1364
  */
@@ -843,9 +1394,7 @@ export class OpenAIProvider extends BaseProvider {
843
1394
  }
844
1395
  // Persist to SettingsService if available
845
1396
  this.setModelParamsInSettings(this.modelParams).catch((error) => {
846
- if (process.env.DEBUG) {
847
- console.warn('Failed to persist model params to SettingsService:', error);
848
- }
1397
+ this.logger.debug(() => `Failed to persist model params to SettingsService: ${error}`);
849
1398
  });
850
1399
  }
851
1400
  /**
@@ -875,14 +1424,10 @@ export class OpenAIProvider extends BaseProvider {
875
1424
  if (savedParams) {
876
1425
  this.modelParams = savedParams;
877
1426
  }
878
- if (process.env.DEBUG) {
879
- console.log(`[OpenAI] Initialized from SettingsService - model: ${this.currentModel}, baseURL: ${this.baseURL}, params:`, this.modelParams);
880
- }
1427
+ this.logger.debug(() => `Initialized from SettingsService - model: ${this.currentModel}, baseURL: ${this.baseURL}, params: ${JSON.stringify(this.modelParams)}`);
881
1428
  }
882
1429
  catch (error) {
883
- if (process.env.DEBUG) {
884
- console.error('Failed to initialize OpenAI provider from SettingsService:', error);
885
- }
1430
+ this.logger.debug(() => `Failed to initialize OpenAI provider from SettingsService: ${error}`);
886
1431
  }
887
1432
  }
888
1433
  /**
@@ -922,9 +1467,7 @@ export class OpenAIProvider extends BaseProvider {
922
1467
  return 'openai';
923
1468
  }
924
1469
  catch (error) {
925
- if (process.env.DEBUG) {
926
- console.warn('Failed to detect tool format from SettingsService:', error);
927
- }
1470
+ this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
928
1471
  // Fallback detection without SettingsService
929
1472
  const modelName = this.currentModel.toLowerCase();
930
1473
  if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {