@vybestack/llxprt-code-core 0.1.22 → 0.1.23-nightly.250824.52dcfe37

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 (168) hide show
  1. package/README.md +19 -0
  2. package/dist/src/code_assist/oauth2.js +60 -31
  3. package/dist/src/code_assist/oauth2.js.map +1 -1
  4. package/dist/src/config/config.d.ts +14 -1
  5. package/dist/src/config/config.js +23 -3
  6. package/dist/src/config/config.js.map +1 -1
  7. package/dist/src/core/client.js +2 -2
  8. package/dist/src/core/client.js.map +1 -1
  9. package/dist/src/core/coreToolScheduler.d.ts +2 -1
  10. package/dist/src/core/coreToolScheduler.js +19 -0
  11. package/dist/src/core/coreToolScheduler.js.map +1 -1
  12. package/dist/src/core/geminiChat.js +28 -13
  13. package/dist/src/core/geminiChat.js.map +1 -1
  14. package/dist/src/core/loggingContentGenerator.d.ts +1 -5
  15. package/dist/src/core/loggingContentGenerator.js +4 -6
  16. package/dist/src/core/loggingContentGenerator.js.map +1 -1
  17. package/dist/src/core/nonInteractiveToolExecutor.d.ts +2 -2
  18. package/dist/src/core/nonInteractiveToolExecutor.js +13 -9
  19. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  20. package/dist/src/core/subagent.js +5 -5
  21. package/dist/src/core/subagent.js.map +1 -1
  22. package/dist/src/debug/ConfigurationManager.d.ts +28 -0
  23. package/dist/src/debug/ConfigurationManager.js +211 -0
  24. package/dist/src/debug/ConfigurationManager.js.map +1 -0
  25. package/dist/src/debug/DebugLogger.d.ts +26 -0
  26. package/dist/src/debug/DebugLogger.js +230 -0
  27. package/dist/src/debug/DebugLogger.js.map +1 -0
  28. package/dist/src/debug/FileOutput.d.ts +23 -0
  29. package/dist/src/debug/FileOutput.js +144 -0
  30. package/dist/src/debug/FileOutput.js.map +1 -0
  31. package/dist/src/debug/MockConfigurationManager.d.ts +28 -0
  32. package/dist/src/debug/MockConfigurationManager.js +45 -0
  33. package/dist/src/debug/MockConfigurationManager.js.map +1 -0
  34. package/dist/src/debug/MockFileOutput.d.ts +20 -0
  35. package/dist/src/debug/MockFileOutput.js +25 -0
  36. package/dist/src/debug/MockFileOutput.js.map +1 -0
  37. package/dist/src/debug/index.d.ts +8 -0
  38. package/dist/src/debug/index.js +9 -0
  39. package/dist/src/debug/index.js.map +1 -0
  40. package/dist/src/debug/types.d.ts +23 -0
  41. package/dist/src/debug/types.js +2 -0
  42. package/dist/src/debug/types.js.map +1 -0
  43. package/dist/src/ide/ide-client.d.ts +4 -0
  44. package/dist/src/ide/ide-client.js +41 -2
  45. package/dist/src/ide/ide-client.js.map +1 -1
  46. package/dist/src/ide/ide-installer.js +17 -4
  47. package/dist/src/ide/ide-installer.js.map +1 -1
  48. package/dist/src/ide/process-utils.d.ts +10 -5
  49. package/dist/src/ide/process-utils.js +113 -30
  50. package/dist/src/ide/process-utils.js.map +1 -1
  51. package/dist/src/index.d.ts +5 -0
  52. package/dist/src/index.js +4 -0
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/prompt-config/defaults/core.md +34 -230
  55. package/dist/src/prompt-config/defaults/tools/read-file.md +1 -1
  56. package/dist/src/providers/BaseProvider.js +13 -2
  57. package/dist/src/providers/BaseProvider.js.map +1 -1
  58. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +1 -0
  59. package/dist/src/providers/anthropic/AnthropicProvider.js +20 -31
  60. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  61. package/dist/src/providers/gemini/GeminiProvider.d.ts +6 -0
  62. package/dist/src/providers/gemini/GeminiProvider.js +31 -50
  63. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  64. package/dist/src/providers/openai/OpenAIProvider.d.ts +1 -0
  65. package/dist/src/providers/openai/OpenAIProvider.js +513 -131
  66. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  67. package/dist/src/providers/openai/buildResponsesRequest.js +9 -6
  68. package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
  69. package/dist/src/providers/openai/getOpenAIProviderInfo.js +8 -3
  70. package/dist/src/providers/openai/getOpenAIProviderInfo.js.map +1 -1
  71. package/dist/src/providers/openai/parseResponsesStream.js +13 -4
  72. package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
  73. package/dist/src/providers/openai/syntheticToolResponses.d.ts +0 -9
  74. package/dist/src/providers/openai/syntheticToolResponses.js +87 -16
  75. package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
  76. package/dist/src/services/ClipboardService.d.ts +19 -0
  77. package/dist/src/services/ClipboardService.js +66 -0
  78. package/dist/src/services/ClipboardService.js.map +1 -0
  79. package/dist/src/services/fileSystemService.d.ts +31 -0
  80. package/dist/src/services/fileSystemService.js +18 -0
  81. package/dist/src/services/fileSystemService.js.map +1 -0
  82. package/dist/src/services/index.d.ts +1 -0
  83. package/dist/src/services/index.js +1 -0
  84. package/dist/src/services/index.js.map +1 -1
  85. package/dist/src/services/loopDetectionService.d.ts +5 -0
  86. package/dist/src/services/loopDetectionService.js +89 -1
  87. package/dist/src/services/loopDetectionService.js.map +1 -1
  88. package/dist/src/services/shellExecutionService.d.ts +8 -6
  89. package/dist/src/services/shellExecutionService.js +297 -133
  90. package/dist/src/services/shellExecutionService.js.map +1 -1
  91. package/dist/src/telemetry/loggers.js +1 -1
  92. package/dist/src/telemetry/loggers.js.map +1 -1
  93. package/dist/src/telemetry/metrics.d.ts +1 -1
  94. package/dist/src/telemetry/metrics.js +2 -1
  95. package/dist/src/telemetry/metrics.js.map +1 -1
  96. package/dist/src/telemetry/sdk.js +3 -3
  97. package/dist/src/telemetry/sdk.js.map +1 -1
  98. package/dist/src/telemetry/types.d.ts +1 -0
  99. package/dist/src/telemetry/types.js +20 -0
  100. package/dist/src/telemetry/types.js.map +1 -1
  101. package/dist/src/telemetry/uiTelemetry.d.ts +4 -0
  102. package/dist/src/telemetry/uiTelemetry.js +16 -1
  103. package/dist/src/telemetry/uiTelemetry.js.map +1 -1
  104. package/dist/src/test-utils/tools.d.ts +8 -2
  105. package/dist/src/test-utils/tools.js +2 -3
  106. package/dist/src/test-utils/tools.js.map +1 -1
  107. package/dist/src/tools/ToolFormatter.d.ts +1 -0
  108. package/dist/src/tools/ToolFormatter.js +90 -9
  109. package/dist/src/tools/ToolFormatter.js.map +1 -1
  110. package/dist/src/tools/edit.d.ts +1 -1
  111. package/dist/src/tools/edit.js +15 -9
  112. package/dist/src/tools/edit.js.map +1 -1
  113. package/dist/src/tools/glob.d.ts +1 -1
  114. package/dist/src/tools/glob.js +8 -8
  115. package/dist/src/tools/glob.js.map +1 -1
  116. package/dist/src/tools/grep.d.ts +1 -1
  117. package/dist/src/tools/grep.js +1 -6
  118. package/dist/src/tools/grep.js.map +1 -1
  119. package/dist/src/tools/ls.d.ts +1 -1
  120. package/dist/src/tools/ls.js +1 -6
  121. package/dist/src/tools/ls.js.map +1 -1
  122. package/dist/src/tools/mcp-client-manager.d.ts +38 -0
  123. package/dist/src/tools/mcp-client-manager.js +74 -0
  124. package/dist/src/tools/mcp-client-manager.js.map +1 -0
  125. package/dist/src/tools/mcp-client.d.ts +43 -0
  126. package/dist/src/tools/mcp-client.js +117 -1
  127. package/dist/src/tools/mcp-client.js.map +1 -1
  128. package/dist/src/tools/memoryTool.d.ts +1 -1
  129. package/dist/src/tools/memoryTool.js +1 -6
  130. package/dist/src/tools/memoryTool.js.map +1 -1
  131. package/dist/src/tools/read-file.d.ts +7 -2
  132. package/dist/src/tools/read-file.js +22 -13
  133. package/dist/src/tools/read-file.js.map +1 -1
  134. package/dist/src/tools/read-many-files.d.ts +0 -1
  135. package/dist/src/tools/read-many-files.js +16 -11
  136. package/dist/src/tools/read-many-files.js.map +1 -1
  137. package/dist/src/tools/shell.d.ts +1 -1
  138. package/dist/src/tools/shell.js +32 -28
  139. package/dist/src/tools/shell.js.map +1 -1
  140. package/dist/src/tools/tool-registry.d.ts +5 -0
  141. package/dist/src/tools/tool-registry.js +13 -4
  142. package/dist/src/tools/tool-registry.js.map +1 -1
  143. package/dist/src/tools/tools.d.ts +3 -1
  144. package/dist/src/tools/tools.js +25 -0
  145. package/dist/src/tools/tools.js.map +1 -1
  146. package/dist/src/tools/web-fetch.d.ts +1 -1
  147. package/dist/src/tools/web-fetch.js +1 -6
  148. package/dist/src/tools/web-fetch.js.map +1 -1
  149. package/dist/src/tools/web-search.d.ts +6 -1
  150. package/dist/src/tools/web-search.js +6 -6
  151. package/dist/src/tools/web-search.js.map +1 -1
  152. package/dist/src/tools/write-file.d.ts +7 -2
  153. package/dist/src/tools/write-file.js +49 -31
  154. package/dist/src/tools/write-file.js.map +1 -1
  155. package/dist/src/utils/environmentContext.js +2 -2
  156. package/dist/src/utils/environmentContext.js.map +1 -1
  157. package/dist/src/utils/errorReporting.js +1 -1
  158. package/dist/src/utils/fileUtils.d.ts +2 -1
  159. package/dist/src/utils/fileUtils.js +3 -3
  160. package/dist/src/utils/fileUtils.js.map +1 -1
  161. package/dist/src/utils/getPty.d.ts +19 -0
  162. package/dist/src/utils/getPty.js +23 -0
  163. package/dist/src/utils/getPty.js.map +1 -0
  164. package/dist/src/utils/schemaValidator.js +16 -1
  165. package/dist/src/utils/schemaValidator.js.map +1 -1
  166. package/dist/src/utils/user_account.js +58 -53
  167. package/dist/src/utils/user_account.js.map +1 -1
  168. 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
@@ -271,6 +275,7 @@ export class OpenAIProvider extends BaseProvider {
271
275
  conversationId: options?.conversationId,
272
276
  parentId: options?.parentId,
273
277
  tool_choice: options?.tool_choice,
278
+ ...(this.modelParams || {}),
274
279
  });
275
280
  // Make the API call
276
281
  const baseURL = this.baseURL || 'https://api.openai.com/v1';
@@ -295,9 +300,7 @@ export class OpenAIProvider extends BaseProvider {
295
300
  // Handle 422 context_length_exceeded error
296
301
  if (response.status === 422 &&
297
302
  errorBody.includes('context_length_exceeded')) {
298
- if (process.env.DEBUG) {
299
- console.warn('[OpenAI] Context length exceeded, invalidating cache and retrying stateless...');
300
- }
303
+ this.logger.debug(() => 'Context length exceeded, invalidating cache and retrying stateless...');
301
304
  // Invalidate the cache for this conversation
302
305
  if (options?.conversationId && options?.parentId) {
303
306
  this.conversationCache.invalidate(options.conversationId, options.parentId);
@@ -310,6 +313,7 @@ export class OpenAIProvider extends BaseProvider {
310
313
  stream: options?.stream ?? true,
311
314
  // Omit conversationId and parentId for stateless retry
312
315
  tool_choice: options?.tool_choice,
316
+ ...(this.modelParams || {}),
313
317
  });
314
318
  // Ensure proper UTF-8 encoding for retry request as well
315
319
  const retryRequestBody = JSON.stringify(retryRequest);
@@ -380,9 +384,10 @@ export class OpenAIProvider extends BaseProvider {
380
384
  // DEFENSIVE FIX: Handle potential array response from providers that violate OpenAI spec
381
385
  // Some providers (like Cerebras) may return an array of responses instead of a single response
382
386
  if (Array.isArray(data)) {
383
- if (process.env.DEBUG) {
384
- console.log('[OpenAIProvider] Detected malformed array response, aggregating...');
385
- }
387
+ this.logger.error(() => '[Cerebras Corruption] Detected malformed array response from provider, aggregating...', {
388
+ provider: this.baseURL,
389
+ arrayLength: data.length,
390
+ });
386
391
  const aggregatedContent = [];
387
392
  let aggregatedToolCalls = [];
388
393
  let aggregatedUsage = undefined;
@@ -483,9 +488,7 @@ export class OpenAIProvider extends BaseProvider {
483
488
  return models;
484
489
  }
485
490
  catch (error) {
486
- if (process.env.DEBUG) {
487
- console.error('Error fetching models from OpenAI:', error);
488
- }
491
+ this.logger.debug(() => `Error fetching models from OpenAI: ${error}`);
489
492
  // Return a hardcoded list as fallback
490
493
  // Check if this is a Qwen endpoint
491
494
  if (isQwenEndpoint(this.baseURL || '')) {
@@ -551,15 +554,60 @@ export class OpenAIProvider extends BaseProvider {
551
554
  });
552
555
  return;
553
556
  }
557
+ // Don't automatically add synthetic responses - they should only be added when tools are actually cancelled
558
+ // Check if we have any existing synthetic responses (from actual cancellations)
559
+ const existingSyntheticCount = messages.filter((msg) => msg._synthetic).length;
560
+ if (existingSyntheticCount > 0) {
561
+ this.logger.debug(() => `[Synthetic] Found ${existingSyntheticCount} existing synthetic responses in conversation`);
562
+ }
563
+ // Just use the messages as-is without "fixing" them
564
+ const patchedMessages = messages;
554
565
  // Validate tool messages have required tool_call_id
555
- const toolMessages = messages.filter((msg) => msg.role === 'tool');
566
+ const toolMessages = patchedMessages.filter((msg) => msg.role === 'tool');
556
567
  const missingIds = toolMessages.filter((msg) => !msg.tool_call_id);
557
568
  if (missingIds.length > 0) {
558
- if (process.env.DEBUG) {
559
- console.error('[OpenAIProvider] FATAL: Tool messages missing tool_call_id:', missingIds);
560
- }
569
+ this.logger.error(() => `FATAL: Tool messages missing tool_call_id: ${JSON.stringify(missingIds)}`);
561
570
  throw new Error(`OpenAI API requires tool_call_id for all tool messages. Found ${missingIds.length} tool message(s) without IDs.`);
562
571
  }
572
+ // Log synthetic responses for debugging
573
+ const syntheticMessages = patchedMessages.filter((msg) => msg._synthetic);
574
+ if (syntheticMessages.length > 0) {
575
+ this.logger.debug(() => `[Synthetic] Added ${syntheticMessages.length} synthetic tool responses`);
576
+ // Check for ordering issues - using debug logger which only executes when enabled
577
+ this.logger.debug(() => {
578
+ const orderingErrors = [];
579
+ const orderingWarnings = [];
580
+ for (let i = 0; i < patchedMessages.length - 1; i++) {
581
+ const current = patchedMessages[i];
582
+ const next = patchedMessages[i + 1];
583
+ // Check if a tool response comes before its corresponding tool call
584
+ if (current.role === 'tool' && current.tool_call_id) {
585
+ // Find the assistant message with this tool call
586
+ const callIndex = patchedMessages.findIndex((m) => m.role === 'assistant' &&
587
+ m.tool_calls?.some((tc) => tc.id === current.tool_call_id));
588
+ if (callIndex === -1 || callIndex > i) {
589
+ orderingErrors.push(`Tool response ${current.tool_call_id} appears before its tool call or call not found`);
590
+ }
591
+ }
592
+ // Check if we have consecutive assistant messages with tool calls
593
+ if (current.role === 'assistant' &&
594
+ current.tool_calls &&
595
+ next.role === 'assistant' &&
596
+ next.tool_calls) {
597
+ orderingWarnings.push(`Consecutive assistant messages with tool calls at indices ${i} and ${i + 1}`);
598
+ }
599
+ }
600
+ if (orderingErrors.length > 0) {
601
+ return `[Synthetic Order Check] Errors found: ${orderingErrors.join('; ')}`;
602
+ }
603
+ else if (orderingWarnings.length > 0) {
604
+ return `[Synthetic Order Check] Warnings: ${orderingWarnings.join('; ')}`;
605
+ }
606
+ else {
607
+ return '[Synthetic Order Check] No issues found';
608
+ }
609
+ });
610
+ }
563
611
  const parser = this.requiresTextToolCallParsing()
564
612
  ? new GemmaToolCallParser()
565
613
  : null;
@@ -573,24 +621,106 @@ export class OpenAIProvider extends BaseProvider {
573
621
  const finalStreamOptions = streamOptions !== undefined ? streamOptions : { include_usage: true };
574
622
  // Get streaming setting from ephemeral settings (default: enabled)
575
623
  const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
576
- const streamingEnabled = streamingSetting !== 'disabled';
624
+ let streamingEnabled = streamingSetting !== 'disabled';
577
625
  // Get resolved authentication and update client if needed
578
626
  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,
627
+ // Strip internal tracking fields that some APIs don't accept
628
+ // We keep the synthetic responses but remove the metadata fields
629
+ const cleanedMessages = patchedMessages.map((msg) => {
630
+ // Create a shallow copy and remove internal fields
631
+ const { _synthetic, _cancelled, ...cleanMsg } = msg;
632
+ // Log synthetic tool responses for debugging
633
+ if (msg._synthetic) {
634
+ this.logger.debug(() => `[Synthetic Tool Response] ${JSON.stringify(cleanMsg)}`);
635
+ }
636
+ return cleanMsg;
593
637
  });
638
+ 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}`);
639
+ // Debug: Log message roles being sent
640
+ this.logger.debug(() => `Messages being sent to OpenAI (${cleanedMessages.length} total): ${cleanedMessages
641
+ .map((m) => `${m.role}${m.role === 'system' ? ` (length: ${m.content?.length})` : ''}`)
642
+ .join(', ')}`);
643
+ let response;
644
+ try {
645
+ // Build request params with exact order from original
646
+ response = await this.openai.chat.completions.create({
647
+ model: this.currentModel,
648
+ messages: cleanedMessages,
649
+ stream: streamingEnabled,
650
+ ...(streamingEnabled && finalStreamOptions
651
+ ? { stream_options: finalStreamOptions }
652
+ : {}),
653
+ tools: formattedTools,
654
+ tool_choice: this.getToolChoiceForFormat(tools),
655
+ ...this.modelParams,
656
+ });
657
+ }
658
+ catch (error) {
659
+ // Debug the error
660
+ const errorStatus = error?.status ||
661
+ error?.response?.status;
662
+ const errorLabel = errorStatus === 400 ? '[API Error 400]' : '[API Error]';
663
+ this.logger.error(() => `${errorLabel} Error caught in API call: ${error}`);
664
+ this.logger.error(() => `${errorLabel} Error type: ${error?.constructor?.name}`);
665
+ this.logger.error(() => `${errorLabel} Error status: ${errorStatus}`);
666
+ this.logger.error(() => `${errorLabel} Error response data: ${JSON.stringify(error?.response?.data, null, 2)}`);
667
+ // Log the last few messages to understand what's being sent
668
+ if (errorStatus === 400) {
669
+ // Log additional diagnostics for 400 errors
670
+ const hasSyntheticMessages = cleanedMessages.some((msg) => msg.role === 'tool' &&
671
+ msg.content === 'Tool execution cancelled by user');
672
+ const hasPendingToolCalls = cleanedMessages.some((msg, idx) => {
673
+ if (msg.role === 'assistant' && msg.tool_calls) {
674
+ // Check if there's a matching tool response
675
+ const toolCallIds = msg.tool_calls.map((tc) => tc.id);
676
+ const hasResponses = toolCallIds.every((id) => cleanedMessages
677
+ .slice(idx + 1)
678
+ .some((m) => m.role === 'tool' && m.tool_call_id === id));
679
+ return !hasResponses;
680
+ }
681
+ return false;
682
+ });
683
+ this.logger.error(() => `${errorLabel} Last 5 messages being sent:`);
684
+ this.logger.error(() => `${errorLabel} Has synthetic messages: ${hasSyntheticMessages}`);
685
+ this.logger.error(() => `${errorLabel} Has pending tool calls without responses: ${hasPendingToolCalls}`);
686
+ const lastMessages = cleanedMessages.slice(-5);
687
+ lastMessages.forEach((msg, idx) => {
688
+ 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)` : ''}`);
689
+ if (msg.tool_calls) {
690
+ msg.tool_calls.forEach((tc) => {
691
+ this.logger.error(() => ` - Tool call: ${tc.id} -> ${tc.function.name}`);
692
+ });
693
+ }
694
+ });
695
+ }
696
+ // Check for JSONResponse mutation errors
697
+ const errorMessage = error instanceof Error ? error.message : String(error);
698
+ if (errorMessage?.includes('JSONResponse') &&
699
+ errorMessage?.includes('does not support item assignment')) {
700
+ this.logger.debug(() => '[JSONResponse Error] Detected JSONResponse mutation error, retrying without streaming');
701
+ this.logger.error(() => '[Cerebras Corruption] JSONResponse mutation error detected. This typically occurs with certain providers like Cerebras. Falling back to non-streaming mode.', {
702
+ errorMessage,
703
+ provider: this.baseURL,
704
+ streamingEnabled,
705
+ });
706
+ // Retry with streaming disabled
707
+ response = await this.openai.chat.completions.create({
708
+ model: this.currentModel,
709
+ messages: cleanedMessages,
710
+ stream: false, // Force non-streaming
711
+ tools: formattedTools,
712
+ tool_choice: this.getToolChoiceForFormat(tools),
713
+ ...this.modelParams,
714
+ });
715
+ // Override streamingEnabled for the rest of this function
716
+ streamingEnabled = false;
717
+ }
718
+ else {
719
+ this.logger.debug(() => `${errorLabel} Re-throwing error (not a JSONResponse mutation)`);
720
+ // Re-throw other errors
721
+ throw error;
722
+ }
723
+ }
594
724
  let fullContent = '';
595
725
  const accumulatedToolCalls = [];
596
726
  let hasStreamedContent = false;
@@ -602,8 +732,129 @@ export class OpenAIProvider extends BaseProvider {
602
732
  // We need to buffer all chunks to detect and handle malformed streams
603
733
  // Some providers (like Cerebras) send message format instead of delta
604
734
  const allChunks = [];
605
- for await (const chunk of response) {
606
- allChunks.push(chunk);
735
+ this.logger.debug(() => '[Stream Detection] Starting to buffer chunks for corruption detection', {
736
+ provider: this.baseURL,
737
+ streamingEnabled,
738
+ isUsingQwen: this.isUsingQwen(),
739
+ currentModel: this.currentModel,
740
+ });
741
+ try {
742
+ for await (const chunk of response) {
743
+ // CRITICAL: Create a deep copy to avoid JSONResponse mutation issues
744
+ // Cerebras and other providers may return immutable JSONResponse objects
745
+ // Cast to unknown first to bypass type checking, then to our extended type
746
+ const extendedChunk = chunk;
747
+ const safeChunk = {
748
+ choices: extendedChunk.choices?.map((choice) => ({
749
+ delta: choice.delta
750
+ ? {
751
+ content: choice.delta.content ?? undefined,
752
+ role: choice.delta.role,
753
+ tool_calls: choice.delta.tool_calls?.map((tc, idx) => ({
754
+ id: tc.id,
755
+ type: tc.type,
756
+ function: tc.function
757
+ ? {
758
+ name: tc.function.name,
759
+ arguments: tc.function.arguments,
760
+ }
761
+ : undefined,
762
+ index: tc.index !== undefined ? tc.index : idx,
763
+ })),
764
+ }
765
+ : undefined,
766
+ message: choice.message
767
+ ? {
768
+ content: choice.message.content ?? undefined,
769
+ role: choice.message.role,
770
+ tool_calls: choice.message.tool_calls?.map((tc) => ({
771
+ id: tc.id,
772
+ type: tc.type,
773
+ function: tc.function
774
+ ? {
775
+ name: tc.function.name,
776
+ arguments: tc.function.arguments,
777
+ }
778
+ : undefined,
779
+ })),
780
+ }
781
+ : undefined,
782
+ index: choice.index,
783
+ finish_reason: choice.finish_reason,
784
+ })),
785
+ usage: extendedChunk.usage
786
+ ? {
787
+ prompt_tokens: extendedChunk.usage.prompt_tokens,
788
+ completion_tokens: extendedChunk.usage.completion_tokens,
789
+ total_tokens: extendedChunk.usage.total_tokens,
790
+ }
791
+ : undefined,
792
+ };
793
+ allChunks.push(safeChunk);
794
+ }
795
+ this.logger.debug(() => `[Stream Buffering Complete] Collected ${allChunks.length} chunks`, {
796
+ chunkCount: allChunks.length,
797
+ hasContent: allChunks.some((c) => c.choices?.[0]?.delta?.content),
798
+ hasToolCalls: allChunks.some((c) => c.choices?.[0]?.delta?.tool_calls),
799
+ });
800
+ }
801
+ catch (error) {
802
+ // Handle JSONResponse mutation errors that occur during iteration
803
+ const errorMessage = error instanceof Error ? error.message : String(error);
804
+ if (errorMessage?.includes('JSONResponse') &&
805
+ errorMessage?.includes('does not support item assignment')) {
806
+ 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.', {
807
+ error: errorMessage,
808
+ provider: this.baseURL,
809
+ chunksCollected: allChunks.length,
810
+ });
811
+ // Retry the entire request with streaming disabled
812
+ // This is the nuclear option but ensures we get a response
813
+ const nonStreamingResponse = await this.openai.chat.completions.create({
814
+ model: this.currentModel,
815
+ messages: cleanedMessages,
816
+ stream: false, // Force non-streaming
817
+ tools: formattedTools,
818
+ tool_choice: this.getToolChoiceForFormat(tools),
819
+ ...this.modelParams,
820
+ });
821
+ // Handle as non-streaming response
822
+ const completionResponse = nonStreamingResponse;
823
+ const choice = completionResponse.choices[0];
824
+ if (choice?.message.content) {
825
+ fullContent = choice.message.content;
826
+ }
827
+ if (choice?.message.tool_calls) {
828
+ for (const toolCall of choice.message.tool_calls) {
829
+ if (toolCall.type === 'function' && toolCall.function) {
830
+ accumulatedToolCalls.push({
831
+ id: toolCall.id,
832
+ type: 'function',
833
+ function: toolCall.function,
834
+ });
835
+ }
836
+ }
837
+ }
838
+ if (completionResponse.usage) {
839
+ usageData = {
840
+ prompt_tokens: completionResponse.usage.prompt_tokens,
841
+ completion_tokens: completionResponse.usage.completion_tokens,
842
+ total_tokens: completionResponse.usage.total_tokens,
843
+ };
844
+ }
845
+ // Yield the complete response
846
+ yield {
847
+ role: ContentGeneratorRole.ASSISTANT,
848
+ content: fullContent || '',
849
+ tool_calls: accumulatedToolCalls.length > 0
850
+ ? accumulatedToolCalls
851
+ : undefined,
852
+ usage: usageData,
853
+ };
854
+ return;
855
+ }
856
+ // Re-throw other errors
857
+ throw error;
607
858
  }
608
859
  // Check first chunk to see if we have malformed stream
609
860
  let detectedMalformedStream = false;
@@ -612,9 +863,7 @@ export class OpenAIProvider extends BaseProvider {
612
863
  if (firstChunk.choices?.[0]?.message &&
613
864
  !firstChunk.choices?.[0]?.delta) {
614
865
  detectedMalformedStream = true;
615
- if (process.env.DEBUG) {
616
- console.log('[OpenAIProvider] Detected malformed stream (message instead of delta), using aggregation mode');
617
- }
866
+ this.logger.debug(() => 'Detected malformed stream (message instead of delta), using aggregation mode');
618
867
  }
619
868
  }
620
869
  // If we detected issues, aggregate everything
@@ -629,10 +878,22 @@ export class OpenAIProvider extends BaseProvider {
629
878
  contentParts.push(message.content);
630
879
  }
631
880
  if (message?.tool_calls) {
632
- aggregatedToolCalls = message.tool_calls;
881
+ // Ensure tool_calls match the expected format
882
+ aggregatedToolCalls = message.tool_calls.map((tc) => ({
883
+ id: tc.id || `call_${Date.now()}`,
884
+ type: (tc.type || 'function'),
885
+ function: {
886
+ name: tc.function?.name || '',
887
+ arguments: tc.function?.arguments || '',
888
+ },
889
+ }));
633
890
  }
634
891
  if (chunk.usage) {
635
- finalUsageData = chunk.usage;
892
+ finalUsageData = {
893
+ prompt_tokens: chunk.usage.prompt_tokens || 0,
894
+ completion_tokens: chunk.usage.completion_tokens || 0,
895
+ total_tokens: chunk.usage.total_tokens || 0,
896
+ };
636
897
  }
637
898
  }
638
899
  // Yield single reconstructed message
@@ -645,64 +906,72 @@ export class OpenAIProvider extends BaseProvider {
645
906
  return;
646
907
  }
647
908
  // Process chunks normally - stream them as they come
909
+ this.logger.debug(() => `[Processing Chunks] Starting to process ${allChunks.length} buffered chunks`, {
910
+ isUsingQwen: this.isUsingQwen(),
911
+ });
912
+ let chunkIndex = 0;
648
913
  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
914
+ chunkIndex++;
915
+ // Since we created safe copies during buffering, chunks are now mutable
916
+ // Check if this chunk has message format instead of delta (malformed stream)
652
917
  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;
918
+ if (chunk.choices?.[0]?.message && !chunk.choices?.[0]?.delta) {
919
+ this.logger.error(() => '[Cerebras Corruption] Converting malformed chunk from message to delta format', {
920
+ provider: this.baseURL,
921
+ hasMessage: true,
922
+ hasDelta: false,
923
+ messageContent: chunk.choices[0].message?.content?.substring(0, 100),
924
+ });
925
+ // Convert message format to delta format for consistent processing
926
+ const message = chunk.choices[0].message;
661
927
  processedChunk = {
662
- ...originalChunk,
663
928
  choices: [
664
929
  {
665
930
  delta: {
666
- content: message.content,
667
- tool_calls: message.tool_calls?.map((tc, index) => ({
668
- ...tc,
669
- index,
670
- })),
931
+ content: message?.content ?? undefined,
932
+ role: message?.role,
933
+ tool_calls: message?.tool_calls,
671
934
  },
672
- message: undefined, // Remove message field
673
935
  },
674
936
  ],
675
- usage: originalChunk.usage,
937
+ usage: chunk.usage,
676
938
  };
677
939
  }
678
940
  const delta = processedChunk.choices?.[0]?.delta;
679
941
  if (delta?.content) {
680
942
  // Enhanced debug logging to understand streaming behavior
681
- if (process.env.DEBUG && this.isUsingQwen()) {
682
- console.log(`[OpenAIProvider/${this.currentModel}] Chunk:`, {
943
+ if (this.isUsingQwen()) {
944
+ this.logger.debug(() => `Chunk: ${JSON.stringify({
683
945
  content: delta.content,
684
- contentLength: delta.content.length,
685
- isWhitespaceOnly: delta.content.trim() === '',
946
+ contentLength: delta.content?.length ?? 0,
947
+ isWhitespaceOnly: delta.content?.trim() === '',
686
948
  chunkIndex: 0,
687
- });
949
+ })}`);
688
950
  }
689
951
  // For text-based models, don't yield content chunks yet
690
- if (!parser) {
952
+ if (!parser && delta.content) {
953
+ this.logger.debug(() => `[Content Processing] Chunk ${chunkIndex}/${allChunks.length} has content`, {
954
+ contentLength: delta.content.length,
955
+ contentPreview: delta.content.substring(0, 50),
956
+ isUsingQwen: this.isUsingQwen(),
957
+ willBuffer: this.isUsingQwen() && delta.content.trim() === '',
958
+ });
691
959
  if (this.isUsingQwen()) {
692
960
  const isWhitespaceOnly = delta.content.trim() === '';
693
961
  if (isWhitespaceOnly) {
694
962
  // Buffer whitespace-only chunk
695
963
  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
- }
964
+ this.logger.debug(() => `[Whitespace Buffering] Buffered whitespace-only chunk (len=${delta.content?.length ?? 0}). pendingWhitespace now len=${pendingWhitespace?.length ?? 0}`, {
965
+ chunkIndex,
966
+ totalChunks: allChunks.length,
967
+ isLastChunk: chunkIndex === allChunks.length,
968
+ contentHex: Buffer.from(delta.content).toString('hex'),
969
+ });
699
970
  continue;
700
971
  }
701
972
  else if (pendingWhitespace) {
702
973
  // 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
- }
974
+ this.logger.debug(() => `Flushing pending whitespace (len=${pendingWhitespace?.length ?? 0}) before non-empty chunk`);
706
975
  yield {
707
976
  role: ContentGeneratorRole.ASSISTANT,
708
977
  content: pendingWhitespace,
@@ -712,6 +981,10 @@ export class OpenAIProvider extends BaseProvider {
712
981
  pendingWhitespace = null;
713
982
  }
714
983
  }
984
+ this.logger.debug(() => `[Yielding Content] Yielding chunk ${chunkIndex}/${allChunks.length}`, {
985
+ contentLength: delta.content.length,
986
+ hasStreamedContent,
987
+ });
715
988
  yield {
716
989
  role: ContentGeneratorRole.ASSISTANT,
717
990
  content: delta.content,
@@ -728,9 +1001,9 @@ export class OpenAIProvider extends BaseProvider {
728
1001
  // Check for usage data in the chunk
729
1002
  if (processedChunk.usage) {
730
1003
  usageData = {
731
- prompt_tokens: processedChunk.usage.prompt_tokens,
732
- completion_tokens: processedChunk.usage.completion_tokens,
733
- total_tokens: processedChunk.usage.total_tokens,
1004
+ prompt_tokens: processedChunk.usage.prompt_tokens || 0,
1005
+ completion_tokens: processedChunk.usage.completion_tokens || 0,
1006
+ total_tokens: processedChunk.usage.total_tokens || 0,
734
1007
  };
735
1008
  }
736
1009
  }
@@ -746,6 +1019,7 @@ export class OpenAIProvider extends BaseProvider {
746
1019
  // Convert tool calls to the standard format
747
1020
  for (const toolCall of choice.message.tool_calls) {
748
1021
  if (toolCall.type === 'function' && toolCall.function) {
1022
+ // Don't fix double stringification here - it's handled later in the final processing
749
1023
  accumulatedToolCalls.push({
750
1024
  id: toolCall.id,
751
1025
  type: 'function',
@@ -772,9 +1046,7 @@ export class OpenAIProvider extends BaseProvider {
772
1046
  }
773
1047
  // Flush any remaining pending whitespace for Qwen
774
1048
  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
- }
1049
+ this.logger.debug(() => `Flushing trailing pending whitespace (len=${pendingWhitespace?.length ?? 0}) at stream end`);
778
1050
  yield {
779
1051
  role: ContentGeneratorRole.ASSISTANT,
780
1052
  content: pendingWhitespace,
@@ -815,35 +1087,161 @@ export class OpenAIProvider extends BaseProvider {
815
1087
  else {
816
1088
  // Standard OpenAI tool call handling
817
1089
  if (accumulatedToolCalls.length > 0) {
818
- if (process.env.DEBUG && this.isUsingQwen()) {
819
- console.log(`[OpenAIProvider/${this.currentModel}] Final message with tool calls:`, {
1090
+ // Fix double stringification for Qwen tool calls
1091
+ // Qwen models pre-stringify arguments values, but later in the process
1092
+ // they are being JSON.stringify'd again
1093
+ let fixedToolCalls = accumulatedToolCalls;
1094
+ if (this.isUsingQwen()) {
1095
+ this.logger.debug(() => `[Qwen Fix] Processing ${accumulatedToolCalls.length} tool calls for double-stringification fix`);
1096
+ fixedToolCalls = accumulatedToolCalls.map((toolCall, index) => {
1097
+ this.logger.debug(() => `[Qwen Fix] Tool call ${index}: ${JSON.stringify({
1098
+ name: toolCall.function.name,
1099
+ argumentsType: typeof toolCall.function.arguments,
1100
+ argumentsLength: toolCall.function.arguments?.length,
1101
+ argumentsSample: toolCall.function.arguments?.substring(0, 100),
1102
+ })}`);
1103
+ // For Qwen, check for nested double-stringification
1104
+ // Qwen models stringify array/object values WITHIN the JSON arguments
1105
+ if (toolCall.function.arguments &&
1106
+ typeof toolCall.function.arguments === 'string') {
1107
+ try {
1108
+ // First, parse the arguments to get the JSON object
1109
+ const parsedArgs = JSON.parse(toolCall.function.arguments);
1110
+ let hasNestedStringification = false;
1111
+ // Check each property to see if it's a stringified array/object/number
1112
+ const fixedArgs = {};
1113
+ for (const [key, value] of Object.entries(parsedArgs)) {
1114
+ if (typeof value === 'string') {
1115
+ const trimmed = value.trim();
1116
+ // Check if it's a stringified number (integer or float)
1117
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
1118
+ const numValue = trimmed.includes('.')
1119
+ ? parseFloat(trimmed)
1120
+ : parseInt(trimmed, 10);
1121
+ fixedArgs[key] = numValue;
1122
+ hasNestedStringification = true;
1123
+ this.logger.debug(() => `[Qwen Fix] Fixed stringified number in property '${key}' for ${toolCall.function.name}: "${value}" -> ${numValue}`);
1124
+ }
1125
+ // Check if it looks like a stringified array or object
1126
+ // Also check for Python-style dictionaries with single quotes
1127
+ else if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
1128
+ (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
1129
+ try {
1130
+ // Try to parse it as JSON
1131
+ const nestedParsed = JSON.parse(value);
1132
+ fixedArgs[key] = nestedParsed;
1133
+ hasNestedStringification = true;
1134
+ this.logger.debug(() => `[Qwen Fix] Fixed nested stringification in property '${key}' for ${toolCall.function.name}`);
1135
+ }
1136
+ catch {
1137
+ // Try to convert Python-style to JSON (single quotes to double quotes)
1138
+ try {
1139
+ const jsonified = value
1140
+ .replace(/'/g, '"')
1141
+ .replace(/: True/g, ': true')
1142
+ .replace(/: False/g, ': false')
1143
+ .replace(/: None/g, ': null');
1144
+ const nestedParsed = JSON.parse(jsonified);
1145
+ fixedArgs[key] = nestedParsed;
1146
+ hasNestedStringification = true;
1147
+ this.logger.debug(() => `[Qwen Fix] Fixed Python-style nested stringification in property '${key}' for ${toolCall.function.name}`);
1148
+ }
1149
+ catch {
1150
+ // Not valid JSON even after conversion, keep as string
1151
+ fixedArgs[key] = value;
1152
+ }
1153
+ }
1154
+ }
1155
+ else {
1156
+ fixedArgs[key] = value;
1157
+ }
1158
+ }
1159
+ else {
1160
+ fixedArgs[key] = value;
1161
+ }
1162
+ }
1163
+ if (hasNestedStringification) {
1164
+ this.logger.debug(() => `[Qwen Fix] Fixed nested double-stringification for ${toolCall.function.name}`);
1165
+ return {
1166
+ ...toolCall,
1167
+ function: {
1168
+ ...toolCall.function,
1169
+ arguments: JSON.stringify(fixedArgs),
1170
+ },
1171
+ };
1172
+ }
1173
+ }
1174
+ catch (_e) {
1175
+ // If parsing fails, check for old-style double-stringification
1176
+ if (toolCall.function.arguments.startsWith('"') &&
1177
+ toolCall.function.arguments.endsWith('"')) {
1178
+ try {
1179
+ // Old fix: entire arguments were double-stringified
1180
+ const parsedArgs = JSON.parse(toolCall.function.arguments);
1181
+ this.logger.debug(() => `[Qwen Fix] Fixed whole-argument double-stringification for ${toolCall.function.name}`);
1182
+ return {
1183
+ ...toolCall,
1184
+ function: {
1185
+ ...toolCall.function,
1186
+ arguments: JSON.stringify(parsedArgs),
1187
+ },
1188
+ };
1189
+ }
1190
+ catch {
1191
+ // Leave as-is if we can't parse
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+ // No fix needed
1197
+ this.logger.debug(() => `[Qwen Fix] No double-stringification detected for ${toolCall.function.name}, keeping original`);
1198
+ return toolCall;
1199
+ });
1200
+ }
1201
+ if (this.isUsingQwen()) {
1202
+ this.logger.debug(() => `Final message with tool calls: ${JSON.stringify({
820
1203
  contentLength: fullContent.length,
821
1204
  content: fullContent.substring(0, 200) +
822
1205
  (fullContent.length > 200 ? '...' : ''),
823
1206
  toolCallCount: accumulatedToolCalls.length,
824
1207
  hasStreamedContent,
825
- });
1208
+ })}`);
826
1209
  }
827
1210
  // For Qwen models, don't duplicate content if we've already streamed it
828
1211
  // BUT Cerebras needs at least a space to continue after tool responses
829
1212
  const isCerebras = this.baseURL?.toLowerCase().includes('cerebras.ai');
1213
+ if (isCerebras) {
1214
+ this.logger.debug(() => '[Cerebras] Special handling for Cerebras provider after tool responses', {
1215
+ hasStreamedContent,
1216
+ willSendSpace: hasStreamedContent,
1217
+ });
1218
+ }
830
1219
  const shouldOmitContent = hasStreamedContent && this.isUsingQwen() && !isCerebras;
1220
+ this.logger.debug(() => '[Tool Call Handling] Deciding how to yield tool calls', {
1221
+ hasStreamedContent,
1222
+ isUsingQwen: this.isUsingQwen(),
1223
+ isCerebras,
1224
+ shouldOmitContent,
1225
+ fullContentLength: fullContent.length,
1226
+ toolCallCount: fixedToolCalls?.length || 0,
1227
+ });
831
1228
  if (shouldOmitContent) {
832
- // Only yield tool calls with empty content to avoid duplication
1229
+ // Qwen: Send just a space (like Cerebras) to prevent stream stopping
833
1230
  yield {
834
1231
  role: ContentGeneratorRole.ASSISTANT,
835
- content: '',
836
- tool_calls: accumulatedToolCalls,
1232
+ content: ' ', // Single space instead of empty to keep stream alive
1233
+ tool_calls: fixedToolCalls,
837
1234
  usage: usageData,
838
1235
  };
839
1236
  }
840
1237
  else if (isCerebras && hasStreamedContent) {
841
1238
  // Cerebras: Send just a space to prevent duplication but allow continuation
842
1239
  // This prevents the repeated "Let me search..." text
1240
+ this.logger.debug(() => '[Cerebras] Sending minimal space content to prevent duplication');
843
1241
  yield {
844
1242
  role: ContentGeneratorRole.ASSISTANT,
845
1243
  content: ' ', // Single space instead of full content
846
- tool_calls: accumulatedToolCalls,
1244
+ tool_calls: fixedToolCalls,
847
1245
  usage: usageData,
848
1246
  };
849
1247
  }
@@ -852,7 +1250,7 @@ export class OpenAIProvider extends BaseProvider {
852
1250
  yield {
853
1251
  role: ContentGeneratorRole.ASSISTANT,
854
1252
  content: fullContent || '',
855
- tool_calls: accumulatedToolCalls,
1253
+ tool_calls: fixedToolCalls,
856
1254
  usage: usageData,
857
1255
  };
858
1256
  }
@@ -870,9 +1268,7 @@ export class OpenAIProvider extends BaseProvider {
870
1268
  setModel(modelId) {
871
1269
  // Update SettingsService as the source of truth
872
1270
  this.setModelInSettings(modelId).catch((error) => {
873
- if (process.env.DEBUG) {
874
- console.warn('Failed to persist model to SettingsService:', error);
875
- }
1271
+ this.logger.debug(() => `Failed to persist model to SettingsService: ${error}`);
876
1272
  });
877
1273
  // Keep local cache for performance
878
1274
  this.currentModel = modelId;
@@ -887,9 +1283,7 @@ export class OpenAIProvider extends BaseProvider {
887
1283
  }
888
1284
  }
889
1285
  catch (error) {
890
- if (process.env.DEBUG) {
891
- console.warn('Failed to get model from SettingsService:', error);
892
- }
1286
+ this.logger.debug(() => `Failed to get model from SettingsService: ${error}`);
893
1287
  }
894
1288
  // Fall back to cached value or default
895
1289
  return this.currentModel || this.getDefaultModel();
@@ -900,16 +1294,14 @@ export class OpenAIProvider extends BaseProvider {
900
1294
  if (this.isUsingQwen()) {
901
1295
  return 'qwen3-coder-plus';
902
1296
  }
903
- return process.env.LLXPRT_DEFAULT_MODEL || 'llama3-70b-8192';
1297
+ return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
904
1298
  }
905
1299
  setApiKey(apiKey) {
906
1300
  // Call base provider implementation
907
1301
  super.setApiKey?.(apiKey);
908
1302
  // Persist to SettingsService if available
909
1303
  this.setApiKeyInSettings(apiKey).catch((error) => {
910
- if (process.env.DEBUG) {
911
- console.warn('Failed to persist API key to SettingsService:', error);
912
- }
1304
+ this.logger.debug(() => `Failed to persist API key to SettingsService: ${error}`);
913
1305
  });
914
1306
  // Create a new OpenAI client with the updated API key
915
1307
  const clientOptions = {
@@ -928,9 +1320,7 @@ export class OpenAIProvider extends BaseProvider {
928
1320
  this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
929
1321
  // Persist to SettingsService if available
930
1322
  this.setBaseUrlInSettings(this.baseURL).catch((error) => {
931
- if (process.env.DEBUG) {
932
- console.warn('Failed to persist base URL to SettingsService:', error);
933
- }
1323
+ this.logger.debug(() => `Failed to persist base URL to SettingsService: ${error}`);
934
1324
  });
935
1325
  // Update OAuth configuration based on endpoint validation
936
1326
  // Enable OAuth for Qwen endpoints if we have an OAuth manager
@@ -1013,9 +1403,7 @@ export class OpenAIProvider extends BaseProvider {
1013
1403
  }
1014
1404
  // Persist to SettingsService if available
1015
1405
  this.setModelParamsInSettings(this.modelParams).catch((error) => {
1016
- if (process.env.DEBUG) {
1017
- console.warn('Failed to persist model params to SettingsService:', error);
1018
- }
1406
+ this.logger.debug(() => `Failed to persist model params to SettingsService: ${error}`);
1019
1407
  });
1020
1408
  }
1021
1409
  /**
@@ -1045,14 +1433,10 @@ export class OpenAIProvider extends BaseProvider {
1045
1433
  if (savedParams) {
1046
1434
  this.modelParams = savedParams;
1047
1435
  }
1048
- if (process.env.DEBUG) {
1049
- console.log(`[OpenAI] Initialized from SettingsService - model: ${this.currentModel}, baseURL: ${this.baseURL}, params:`, this.modelParams);
1050
- }
1436
+ this.logger.debug(() => `Initialized from SettingsService - model: ${this.currentModel}, baseURL: ${this.baseURL}, params: ${JSON.stringify(this.modelParams)}`);
1051
1437
  }
1052
1438
  catch (error) {
1053
- if (process.env.DEBUG) {
1054
- console.error('Failed to initialize OpenAI provider from SettingsService:', error);
1055
- }
1439
+ this.logger.debug(() => `Failed to initialize OpenAI provider from SettingsService: ${error}`);
1056
1440
  }
1057
1441
  }
1058
1442
  /**
@@ -1092,9 +1476,7 @@ export class OpenAIProvider extends BaseProvider {
1092
1476
  return 'openai';
1093
1477
  }
1094
1478
  catch (error) {
1095
- if (process.env.DEBUG) {
1096
- console.warn('Failed to detect tool format from SettingsService:', error);
1097
- }
1479
+ this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
1098
1480
  // Fallback detection without SettingsService
1099
1481
  const modelName = this.currentModel.toLowerCase();
1100
1482
  if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {