@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.
- package/dist/src/config/config.d.ts +14 -1
- package/dist/src/config/config.js +23 -3
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/core/client.js +2 -2
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +2 -1
- package/dist/src/core/coreToolScheduler.js +19 -0
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.js +28 -13
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/loggingContentGenerator.d.ts +1 -5
- package/dist/src/core/loggingContentGenerator.js +4 -6
- package/dist/src/core/loggingContentGenerator.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.d.ts +2 -2
- package/dist/src/core/nonInteractiveToolExecutor.js +13 -9
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/subagent.js +5 -5
- package/dist/src/core/subagent.js.map +1 -1
- package/dist/src/debug/ConfigurationManager.d.ts +28 -0
- package/dist/src/debug/ConfigurationManager.js +211 -0
- package/dist/src/debug/ConfigurationManager.js.map +1 -0
- package/dist/src/debug/DebugLogger.d.ts +26 -0
- package/dist/src/debug/DebugLogger.js +230 -0
- package/dist/src/debug/DebugLogger.js.map +1 -0
- package/dist/src/debug/FileOutput.d.ts +23 -0
- package/dist/src/debug/FileOutput.js +144 -0
- package/dist/src/debug/FileOutput.js.map +1 -0
- package/dist/src/debug/MockConfigurationManager.d.ts +28 -0
- package/dist/src/debug/MockConfigurationManager.js +45 -0
- package/dist/src/debug/MockConfigurationManager.js.map +1 -0
- package/dist/src/debug/MockFileOutput.d.ts +20 -0
- package/dist/src/debug/MockFileOutput.js +25 -0
- package/dist/src/debug/MockFileOutput.js.map +1 -0
- package/dist/src/debug/index.d.ts +8 -0
- package/dist/src/debug/index.js +9 -0
- package/dist/src/debug/index.js.map +1 -0
- package/dist/src/debug/types.d.ts +23 -0
- package/dist/src/debug/types.js +2 -0
- package/dist/src/debug/types.js.map +1 -0
- package/dist/src/ide/ide-client.d.ts +4 -0
- package/dist/src/ide/ide-client.js +41 -2
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/ide/ide-installer.js +17 -4
- package/dist/src/ide/ide-installer.js.map +1 -1
- package/dist/src/ide/process-utils.d.ts +10 -5
- package/dist/src/ide/process-utils.js +113 -30
- package/dist/src/ide/process-utils.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/prompt-config/defaults/core.md +34 -230
- package/dist/src/prompt-config/defaults/tools/read-file.md +1 -1
- package/dist/src/providers/BaseProvider.js +13 -2
- package/dist/src/providers/BaseProvider.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.d.ts +1 -0
- package/dist/src/providers/anthropic/AnthropicProvider.js +20 -31
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/gemini/GeminiProvider.d.ts +1 -0
- package/dist/src/providers/gemini/GeminiProvider.js +23 -50
- package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
- package/dist/src/providers/openai/OpenAIProvider.d.ts +9 -0
- package/dist/src/providers/openai/OpenAIProvider.js +502 -129
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/providers/openai/buildResponsesRequest.js +9 -6
- package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
- package/dist/src/providers/openai/getOpenAIProviderInfo.js +8 -3
- package/dist/src/providers/openai/getOpenAIProviderInfo.js.map +1 -1
- package/dist/src/providers/openai/parseResponsesStream.js +13 -4
- package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
- package/dist/src/providers/openai/syntheticToolResponses.d.ts +0 -9
- package/dist/src/providers/openai/syntheticToolResponses.js +87 -16
- package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
- package/dist/src/services/fileSystemService.d.ts +31 -0
- package/dist/src/services/fileSystemService.js +18 -0
- package/dist/src/services/fileSystemService.js.map +1 -0
- package/dist/src/services/loopDetectionService.d.ts +5 -0
- package/dist/src/services/loopDetectionService.js +89 -1
- package/dist/src/services/loopDetectionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.d.ts +8 -6
- package/dist/src/services/shellExecutionService.js +297 -133
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/telemetry/loggers.js +1 -1
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +1 -1
- package/dist/src/telemetry/metrics.js +2 -1
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/sdk.js +3 -3
- package/dist/src/telemetry/sdk.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +1 -0
- package/dist/src/telemetry/types.js +20 -0
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.d.ts +4 -0
- package/dist/src/telemetry/uiTelemetry.js +16 -1
- package/dist/src/telemetry/uiTelemetry.js.map +1 -1
- package/dist/src/test-utils/tools.d.ts +8 -2
- package/dist/src/test-utils/tools.js +2 -3
- package/dist/src/test-utils/tools.js.map +1 -1
- package/dist/src/tools/ToolFormatter.d.ts +1 -0
- package/dist/src/tools/ToolFormatter.js +90 -9
- package/dist/src/tools/ToolFormatter.js.map +1 -1
- package/dist/src/tools/edit.d.ts +1 -1
- package/dist/src/tools/edit.js +15 -9
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/glob.d.ts +1 -1
- package/dist/src/tools/glob.js +8 -8
- package/dist/src/tools/glob.js.map +1 -1
- package/dist/src/tools/grep.d.ts +1 -1
- package/dist/src/tools/grep.js +1 -6
- package/dist/src/tools/grep.js.map +1 -1
- package/dist/src/tools/ls.d.ts +1 -1
- package/dist/src/tools/ls.js +1 -6
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/mcp-client-manager.d.ts +38 -0
- package/dist/src/tools/mcp-client-manager.js +74 -0
- package/dist/src/tools/mcp-client-manager.js.map +1 -0
- package/dist/src/tools/mcp-client.d.ts +43 -0
- package/dist/src/tools/mcp-client.js +117 -1
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/memoryTool.d.ts +1 -1
- package/dist/src/tools/memoryTool.js +1 -6
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/read-file.d.ts +1 -1
- package/dist/src/tools/read-file.js +5 -7
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/read-many-files.d.ts +0 -1
- package/dist/src/tools/read-many-files.js +16 -11
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/shell.d.ts +1 -1
- package/dist/src/tools/shell.js +32 -28
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +5 -0
- package/dist/src/tools/tool-registry.js +13 -4
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tools.d.ts +3 -1
- package/dist/src/tools/tools.js +12 -0
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/web-fetch.d.ts +1 -1
- package/dist/src/tools/web-fetch.js +1 -6
- package/dist/src/tools/web-fetch.js.map +1 -1
- package/dist/src/tools/web-search.d.ts +6 -1
- package/dist/src/tools/web-search.js +6 -6
- package/dist/src/tools/web-search.js.map +1 -1
- package/dist/src/tools/write-file.d.ts +1 -1
- package/dist/src/tools/write-file.js +7 -8
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/utils/environmentContext.js +2 -2
- package/dist/src/utils/environmentContext.js.map +1 -1
- package/dist/src/utils/errorReporting.js +1 -1
- package/dist/src/utils/fileUtils.d.ts +2 -1
- package/dist/src/utils/fileUtils.js +3 -3
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/getPty.d.ts +19 -0
- package/dist/src/utils/getPty.js +23 -0
- package/dist/src/utils/getPty.js.map +1 -0
- package/dist/src/utils/user_account.js +58 -53
- package/dist/src/utils/user_account.js.map +1 -1
- 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 || '
|
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
|
-
//
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
384
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
594
|
+
let streamingEnabled = streamingSetting !== 'disabled';
|
577
595
|
// Get resolved authentication and update client if needed
|
578
596
|
await this.updateClientWithResolvedAuth();
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
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
|
-
|
606
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
650
|
-
//
|
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 (
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
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
|
667
|
-
|
668
|
-
|
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:
|
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 (
|
682
|
-
|
883
|
+
if (this.isUsingQwen()) {
|
884
|
+
this.logger.debug(() => `Chunk: ${JSON.stringify({
|
683
885
|
content: delta.content,
|
684
|
-
contentLength: delta.content
|
685
|
-
isWhitespaceOnly: delta.content
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
819
|
-
|
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:
|
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:
|
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:
|
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
|
-
|
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
|
-
|
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 || '
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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')) {
|