@vybestack/llxprt-code-core 0.1.22 → 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 (157) hide show
  1. package/dist/src/config/config.d.ts +14 -1
  2. package/dist/src/config/config.js +23 -3
  3. package/dist/src/config/config.js.map +1 -1
  4. package/dist/src/core/client.js +2 -2
  5. package/dist/src/core/client.js.map +1 -1
  6. package/dist/src/core/coreToolScheduler.d.ts +2 -1
  7. package/dist/src/core/coreToolScheduler.js +19 -0
  8. package/dist/src/core/coreToolScheduler.js.map +1 -1
  9. package/dist/src/core/geminiChat.js +28 -13
  10. package/dist/src/core/geminiChat.js.map +1 -1
  11. package/dist/src/core/loggingContentGenerator.d.ts +1 -5
  12. package/dist/src/core/loggingContentGenerator.js +4 -6
  13. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  14. package/dist/src/core/nonInteractiveToolExecutor.d.ts +2 -2
  15. package/dist/src/core/nonInteractiveToolExecutor.js +13 -9
  16. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  17. package/dist/src/core/subagent.js +5 -5
  18. package/dist/src/core/subagent.js.map +1 -1
  19. package/dist/src/debug/ConfigurationManager.d.ts +28 -0
  20. package/dist/src/debug/ConfigurationManager.js +211 -0
  21. package/dist/src/debug/ConfigurationManager.js.map +1 -0
  22. package/dist/src/debug/DebugLogger.d.ts +26 -0
  23. package/dist/src/debug/DebugLogger.js +230 -0
  24. package/dist/src/debug/DebugLogger.js.map +1 -0
  25. package/dist/src/debug/FileOutput.d.ts +23 -0
  26. package/dist/src/debug/FileOutput.js +144 -0
  27. package/dist/src/debug/FileOutput.js.map +1 -0
  28. package/dist/src/debug/MockConfigurationManager.d.ts +28 -0
  29. package/dist/src/debug/MockConfigurationManager.js +45 -0
  30. package/dist/src/debug/MockConfigurationManager.js.map +1 -0
  31. package/dist/src/debug/MockFileOutput.d.ts +20 -0
  32. package/dist/src/debug/MockFileOutput.js +25 -0
  33. package/dist/src/debug/MockFileOutput.js.map +1 -0
  34. package/dist/src/debug/index.d.ts +8 -0
  35. package/dist/src/debug/index.js +9 -0
  36. package/dist/src/debug/index.js.map +1 -0
  37. package/dist/src/debug/types.d.ts +23 -0
  38. package/dist/src/debug/types.js +2 -0
  39. package/dist/src/debug/types.js.map +1 -0
  40. package/dist/src/ide/ide-client.d.ts +4 -0
  41. package/dist/src/ide/ide-client.js +41 -2
  42. package/dist/src/ide/ide-client.js.map +1 -1
  43. package/dist/src/ide/ide-installer.js +17 -4
  44. package/dist/src/ide/ide-installer.js.map +1 -1
  45. package/dist/src/ide/process-utils.d.ts +10 -5
  46. package/dist/src/ide/process-utils.js +113 -30
  47. package/dist/src/ide/process-utils.js.map +1 -1
  48. package/dist/src/index.d.ts +5 -0
  49. package/dist/src/index.js +4 -0
  50. package/dist/src/index.js.map +1 -1
  51. package/dist/src/prompt-config/defaults/core.md +34 -230
  52. package/dist/src/prompt-config/defaults/tools/read-file.md +1 -1
  53. package/dist/src/providers/BaseProvider.js +13 -2
  54. package/dist/src/providers/BaseProvider.js.map +1 -1
  55. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +1 -0
  56. package/dist/src/providers/anthropic/AnthropicProvider.js +20 -31
  57. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  58. package/dist/src/providers/gemini/GeminiProvider.d.ts +1 -0
  59. package/dist/src/providers/gemini/GeminiProvider.js +23 -50
  60. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  61. package/dist/src/providers/openai/OpenAIProvider.d.ts +9 -0
  62. package/dist/src/providers/openai/OpenAIProvider.js +502 -129
  63. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  64. package/dist/src/providers/openai/buildResponsesRequest.js +9 -6
  65. package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
  66. package/dist/src/providers/openai/getOpenAIProviderInfo.js +8 -3
  67. package/dist/src/providers/openai/getOpenAIProviderInfo.js.map +1 -1
  68. package/dist/src/providers/openai/parseResponsesStream.js +13 -4
  69. package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
  70. package/dist/src/providers/openai/syntheticToolResponses.d.ts +0 -9
  71. package/dist/src/providers/openai/syntheticToolResponses.js +87 -16
  72. package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
  73. package/dist/src/services/fileSystemService.d.ts +31 -0
  74. package/dist/src/services/fileSystemService.js +18 -0
  75. package/dist/src/services/fileSystemService.js.map +1 -0
  76. package/dist/src/services/loopDetectionService.d.ts +5 -0
  77. package/dist/src/services/loopDetectionService.js +89 -1
  78. package/dist/src/services/loopDetectionService.js.map +1 -1
  79. package/dist/src/services/shellExecutionService.d.ts +8 -6
  80. package/dist/src/services/shellExecutionService.js +297 -133
  81. package/dist/src/services/shellExecutionService.js.map +1 -1
  82. package/dist/src/telemetry/loggers.js +1 -1
  83. package/dist/src/telemetry/loggers.js.map +1 -1
  84. package/dist/src/telemetry/metrics.d.ts +1 -1
  85. package/dist/src/telemetry/metrics.js +2 -1
  86. package/dist/src/telemetry/metrics.js.map +1 -1
  87. package/dist/src/telemetry/sdk.js +3 -3
  88. package/dist/src/telemetry/sdk.js.map +1 -1
  89. package/dist/src/telemetry/types.d.ts +1 -0
  90. package/dist/src/telemetry/types.js +20 -0
  91. package/dist/src/telemetry/types.js.map +1 -1
  92. package/dist/src/telemetry/uiTelemetry.d.ts +4 -0
  93. package/dist/src/telemetry/uiTelemetry.js +16 -1
  94. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  95. package/dist/src/test-utils/tools.d.ts +8 -2
  96. package/dist/src/test-utils/tools.js +2 -3
  97. package/dist/src/test-utils/tools.js.map +1 -1
  98. package/dist/src/tools/ToolFormatter.d.ts +1 -0
  99. package/dist/src/tools/ToolFormatter.js +90 -9
  100. package/dist/src/tools/ToolFormatter.js.map +1 -1
  101. package/dist/src/tools/edit.d.ts +1 -1
  102. package/dist/src/tools/edit.js +15 -9
  103. package/dist/src/tools/edit.js.map +1 -1
  104. package/dist/src/tools/glob.d.ts +1 -1
  105. package/dist/src/tools/glob.js +8 -8
  106. package/dist/src/tools/glob.js.map +1 -1
  107. package/dist/src/tools/grep.d.ts +1 -1
  108. package/dist/src/tools/grep.js +1 -6
  109. package/dist/src/tools/grep.js.map +1 -1
  110. package/dist/src/tools/ls.d.ts +1 -1
  111. package/dist/src/tools/ls.js +1 -6
  112. package/dist/src/tools/ls.js.map +1 -1
  113. package/dist/src/tools/mcp-client-manager.d.ts +38 -0
  114. package/dist/src/tools/mcp-client-manager.js +74 -0
  115. package/dist/src/tools/mcp-client-manager.js.map +1 -0
  116. package/dist/src/tools/mcp-client.d.ts +43 -0
  117. package/dist/src/tools/mcp-client.js +117 -1
  118. package/dist/src/tools/mcp-client.js.map +1 -1
  119. package/dist/src/tools/memoryTool.d.ts +1 -1
  120. package/dist/src/tools/memoryTool.js +1 -6
  121. package/dist/src/tools/memoryTool.js.map +1 -1
  122. package/dist/src/tools/read-file.d.ts +1 -1
  123. package/dist/src/tools/read-file.js +5 -7
  124. package/dist/src/tools/read-file.js.map +1 -1
  125. package/dist/src/tools/read-many-files.d.ts +0 -1
  126. package/dist/src/tools/read-many-files.js +16 -11
  127. package/dist/src/tools/read-many-files.js.map +1 -1
  128. package/dist/src/tools/shell.d.ts +1 -1
  129. package/dist/src/tools/shell.js +32 -28
  130. package/dist/src/tools/shell.js.map +1 -1
  131. package/dist/src/tools/tool-registry.d.ts +5 -0
  132. package/dist/src/tools/tool-registry.js +13 -4
  133. package/dist/src/tools/tool-registry.js.map +1 -1
  134. package/dist/src/tools/tools.d.ts +3 -1
  135. package/dist/src/tools/tools.js +12 -0
  136. package/dist/src/tools/tools.js.map +1 -1
  137. package/dist/src/tools/web-fetch.d.ts +1 -1
  138. package/dist/src/tools/web-fetch.js +1 -6
  139. package/dist/src/tools/web-fetch.js.map +1 -1
  140. package/dist/src/tools/web-search.d.ts +6 -1
  141. package/dist/src/tools/web-search.js +6 -6
  142. package/dist/src/tools/web-search.js.map +1 -1
  143. package/dist/src/tools/write-file.d.ts +1 -1
  144. package/dist/src/tools/write-file.js +7 -8
  145. package/dist/src/tools/write-file.js.map +1 -1
  146. package/dist/src/utils/environmentContext.js +2 -2
  147. package/dist/src/utils/environmentContext.js.map +1 -1
  148. package/dist/src/utils/errorReporting.js +1 -1
  149. package/dist/src/utils/fileUtils.d.ts +2 -1
  150. package/dist/src/utils/fileUtils.js +3 -3
  151. package/dist/src/utils/fileUtils.js.map +1 -1
  152. package/dist/src/utils/getPty.d.ts +19 -0
  153. package/dist/src/utils/getPty.js +23 -0
  154. package/dist/src/utils/getPty.js.map +1 -0
  155. package/dist/src/utils/user_account.js +58 -53
  156. package/dist/src/utils/user_account.js.map +1 -1
  157. 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);
@@ -380,9 +382,10 @@ export class OpenAIProvider extends BaseProvider {
380
382
  // DEFENSIVE FIX: Handle potential array response from providers that violate OpenAI spec
381
383
  // Some providers (like Cerebras) may return an array of responses instead of a single response
382
384
  if (Array.isArray(data)) {
383
- if (process.env.DEBUG) {
384
- console.log('[OpenAIProvider] Detected malformed array response, aggregating...');
385
- }
385
+ this.logger.error(() => '[Cerebras Corruption] Detected malformed array response from provider, aggregating...', {
386
+ provider: this.baseURL,
387
+ arrayLength: data.length,
388
+ });
386
389
  const aggregatedContent = [];
387
390
  let aggregatedToolCalls = [];
388
391
  let aggregatedUsage = undefined;
@@ -483,9 +486,7 @@ export class OpenAIProvider extends BaseProvider {
483
486
  return models;
484
487
  }
485
488
  catch (error) {
486
- if (process.env.DEBUG) {
487
- console.error('Error fetching models from OpenAI:', error);
488
- }
489
+ this.logger.debug(() => `Error fetching models from OpenAI: ${error}`);
489
490
  // Return a hardcoded list as fallback
490
491
  // Check if this is a Qwen endpoint
491
492
  if (isQwenEndpoint(this.baseURL || '')) {
@@ -551,15 +552,32 @@ export class OpenAIProvider extends BaseProvider {
551
552
  });
552
553
  return;
553
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;
554
569
  // Validate tool messages have required tool_call_id
555
- const toolMessages = messages.filter((msg) => msg.role === 'tool');
570
+ const toolMessages = patchedMessages.filter((msg) => msg.role === 'tool');
556
571
  const missingIds = toolMessages.filter((msg) => !msg.tool_call_id);
557
572
  if (missingIds.length > 0) {
558
- if (process.env.DEBUG) {
559
- console.error('[OpenAIProvider] FATAL: Tool messages missing tool_call_id:', missingIds);
560
- }
573
+ this.logger.error(() => `FATAL: Tool messages missing tool_call_id: ${JSON.stringify(missingIds)}`);
561
574
  throw new Error(`OpenAI API requires tool_call_id for all tool messages. Found ${missingIds.length} tool message(s) without IDs.`);
562
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
+ }
563
581
  const parser = this.requiresTextToolCallParsing()
564
582
  ? new GemmaToolCallParser()
565
583
  : null;
@@ -573,24 +591,88 @@ export class OpenAIProvider extends BaseProvider {
573
591
  const finalStreamOptions = streamOptions !== undefined ? streamOptions : { include_usage: true };
574
592
  // Get streaming setting from ephemeral settings (default: enabled)
575
593
  const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
576
- const streamingEnabled = streamingSetting !== 'disabled';
594
+ let streamingEnabled = streamingSetting !== 'disabled';
577
595
  // Get resolved authentication and update client if needed
578
596
  await this.updateClientWithResolvedAuth();
579
- if (process.env.DEBUG) {
580
- console.log(`[OpenAI] About to make API call with model: ${this.currentModel}, baseURL: ${this.openai.baseURL}, apiKey: ${this.openai.apiKey?.substring(0, 10)}..., streaming: ${streamingEnabled}`);
581
- }
582
- // Build request params with exact order from original
583
- const response = await this.openai.chat.completions.create({
584
- model: this.currentModel,
585
- messages: messages,
586
- stream: streamingEnabled,
587
- ...(streamingEnabled && finalStreamOptions !== null
588
- ? { stream_options: finalStreamOptions }
589
- : {}),
590
- tools: formattedTools,
591
- tool_choice: this.getToolChoiceForFormat(tools),
592
- ...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;
593
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
+ }
594
676
  let fullContent = '';
595
677
  const accumulatedToolCalls = [];
596
678
  let hasStreamedContent = false;
@@ -602,8 +684,122 @@ export class OpenAIProvider extends BaseProvider {
602
684
  // We need to buffer all chunks to detect and handle malformed streams
603
685
  // Some providers (like Cerebras) send message format instead of delta
604
686
  const allChunks = [];
605
- for await (const chunk of response) {
606
- allChunks.push(chunk);
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,
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;
800
+ }
801
+ // Re-throw other errors
802
+ throw error;
607
803
  }
608
804
  // Check first chunk to see if we have malformed stream
609
805
  let detectedMalformedStream = false;
@@ -612,9 +808,7 @@ export class OpenAIProvider extends BaseProvider {
612
808
  if (firstChunk.choices?.[0]?.message &&
613
809
  !firstChunk.choices?.[0]?.delta) {
614
810
  detectedMalformedStream = true;
615
- if (process.env.DEBUG) {
616
- console.log('[OpenAIProvider] Detected malformed stream (message instead of delta), using aggregation mode');
617
- }
811
+ this.logger.debug(() => 'Detected malformed stream (message instead of delta), using aggregation mode');
618
812
  }
619
813
  }
620
814
  // If we detected issues, aggregate everything
@@ -629,10 +823,22 @@ export class OpenAIProvider extends BaseProvider {
629
823
  contentParts.push(message.content);
630
824
  }
631
825
  if (message?.tool_calls) {
632
- aggregatedToolCalls = 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
+ }));
633
835
  }
634
836
  if (chunk.usage) {
635
- finalUsageData = 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
+ };
636
842
  }
637
843
  }
638
844
  // Yield single reconstructed message
@@ -646,63 +852,55 @@ export class OpenAIProvider extends BaseProvider {
646
852
  }
647
853
  // Process chunks normally - stream them as they come
648
854
  for (const chunk of allChunks) {
649
- const cerebrasChunk = chunk;
650
- // DEFENSIVE FIX: Some providers send full message format instead of delta in streaming
651
- // This violates OpenAI spec but we handle it gracefully
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)
652
857
  let processedChunk = chunk;
653
- if (cerebrasChunk.choices?.[0]?.message &&
654
- !cerebrasChunk.choices[0].delta) {
655
- if (process.env.DEBUG) {
656
- console.log('[OpenAIProvider] Converting malformed chunk from message to delta format');
657
- }
658
- // Convert full message format to delta format - create new object instead of modifying
659
- const message = cerebrasChunk.choices[0].message;
660
- const originalChunk = 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;
661
867
  processedChunk = {
662
- ...originalChunk,
663
868
  choices: [
664
869
  {
665
870
  delta: {
666
- content: message.content,
667
- tool_calls: message.tool_calls?.map((tc, index) => ({
668
- ...tc,
669
- index,
670
- })),
871
+ content: message?.content ?? undefined,
872
+ role: message?.role,
873
+ tool_calls: message?.tool_calls,
671
874
  },
672
- message: undefined, // Remove message field
673
875
  },
674
876
  ],
675
- usage: originalChunk.usage,
877
+ usage: chunk.usage,
676
878
  };
677
879
  }
678
880
  const delta = processedChunk.choices?.[0]?.delta;
679
881
  if (delta?.content) {
680
882
  // Enhanced debug logging to understand streaming behavior
681
- if (process.env.DEBUG && this.isUsingQwen()) {
682
- console.log(`[OpenAIProvider/${this.currentModel}] Chunk:`, {
883
+ if (this.isUsingQwen()) {
884
+ this.logger.debug(() => `Chunk: ${JSON.stringify({
683
885
  content: delta.content,
684
- contentLength: delta.content.length,
685
- isWhitespaceOnly: delta.content.trim() === '',
886
+ contentLength: delta.content?.length ?? 0,
887
+ isWhitespaceOnly: delta.content?.trim() === '',
686
888
  chunkIndex: 0,
687
- });
889
+ })}`);
688
890
  }
689
891
  // For text-based models, don't yield content chunks yet
690
- if (!parser) {
892
+ if (!parser && delta.content) {
691
893
  if (this.isUsingQwen()) {
692
894
  const isWhitespaceOnly = delta.content.trim() === '';
693
895
  if (isWhitespaceOnly) {
694
896
  // Buffer whitespace-only chunk
695
897
  pendingWhitespace = (pendingWhitespace || '') + delta.content;
696
- if (process.env.DEBUG) {
697
- console.log(`[OpenAIProvider/${this.currentModel}] Buffered whitespace-only chunk (len=${delta.content.length}). pendingWhitespace now len=${pendingWhitespace.length}`);
698
- }
898
+ this.logger.debug(() => `Buffered whitespace-only chunk (len=${delta.content?.length ?? 0}). pendingWhitespace now len=${pendingWhitespace?.length ?? 0}`);
699
899
  continue;
700
900
  }
701
901
  else if (pendingWhitespace) {
702
902
  // Flush buffered whitespace before non-empty chunk to preserve spacing
703
- if (process.env.DEBUG) {
704
- console.log(`[OpenAIProvider/${this.currentModel}] Flushing pending whitespace (len=${pendingWhitespace.length}) before non-empty chunk`);
705
- }
903
+ this.logger.debug(() => `Flushing pending whitespace (len=${pendingWhitespace?.length ?? 0}) before non-empty chunk`);
706
904
  yield {
707
905
  role: ContentGeneratorRole.ASSISTANT,
708
906
  content: pendingWhitespace,
@@ -728,9 +926,9 @@ export class OpenAIProvider extends BaseProvider {
728
926
  // Check for usage data in the chunk
729
927
  if (processedChunk.usage) {
730
928
  usageData = {
731
- prompt_tokens: processedChunk.usage.prompt_tokens,
732
- completion_tokens: processedChunk.usage.completion_tokens,
733
- total_tokens: processedChunk.usage.total_tokens,
929
+ prompt_tokens: processedChunk.usage.prompt_tokens || 0,
930
+ completion_tokens: processedChunk.usage.completion_tokens || 0,
931
+ total_tokens: processedChunk.usage.total_tokens || 0,
734
932
  };
735
933
  }
736
934
  }
@@ -746,6 +944,7 @@ export class OpenAIProvider extends BaseProvider {
746
944
  // Convert tool calls to the standard format
747
945
  for (const toolCall of choice.message.tool_calls) {
748
946
  if (toolCall.type === 'function' && toolCall.function) {
947
+ // Don't fix double stringification here - it's handled later in the final processing
749
948
  accumulatedToolCalls.push({
750
949
  id: toolCall.id,
751
950
  type: 'function',
@@ -772,9 +971,7 @@ export class OpenAIProvider extends BaseProvider {
772
971
  }
773
972
  // Flush any remaining pending whitespace for Qwen
774
973
  if (pendingWhitespace && this.isUsingQwen() && !parser) {
775
- if (process.env.DEBUG) {
776
- console.log(`[OpenAIProvider/${this.currentModel}] Flushing trailing pending whitespace (len=${pendingWhitespace.length}) at stream end`);
777
- }
974
+ this.logger.debug(() => `Flushing trailing pending whitespace (len=${pendingWhitespace?.length ?? 0}) at stream end`);
778
975
  yield {
779
976
  role: ContentGeneratorRole.ASSISTANT,
780
977
  content: pendingWhitespace,
@@ -815,35 +1012,144 @@ export class OpenAIProvider extends BaseProvider {
815
1012
  else {
816
1013
  // Standard OpenAI tool call handling
817
1014
  if (accumulatedToolCalls.length > 0) {
818
- if (process.env.DEBUG && this.isUsingQwen()) {
819
- 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({
820
1119
  contentLength: fullContent.length,
821
1120
  content: fullContent.substring(0, 200) +
822
1121
  (fullContent.length > 200 ? '...' : ''),
823
1122
  toolCallCount: accumulatedToolCalls.length,
824
1123
  hasStreamedContent,
825
- });
1124
+ })}`);
826
1125
  }
827
1126
  // For Qwen models, don't duplicate content if we've already streamed it
828
1127
  // BUT Cerebras needs at least a space to continue after tool responses
829
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
+ }
830
1135
  const shouldOmitContent = hasStreamedContent && this.isUsingQwen() && !isCerebras;
831
1136
  if (shouldOmitContent) {
832
1137
  // Only yield tool calls with empty content to avoid duplication
833
1138
  yield {
834
1139
  role: ContentGeneratorRole.ASSISTANT,
835
1140
  content: '',
836
- tool_calls: accumulatedToolCalls,
1141
+ tool_calls: fixedToolCalls,
837
1142
  usage: usageData,
838
1143
  };
839
1144
  }
840
1145
  else if (isCerebras && hasStreamedContent) {
841
1146
  // Cerebras: Send just a space to prevent duplication but allow continuation
842
1147
  // This prevents the repeated "Let me search..." text
1148
+ this.logger.debug(() => '[Cerebras] Sending minimal space content to prevent duplication');
843
1149
  yield {
844
1150
  role: ContentGeneratorRole.ASSISTANT,
845
1151
  content: ' ', // Single space instead of full content
846
- tool_calls: accumulatedToolCalls,
1152
+ tool_calls: fixedToolCalls,
847
1153
  usage: usageData,
848
1154
  };
849
1155
  }
@@ -852,7 +1158,7 @@ export class OpenAIProvider extends BaseProvider {
852
1158
  yield {
853
1159
  role: ContentGeneratorRole.ASSISTANT,
854
1160
  content: fullContent || '',
855
- tool_calls: accumulatedToolCalls,
1161
+ tool_calls: fixedToolCalls,
856
1162
  usage: usageData,
857
1163
  };
858
1164
  }
@@ -870,9 +1176,7 @@ export class OpenAIProvider extends BaseProvider {
870
1176
  setModel(modelId) {
871
1177
  // Update SettingsService as the source of truth
872
1178
  this.setModelInSettings(modelId).catch((error) => {
873
- if (process.env.DEBUG) {
874
- console.warn('Failed to persist model to SettingsService:', error);
875
- }
1179
+ this.logger.debug(() => `Failed to persist model to SettingsService: ${error}`);
876
1180
  });
877
1181
  // Keep local cache for performance
878
1182
  this.currentModel = modelId;
@@ -887,9 +1191,7 @@ export class OpenAIProvider extends BaseProvider {
887
1191
  }
888
1192
  }
889
1193
  catch (error) {
890
- if (process.env.DEBUG) {
891
- console.warn('Failed to get model from SettingsService:', error);
892
- }
1194
+ this.logger.debug(() => `Failed to get model from SettingsService: ${error}`);
893
1195
  }
894
1196
  // Fall back to cached value or default
895
1197
  return this.currentModel || this.getDefaultModel();
@@ -900,16 +1202,14 @@ export class OpenAIProvider extends BaseProvider {
900
1202
  if (this.isUsingQwen()) {
901
1203
  return 'qwen3-coder-plus';
902
1204
  }
903
- return process.env.LLXPRT_DEFAULT_MODEL || 'llama3-70b-8192';
1205
+ return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
904
1206
  }
905
1207
  setApiKey(apiKey) {
906
1208
  // Call base provider implementation
907
1209
  super.setApiKey?.(apiKey);
908
1210
  // Persist to SettingsService if available
909
1211
  this.setApiKeyInSettings(apiKey).catch((error) => {
910
- if (process.env.DEBUG) {
911
- console.warn('Failed to persist API key to SettingsService:', error);
912
- }
1212
+ this.logger.debug(() => `Failed to persist API key to SettingsService: ${error}`);
913
1213
  });
914
1214
  // Create a new OpenAI client with the updated API key
915
1215
  const clientOptions = {
@@ -928,9 +1228,7 @@ export class OpenAIProvider extends BaseProvider {
928
1228
  this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
929
1229
  // Persist to SettingsService if available
930
1230
  this.setBaseUrlInSettings(this.baseURL).catch((error) => {
931
- if (process.env.DEBUG) {
932
- console.warn('Failed to persist base URL to SettingsService:', error);
933
- }
1231
+ this.logger.debug(() => `Failed to persist base URL to SettingsService: ${error}`);
934
1232
  });
935
1233
  // Update OAuth configuration based on endpoint validation
936
1234
  // Enable OAuth for Qwen endpoints if we have an OAuth manager
@@ -978,6 +1276,89 @@ export class OpenAIProvider extends BaseProvider {
978
1276
  getConversationCache() {
979
1277
  return this.conversationCache;
980
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
+ }
981
1362
  /**
982
1363
  * OpenAI always requires payment (API key)
983
1364
  */
@@ -1013,9 +1394,7 @@ export class OpenAIProvider extends BaseProvider {
1013
1394
  }
1014
1395
  // Persist to SettingsService if available
1015
1396
  this.setModelParamsInSettings(this.modelParams).catch((error) => {
1016
- if (process.env.DEBUG) {
1017
- console.warn('Failed to persist model params to SettingsService:', error);
1018
- }
1397
+ this.logger.debug(() => `Failed to persist model params to SettingsService: ${error}`);
1019
1398
  });
1020
1399
  }
1021
1400
  /**
@@ -1045,14 +1424,10 @@ export class OpenAIProvider extends BaseProvider {
1045
1424
  if (savedParams) {
1046
1425
  this.modelParams = savedParams;
1047
1426
  }
1048
- if (process.env.DEBUG) {
1049
- console.log(`[OpenAI] Initialized from SettingsService - model: ${this.currentModel}, baseURL: ${this.baseURL}, params:`, this.modelParams);
1050
- }
1427
+ this.logger.debug(() => `Initialized from SettingsService - model: ${this.currentModel}, baseURL: ${this.baseURL}, params: ${JSON.stringify(this.modelParams)}`);
1051
1428
  }
1052
1429
  catch (error) {
1053
- if (process.env.DEBUG) {
1054
- console.error('Failed to initialize OpenAI provider from SettingsService:', error);
1055
- }
1430
+ this.logger.debug(() => `Failed to initialize OpenAI provider from SettingsService: ${error}`);
1056
1431
  }
1057
1432
  }
1058
1433
  /**
@@ -1092,9 +1467,7 @@ export class OpenAIProvider extends BaseProvider {
1092
1467
  return 'openai';
1093
1468
  }
1094
1469
  catch (error) {
1095
- if (process.env.DEBUG) {
1096
- console.warn('Failed to detect tool format from SettingsService:', error);
1097
- }
1470
+ this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
1098
1471
  // Fallback detection without SettingsService
1099
1472
  const modelName = this.currentModel.toLowerCase();
1100
1473
  if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {