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