@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.
- package/README.md +19 -0
- package/dist/src/code_assist/oauth2.js +60 -31
- package/dist/src/code_assist/oauth2.js.map +1 -1
- 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 +6 -0
- package/dist/src/providers/gemini/GeminiProvider.js +31 -50
- package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
- package/dist/src/providers/openai/OpenAIProvider.d.ts +1 -0
- package/dist/src/providers/openai/OpenAIProvider.js +513 -131
- 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/ClipboardService.d.ts +19 -0
- package/dist/src/services/ClipboardService.js +66 -0
- package/dist/src/services/ClipboardService.js.map +1 -0
- 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/index.d.ts +1 -0
- package/dist/src/services/index.js +1 -0
- package/dist/src/services/index.js.map +1 -1
- 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 +7 -2
- package/dist/src/tools/read-file.js +22 -13
- 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 +25 -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 +7 -2
- package/dist/src/tools/write-file.js +49 -31
- 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/schemaValidator.js +16 -1
- package/dist/src/utils/schemaValidator.js.map +1 -1
- 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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
624
|
+
let streamingEnabled = streamingSetting !== 'disabled';
|
|
577
625
|
// Get resolved authentication and update client if needed
|
|
578
626
|
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,
|
|
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
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
650
|
-
//
|
|
651
|
-
//
|
|
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 (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
|
667
|
-
|
|
668
|
-
|
|
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:
|
|
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 (
|
|
682
|
-
|
|
943
|
+
if (this.isUsingQwen()) {
|
|
944
|
+
this.logger.debug(() => `Chunk: ${JSON.stringify({
|
|
683
945
|
content: delta.content,
|
|
684
|
-
contentLength: delta.content
|
|
685
|
-
isWhitespaceOnly: delta.content
|
|
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
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
819
|
-
|
|
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
|
-
//
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 || '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')) {
|