@vybestack/llxprt-code-core 0.1.23-nightly.250905.97906524 → 0.2.2-nightly.250908.7b895396
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/adapters/IStreamAdapter.d.ts +3 -3
- package/dist/src/auth/precedence.d.ts +1 -1
- package/dist/src/auth/precedence.js +9 -4
- package/dist/src/auth/precedence.js.map +1 -1
- package/dist/src/auth/types.d.ts +4 -4
- package/dist/src/code_assist/codeAssist.js +8 -6
- package/dist/src/code_assist/codeAssist.js.map +1 -1
- package/dist/src/code_assist/setup.js +9 -7
- package/dist/src/code_assist/setup.js.map +1 -1
- package/dist/src/config/index.d.ts +7 -0
- package/dist/src/config/index.js +8 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/core/client.d.ts +9 -21
- package/dist/src/core/client.js +55 -156
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/compression-config.d.ts +1 -1
- package/dist/src/core/compression-config.js +4 -5
- package/dist/src/core/compression-config.js.map +1 -1
- package/dist/src/core/coreToolScheduler.js +50 -15
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/geminiChat.d.ts +51 -2
- package/dist/src/core/geminiChat.js +616 -106
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.js +70 -19
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/prompts.js +34 -26
- package/dist/src/core/prompts.js.map +1 -1
- package/dist/src/core/turn.d.ts +1 -0
- package/dist/src/core/turn.js +8 -6
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/prompt-config/TemplateEngine.js +17 -0
- package/dist/src/prompt-config/TemplateEngine.js.map +1 -1
- package/dist/src/prompt-config/defaults/core-defaults.js +39 -32
- package/dist/src/prompt-config/defaults/core-defaults.js.map +1 -1
- package/dist/src/prompt-config/defaults/core.md +2 -0
- package/dist/src/prompt-config/defaults/provider-defaults.js +34 -27
- package/dist/src/prompt-config/defaults/provider-defaults.js.map +1 -1
- package/dist/src/prompt-config/defaults/providers/gemini/core.md +229 -43
- package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/core.md +12 -0
- package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/gemini-2-5-flash/core.md +12 -0
- package/dist/src/prompt-config/types.d.ts +2 -0
- package/dist/src/providers/BaseProvider.d.ts +32 -6
- package/dist/src/providers/BaseProvider.js +79 -22
- package/dist/src/providers/BaseProvider.js.map +1 -1
- package/dist/src/providers/IProvider.d.ts +9 -3
- package/dist/src/providers/LoggingProviderWrapper.d.ts +10 -3
- package/dist/src/providers/LoggingProviderWrapper.js +33 -27
- package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
- package/dist/src/providers/ProviderContentGenerator.d.ts +2 -2
- package/dist/src/providers/ProviderContentGenerator.js +9 -6
- package/dist/src/providers/ProviderContentGenerator.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.d.ts +27 -21
- package/dist/src/providers/anthropic/AnthropicProvider.js +473 -472
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/gemini/GeminiProvider.d.ts +14 -9
- package/dist/src/providers/gemini/GeminiProvider.js +202 -486
- package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
- package/dist/src/providers/openai/ConversationCache.d.ts +3 -3
- package/dist/src/providers/openai/IChatGenerateParams.d.ts +9 -4
- package/dist/src/providers/openai/OpenAIProvider.d.ts +44 -115
- package/dist/src/providers/openai/OpenAIProvider.js +582 -946
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/providers/openai/buildResponsesRequest.d.ts +3 -3
- package/dist/src/providers/openai/buildResponsesRequest.js +67 -37
- package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
- package/dist/src/providers/openai/estimateRemoteTokens.d.ts +2 -2
- package/dist/src/providers/openai/estimateRemoteTokens.js +21 -8
- package/dist/src/providers/openai/estimateRemoteTokens.js.map +1 -1
- package/dist/src/providers/openai/parseResponsesStream.d.ts +6 -2
- package/dist/src/providers/openai/parseResponsesStream.js +99 -391
- package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
- package/dist/src/providers/openai/syntheticToolResponses.d.ts +5 -5
- package/dist/src/providers/openai/syntheticToolResponses.js +102 -91
- package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +18 -20
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +250 -239
- package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
- package/dist/src/providers/tokenizers/OpenAITokenizer.js +3 -3
- package/dist/src/providers/tokenizers/OpenAITokenizer.js.map +1 -1
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/services/history/ContentConverters.d.ts +6 -1
- package/dist/src/services/history/ContentConverters.js +155 -18
- package/dist/src/services/history/ContentConverters.js.map +1 -1
- package/dist/src/services/history/HistoryService.d.ts +52 -0
- package/dist/src/services/history/HistoryService.js +245 -93
- package/dist/src/services/history/HistoryService.js.map +1 -1
- package/dist/src/services/history/IContent.d.ts +4 -0
- package/dist/src/services/history/IContent.js.map +1 -1
- package/dist/src/settings/SettingsService.js.map +1 -1
- package/dist/src/telemetry/types.d.ts +16 -4
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/tools/IToolFormatter.d.ts +2 -2
- package/dist/src/tools/ToolFormatter.d.ts +42 -4
- package/dist/src/tools/ToolFormatter.js +151 -64
- package/dist/src/tools/ToolFormatter.js.map +1 -1
- package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
- package/dist/src/tools/doubleEscapeUtils.js +241 -0
- package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
- package/dist/src/tools/read-file.js +5 -2
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/todo-schemas.d.ts +4 -4
- package/dist/src/tools/tool-registry.d.ts +8 -1
- package/dist/src/tools/tool-registry.js +79 -23
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/write-file.js +5 -2
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/types/modelParams.d.ts +12 -0
- package/dist/src/utils/bfsFileSearch.js +2 -6
- package/dist/src/utils/bfsFileSearch.js.map +1 -1
- package/package.json +8 -7
- package/dist/src/core/ContentGeneratorAdapter.d.ts +0 -37
- package/dist/src/core/ContentGeneratorAdapter.js +0 -58
- package/dist/src/core/ContentGeneratorAdapter.js.map +0 -1
- package/dist/src/providers/IMessage.d.ts +0 -38
- package/dist/src/providers/IMessage.js +0 -17
- package/dist/src/providers/IMessage.js.map +0 -1
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
- package/dist/src/providers/adapters/GeminiCompatibleWrapper.js.map +0 -1
@@ -17,210 +17,158 @@
|
|
17
17
|
* @plan PLAN-20250120-DEBUGLOGGING.P15
|
18
18
|
* @requirement REQ-INT-001.1
|
19
19
|
*/
|
20
|
-
import { DebugLogger } from '../../debug/index.js';
|
21
|
-
import { ContentGeneratorRole } from '../ContentGeneratorRole.js';
|
22
|
-
import { GemmaToolCallParser } from '../../parsers/TextToolCallParser.js';
|
23
|
-
import { ToolFormatter } from '../../tools/ToolFormatter.js';
|
24
20
|
import OpenAI from 'openai';
|
21
|
+
import * as http from 'http';
|
22
|
+
import * as https from 'https';
|
23
|
+
import * as net from 'net';
|
25
24
|
import { BaseProvider } from '../BaseProvider.js';
|
26
|
-
import {
|
27
|
-
import {
|
25
|
+
import { DebugLogger } from '../../debug/index.js';
|
26
|
+
import { ToolFormatter } from '../../tools/ToolFormatter.js';
|
27
|
+
import { processToolParameters } from '../../tools/doubleEscapeUtils.js';
|
28
|
+
import { getCoreSystemPromptAsync } from '../../core/prompts.js';
|
29
|
+
import { retryWithBackoff } from '../../utils/retry.js';
|
28
30
|
export class OpenAIProvider extends BaseProvider {
|
31
|
+
name = 'openai';
|
29
32
|
logger;
|
30
|
-
openai;
|
31
|
-
currentModel = process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
|
32
|
-
baseURL;
|
33
|
-
providerConfig;
|
34
33
|
toolFormatter;
|
35
|
-
toolFormatOverride;
|
36
|
-
modelParams;
|
37
34
|
_cachedClient;
|
38
35
|
_cachedClientKey;
|
39
36
|
constructor(apiKey, baseURL, config, oauthManager) {
|
37
|
+
// Normalize empty string to undefined for proper precedence handling
|
38
|
+
const normalizedApiKey = apiKey && apiKey.trim() !== '' ? apiKey : undefined;
|
39
|
+
// Detect if this is a Qwen endpoint
|
40
|
+
const isQwenEndpoint = !!(baseURL &&
|
41
|
+
(baseURL.includes('dashscope.aliyuncs.com') ||
|
42
|
+
baseURL.includes('api.qwen.com') ||
|
43
|
+
baseURL.includes('qwen')));
|
40
44
|
// Initialize base provider with auth configuration
|
41
|
-
|
42
|
-
// Check OAuth enablement from OAuth manager if available
|
43
|
-
let shouldEnableQwenOAuth = false;
|
44
|
-
if (oauthManager) {
|
45
|
-
// Check if OAuth is enabled for qwen in the OAuth manager (from settings)
|
46
|
-
const manager = oauthManager;
|
47
|
-
if (manager.isOAuthEnabled &&
|
48
|
-
typeof manager.isOAuthEnabled === 'function') {
|
49
|
-
shouldEnableQwenOAuth = manager.isOAuthEnabled('qwen');
|
50
|
-
}
|
51
|
-
// Also enable if this looks like a Qwen endpoint
|
52
|
-
if (!shouldEnableQwenOAuth) {
|
53
|
-
shouldEnableQwenOAuth =
|
54
|
-
isQwenEndpoint(baseURL || '') ||
|
55
|
-
(!baseURL && (!apiKey || apiKey === '')) ||
|
56
|
-
baseURL === 'https://portal.qwen.ai/v1';
|
57
|
-
}
|
58
|
-
}
|
59
|
-
const baseConfig = {
|
45
|
+
super({
|
60
46
|
name: 'openai',
|
61
|
-
apiKey,
|
47
|
+
apiKey: normalizedApiKey,
|
62
48
|
baseURL,
|
63
|
-
envKeyNames: ['OPENAI_API_KEY'],
|
64
|
-
isOAuthEnabled:
|
65
|
-
oauthProvider:
|
49
|
+
envKeyNames: ['OPENAI_API_KEY'], // Support environment variable fallback
|
50
|
+
isOAuthEnabled: isQwenEndpoint && !!oauthManager,
|
51
|
+
oauthProvider: isQwenEndpoint ? 'qwen' : undefined,
|
66
52
|
oauthManager,
|
67
|
-
};
|
68
|
-
super(baseConfig);
|
69
|
-
this.logger = new DebugLogger('llxprt:providers:openai');
|
70
|
-
this.logger.debug(() => `Constructor - baseURL: ${baseURL}, apiKey: ${apiKey?.substring(0, 10) || 'none'}, oauthManager: ${!!oauthManager}, shouldEnableQwenOAuth: ${shouldEnableQwenOAuth}`);
|
71
|
-
this.baseURL = baseURL;
|
72
|
-
this.providerConfig = config;
|
53
|
+
}, config);
|
73
54
|
this.toolFormatter = new ToolFormatter();
|
74
|
-
//
|
75
|
-
|
76
|
-
|
77
|
-
});
|
78
|
-
// Set appropriate default model based on the provider
|
79
|
-
if (shouldEnableQwenOAuth || isQwenEndpoint(baseURL || '')) {
|
80
|
-
// Default to Qwen model when using Qwen endpoints
|
81
|
-
this.currentModel = 'qwen3-coder-plus';
|
82
|
-
}
|
83
|
-
else if (process.env.LLXPRT_DEFAULT_MODEL) {
|
84
|
-
// Use environment variable if set
|
85
|
-
this.currentModel = process.env.LLXPRT_DEFAULT_MODEL;
|
86
|
-
}
|
87
|
-
const clientOptions = {
|
88
|
-
apiKey: apiKey || 'placeholder', // OpenAI client requires a string, use placeholder if OAuth will be used
|
89
|
-
// Allow browser environment if explicitly configured
|
90
|
-
dangerouslyAllowBrowser: config?.allowBrowserEnvironment || false,
|
91
|
-
};
|
92
|
-
// Only include baseURL if it's defined
|
93
|
-
if (baseURL) {
|
94
|
-
clientOptions.baseURL = baseURL;
|
95
|
-
}
|
96
|
-
this.openai = new OpenAI(clientOptions);
|
97
|
-
this._cachedClientKey = apiKey; // Track the initial key used
|
98
|
-
// Cached client reserved for future optimization
|
99
|
-
void this._cachedClient;
|
100
|
-
}
|
101
|
-
/**
|
102
|
-
* Implementation of BaseProvider abstract method
|
103
|
-
* Determines if this provider supports OAuth authentication
|
104
|
-
*/
|
105
|
-
supportsOAuth() {
|
106
|
-
// Only support Qwen OAuth for Qwen endpoints
|
107
|
-
// Use baseProviderConfig.baseURL if this.baseURL not set yet (during constructor)
|
108
|
-
const baseURL = this.baseURL ||
|
109
|
-
this.baseProviderConfig.baseURL ||
|
110
|
-
'https://api.openai.com/v1';
|
111
|
-
return isQwenEndpoint(baseURL);
|
55
|
+
// new DebugLogger('llxprt:core:toolformatter'), // TODO: Fix ToolFormatter constructor
|
56
|
+
// Setup debug logger
|
57
|
+
this.logger = new DebugLogger('llxprt:provider:openai');
|
112
58
|
}
|
113
59
|
/**
|
114
|
-
*
|
60
|
+
* Create HTTP/HTTPS agents with socket configuration for local AI servers
|
61
|
+
* Returns undefined if no socket settings are configured
|
115
62
|
*/
|
116
|
-
|
117
|
-
//
|
118
|
-
const
|
119
|
-
|
63
|
+
createHttpAgents() {
|
64
|
+
// Get socket configuration from ephemeral settings
|
65
|
+
const settings = this.providerConfig?.getEphemeralSettings?.() || {};
|
66
|
+
// Check if any socket settings are explicitly configured
|
67
|
+
const hasSocketSettings = 'socket-timeout' in settings ||
|
68
|
+
'socket-keepalive' in settings ||
|
69
|
+
'socket-nodelay' in settings;
|
70
|
+
// Only create custom agents if socket settings are configured
|
71
|
+
if (!hasSocketSettings) {
|
72
|
+
return undefined;
|
73
|
+
}
|
74
|
+
// Socket configuration with defaults for when settings ARE configured
|
75
|
+
const socketTimeout = settings['socket-timeout'] || 60000; // 60 seconds default
|
76
|
+
const socketKeepAlive = settings['socket-keepalive'] !== false; // true by default
|
77
|
+
const socketNoDelay = settings['socket-nodelay'] !== false; // true by default
|
78
|
+
// Create HTTP agent with socket options
|
79
|
+
const httpAgent = new http.Agent({
|
80
|
+
keepAlive: socketKeepAlive,
|
81
|
+
keepAliveMsecs: 1000,
|
82
|
+
timeout: socketTimeout,
|
83
|
+
});
|
84
|
+
// Create HTTPS agent with socket options
|
85
|
+
const httpsAgent = new https.Agent({
|
86
|
+
keepAlive: socketKeepAlive,
|
87
|
+
keepAliveMsecs: 1000,
|
88
|
+
timeout: socketTimeout,
|
89
|
+
});
|
90
|
+
// Apply TCP_NODELAY if enabled (reduces latency for local servers)
|
91
|
+
if (socketNoDelay) {
|
92
|
+
const originalCreateConnection = httpAgent.createConnection;
|
93
|
+
httpAgent.createConnection = function (options, callback) {
|
94
|
+
const socket = originalCreateConnection.call(this, options, callback);
|
95
|
+
if (socket instanceof net.Socket) {
|
96
|
+
socket.setNoDelay(true);
|
97
|
+
}
|
98
|
+
return socket;
|
99
|
+
};
|
100
|
+
const originalHttpsCreateConnection = httpsAgent.createConnection;
|
101
|
+
httpsAgent.createConnection = function (options, callback) {
|
102
|
+
const socket = originalHttpsCreateConnection.call(this, options, callback);
|
103
|
+
if (socket instanceof net.Socket) {
|
104
|
+
socket.setNoDelay(true);
|
105
|
+
}
|
106
|
+
return socket;
|
107
|
+
};
|
108
|
+
}
|
109
|
+
return { httpAgent, httpsAgent };
|
120
110
|
}
|
121
111
|
/**
|
122
|
-
*
|
112
|
+
* Get or create OpenAI client instance
|
113
|
+
* Will use the API key from resolved auth
|
114
|
+
* @returns OpenAI client instance
|
123
115
|
*/
|
124
|
-
async
|
116
|
+
async getClient() {
|
125
117
|
const resolvedKey = await this.getAuthToken();
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
// Use the resource_url from the OAuth token
|
148
|
-
effectiveBaseURL = `https://${oauthToken.resource_url}/v1`;
|
149
|
-
this.logger.debug(() => `Using Qwen OAuth endpoint: ${effectiveBaseURL}`);
|
150
|
-
}
|
151
|
-
}
|
152
|
-
}
|
153
|
-
// Only update client if the key or URL has changed
|
154
|
-
if (this._cachedClientKey !== resolvedKey ||
|
155
|
-
this.baseURL !== effectiveBaseURL) {
|
156
|
-
const clientOptions = {
|
157
|
-
apiKey: resolvedKey,
|
158
|
-
// Allow browser environment if explicitly configured
|
159
|
-
dangerouslyAllowBrowser: this.providerConfig?.allowBrowserEnvironment || false,
|
160
|
-
};
|
161
|
-
// Only include baseURL if it's defined
|
162
|
-
if (effectiveBaseURL) {
|
163
|
-
clientOptions.baseURL = effectiveBaseURL;
|
164
|
-
}
|
165
|
-
this.openai = new OpenAI(clientOptions);
|
166
|
-
this._cachedClientKey = resolvedKey;
|
167
|
-
// Update the baseURL to track changes
|
168
|
-
if (effectiveBaseURL !== this.baseURL) {
|
169
|
-
this.baseURL = effectiveBaseURL;
|
118
|
+
// Use the unified getBaseURL() method from BaseProvider
|
119
|
+
const baseURL = this.getBaseURL();
|
120
|
+
const clientKey = `${baseURL}-${resolvedKey}`;
|
121
|
+
// Return cached client if available and auth hasn't changed
|
122
|
+
if (this._cachedClient && this._cachedClientKey === clientKey) {
|
123
|
+
return this._cachedClient;
|
124
|
+
}
|
125
|
+
// Create HTTP agents with socket configuration (if configured)
|
126
|
+
const agents = this.createHttpAgents();
|
127
|
+
// Build client options - OpenAI SDK accepts httpAgent/httpsAgent at runtime
|
128
|
+
// even though they're not in the TypeScript definitions
|
129
|
+
const baseOptions = {
|
130
|
+
apiKey: resolvedKey || '',
|
131
|
+
baseURL,
|
132
|
+
};
|
133
|
+
// Add socket configuration if available
|
134
|
+
const clientOptions = agents
|
135
|
+
? {
|
136
|
+
...baseOptions,
|
137
|
+
httpAgent: agents.httpAgent,
|
138
|
+
httpsAgent: agents.httpsAgent,
|
170
139
|
}
|
171
|
-
|
140
|
+
: baseOptions;
|
141
|
+
// Create new client with current auth and optional socket configuration
|
142
|
+
// Cast to unknown then to the expected type to bypass TypeScript's structural checking
|
143
|
+
this._cachedClient = new OpenAI(clientOptions);
|
144
|
+
this._cachedClientKey = clientKey;
|
145
|
+
return this._cachedClient;
|
172
146
|
}
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
const
|
179
|
-
|
180
|
-
if (
|
147
|
+
/**
|
148
|
+
* Check if OAuth is supported for this provider
|
149
|
+
* Qwen endpoints support OAuth, standard OpenAI does not
|
150
|
+
*/
|
151
|
+
supportsOAuth() {
|
152
|
+
const baseURL = this.getBaseURL();
|
153
|
+
// Check if this is a Qwen endpoint that supports OAuth
|
154
|
+
if (baseURL &&
|
155
|
+
(baseURL.includes('dashscope.aliyuncs.com') ||
|
156
|
+
baseURL.includes('api.qwen.com') ||
|
157
|
+
baseURL.includes('qwen'))) {
|
181
158
|
return true;
|
182
159
|
}
|
183
|
-
|
184
|
-
return
|
185
|
-
}
|
186
|
-
getToolFormat() {
|
187
|
-
// Check manual override first
|
188
|
-
if (this.toolFormatOverride) {
|
189
|
-
return this.toolFormatOverride;
|
190
|
-
}
|
191
|
-
// Check for settings override
|
192
|
-
if (this.providerConfig?.providerToolFormatOverrides?.[this.name]) {
|
193
|
-
return this.providerConfig.providerToolFormatOverrides[this.name];
|
194
|
-
}
|
195
|
-
// Auto-detect tool format based on model or base URL
|
196
|
-
if (this.currentModel.includes('deepseek') ||
|
197
|
-
this.baseURL?.includes('deepseek')) {
|
198
|
-
return 'deepseek';
|
199
|
-
}
|
200
|
-
// Check for Qwen - including OAuth authenticated Qwen
|
201
|
-
if (this.isUsingQwen()) {
|
202
|
-
return 'qwen';
|
203
|
-
}
|
204
|
-
// Default to OpenAI format
|
205
|
-
return 'openai';
|
160
|
+
// Standard OpenAI endpoints don't support OAuth
|
161
|
+
return false;
|
206
162
|
}
|
207
163
|
async getModels() {
|
208
|
-
// Check if API key is available (using resolved authentication)
|
209
|
-
const apiKey = await this.getAuthToken();
|
210
|
-
if (!apiKey) {
|
211
|
-
const endpoint = this.baseURL || 'https://api.openai.com/v1';
|
212
|
-
if (this.isOAuthEnabled() && !this.supportsOAuth()) {
|
213
|
-
throw new Error(generateOAuthEndpointMismatchError(endpoint, 'qwen'));
|
214
|
-
}
|
215
|
-
throw new Error('OpenAI API key is required to fetch models');
|
216
|
-
}
|
217
164
|
try {
|
218
|
-
//
|
219
|
-
|
220
|
-
const
|
165
|
+
// Always try to fetch models, regardless of auth status
|
166
|
+
// Local endpoints often work without authentication
|
167
|
+
const client = await this.getClient();
|
168
|
+
const response = await client.models.list();
|
221
169
|
const models = [];
|
222
170
|
for await (const model of response) {
|
223
|
-
// Filter out non-chat models (embeddings, audio, image,
|
171
|
+
// Filter out non-chat models (embeddings, audio, image, vision, DALL·E, etc.)
|
224
172
|
if (!/embedding|whisper|audio|tts|image|vision|dall[- ]?e|moderation/i.test(model.id)) {
|
225
173
|
models.push({
|
226
174
|
id: model.id,
|
@@ -235,844 +183,532 @@ export class OpenAIProvider extends BaseProvider {
|
|
235
183
|
catch (error) {
|
236
184
|
this.logger.debug(() => `Error fetching models from OpenAI: ${error}`);
|
237
185
|
// Return a hardcoded list as fallback
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
]
|
248
|
-
}
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
await this.validateRequestPreconditions(messages);
|
281
|
-
// 2. Prepare request configuration
|
282
|
-
const requestConfig = this.prepareApiRequest(messages, tools);
|
283
|
-
// 3. Make API call with error handling
|
284
|
-
const response = await this.executeApiCall(messages, tools, requestConfig);
|
285
|
-
// 4. Process response based on streaming mode
|
286
|
-
let processedData = {
|
287
|
-
fullContent: '',
|
288
|
-
accumulatedToolCalls: [],
|
289
|
-
hasStreamedContent: false,
|
290
|
-
usageData: undefined,
|
291
|
-
pendingWhitespace: null,
|
292
|
-
};
|
293
|
-
if (requestConfig.streamingEnabled) {
|
294
|
-
// Need to yield streaming content as it comes
|
295
|
-
const streamResponse = response;
|
296
|
-
for await (const chunk of streamResponse) {
|
297
|
-
const delta = chunk.choices?.[0]?.delta;
|
298
|
-
if (delta?.content && !requestConfig.parser) {
|
299
|
-
if (this.isUsingQwen()) {
|
300
|
-
// Handle Qwen whitespace buffering inline for yielding
|
301
|
-
// This is needed because we yield during streaming
|
302
|
-
// We'll refactor this separately if needed
|
303
|
-
const whitespaceResult = this.handleQwenStreamingWhitespace(delta, processedData.pendingWhitespace, processedData.fullContent);
|
304
|
-
if (whitespaceResult.shouldYield) {
|
305
|
-
yield {
|
306
|
-
role: ContentGeneratorRole.ASSISTANT,
|
307
|
-
content: whitespaceResult.content,
|
308
|
-
};
|
309
|
-
}
|
310
|
-
// Update our tracking of processed data
|
311
|
-
processedData = {
|
312
|
-
fullContent: whitespaceResult.updatedFullContent,
|
313
|
-
accumulatedToolCalls: processedData.accumulatedToolCalls,
|
314
|
-
hasStreamedContent: processedData.hasStreamedContent ||
|
315
|
-
whitespaceResult.shouldYield,
|
316
|
-
usageData: processedData.usageData,
|
317
|
-
pendingWhitespace: whitespaceResult.updatedPendingWhitespace,
|
318
|
-
};
|
319
|
-
}
|
320
|
-
else {
|
321
|
-
yield {
|
322
|
-
role: ContentGeneratorRole.ASSISTANT,
|
323
|
-
content: delta.content,
|
324
|
-
};
|
325
|
-
processedData = {
|
326
|
-
fullContent: processedData.fullContent + delta.content,
|
327
|
-
accumulatedToolCalls: processedData.accumulatedToolCalls,
|
328
|
-
hasStreamedContent: true,
|
329
|
-
usageData: processedData.usageData,
|
330
|
-
pendingWhitespace: null,
|
331
|
-
};
|
332
|
-
}
|
333
|
-
}
|
334
|
-
else if (delta?.content) {
|
335
|
-
// Parser mode - just accumulate
|
336
|
-
processedData = {
|
337
|
-
fullContent: processedData.fullContent + delta.content,
|
338
|
-
accumulatedToolCalls: processedData.accumulatedToolCalls,
|
339
|
-
hasStreamedContent: processedData.hasStreamedContent,
|
340
|
-
usageData: processedData.usageData,
|
341
|
-
pendingWhitespace: processedData.pendingWhitespace,
|
342
|
-
};
|
343
|
-
}
|
344
|
-
// Handle tool calls
|
345
|
-
if (delta?.tool_calls) {
|
346
|
-
const accumulated = processedData.accumulatedToolCalls;
|
347
|
-
for (const toolCall of delta.tool_calls) {
|
348
|
-
this.toolFormatter.accumulateStreamingToolCall(toolCall, accumulated, requestConfig.currentToolFormat);
|
349
|
-
}
|
350
|
-
processedData = {
|
351
|
-
...processedData,
|
352
|
-
accumulatedToolCalls: accumulated,
|
353
|
-
};
|
354
|
-
}
|
355
|
-
// Check for usage data
|
356
|
-
if (chunk.usage) {
|
357
|
-
processedData = {
|
358
|
-
...processedData,
|
359
|
-
usageData: {
|
360
|
-
prompt_tokens: chunk.usage.prompt_tokens || 0,
|
361
|
-
completion_tokens: chunk.usage.completion_tokens || 0,
|
362
|
-
total_tokens: chunk.usage.total_tokens || 0,
|
363
|
-
},
|
364
|
-
};
|
365
|
-
}
|
366
|
-
}
|
367
|
-
}
|
368
|
-
else {
|
369
|
-
// Non-streaming response
|
370
|
-
processedData = this.processNonStreamingResponse(response);
|
371
|
-
// For non-streaming, yield content if no parser
|
372
|
-
if (!requestConfig.parser && processedData.fullContent) {
|
373
|
-
yield {
|
374
|
-
role: ContentGeneratorRole.ASSISTANT,
|
375
|
-
content: processedData.fullContent,
|
376
|
-
};
|
377
|
-
processedData.hasStreamedContent = true;
|
378
|
-
}
|
379
|
-
}
|
380
|
-
// 5. Flush pending whitespace if needed (for Qwen)
|
381
|
-
if (processedData.pendingWhitespace &&
|
382
|
-
this.isUsingQwen() &&
|
383
|
-
!requestConfig.parser) {
|
384
|
-
this.logger.debug(() => `Flushing trailing pending whitespace (len=${processedData.pendingWhitespace?.length ?? 0}) at stream end`);
|
385
|
-
yield {
|
386
|
-
role: ContentGeneratorRole.ASSISTANT,
|
387
|
-
content: processedData.pendingWhitespace,
|
388
|
-
};
|
389
|
-
processedData.hasStreamedContent = true;
|
390
|
-
processedData.fullContent += processedData.pendingWhitespace;
|
391
|
-
processedData.pendingWhitespace = null;
|
392
|
-
}
|
393
|
-
// 6. Process and yield final results
|
394
|
-
yield* this.processFinalResponse(processedData, requestConfig.parser);
|
395
|
-
}
|
396
|
-
setModel(modelId) {
|
397
|
-
// Update SettingsService as the source of truth
|
398
|
-
this.setModelInSettings(modelId).catch((error) => {
|
399
|
-
this.logger.debug(() => `Failed to persist model to SettingsService: ${error}`);
|
400
|
-
});
|
401
|
-
// Keep local cache for performance
|
402
|
-
this.currentModel = modelId;
|
403
|
-
}
|
404
|
-
getCurrentModel() {
|
405
|
-
// Try to get from SettingsService first (source of truth)
|
406
|
-
try {
|
407
|
-
const settingsService = getSettingsService();
|
408
|
-
const providerSettings = settingsService.getProviderSettings(this.name);
|
409
|
-
if (providerSettings.model) {
|
410
|
-
return providerSettings.model;
|
411
|
-
}
|
412
|
-
}
|
413
|
-
catch (error) {
|
414
|
-
this.logger.debug(() => `Failed to get model from SettingsService: ${error}`);
|
415
|
-
}
|
416
|
-
// Fall back to cached value or default
|
417
|
-
return this.currentModel || this.getDefaultModel();
|
186
|
+
return this.getFallbackModels();
|
187
|
+
}
|
188
|
+
}
|
189
|
+
getFallbackModels() {
|
190
|
+
return [
|
191
|
+
{
|
192
|
+
id: 'gpt-5',
|
193
|
+
name: 'GPT-5',
|
194
|
+
provider: 'openai',
|
195
|
+
supportedToolFormats: ['openai'],
|
196
|
+
},
|
197
|
+
{
|
198
|
+
id: 'gpt-4.1',
|
199
|
+
name: 'GPT-4.1',
|
200
|
+
provider: 'openai',
|
201
|
+
supportedToolFormats: ['openai'],
|
202
|
+
},
|
203
|
+
{
|
204
|
+
id: 'gpt-4o',
|
205
|
+
name: 'GPT-4o',
|
206
|
+
provider: 'openai',
|
207
|
+
supportedToolFormats: ['openai'],
|
208
|
+
},
|
209
|
+
{
|
210
|
+
id: 'o3',
|
211
|
+
name: 'O3',
|
212
|
+
provider: 'openai',
|
213
|
+
supportedToolFormats: ['openai'],
|
214
|
+
},
|
215
|
+
{
|
216
|
+
id: 'o4-mini',
|
217
|
+
name: 'O4 Mini',
|
218
|
+
provider: 'openai',
|
219
|
+
supportedToolFormats: ['openai'],
|
220
|
+
},
|
221
|
+
{
|
222
|
+
id: 'gpt-3.5-turbo',
|
223
|
+
name: 'GPT-3.5 Turbo (Legacy)',
|
224
|
+
provider: 'openai',
|
225
|
+
supportedToolFormats: ['openai'],
|
226
|
+
},
|
227
|
+
];
|
418
228
|
}
|
419
229
|
getDefaultModel() {
|
420
|
-
// Return
|
421
|
-
// This can be overridden based on configuration or endpoint
|
422
|
-
if (this.isUsingQwen()) {
|
423
|
-
return 'qwen3-coder-plus';
|
424
|
-
}
|
230
|
+
// Return hardcoded default - do NOT call getModel() to avoid circular dependency
|
425
231
|
return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
|
426
232
|
}
|
427
|
-
setApiKey(apiKey) {
|
428
|
-
// Call base provider implementation
|
429
|
-
super.setApiKey(apiKey);
|
430
|
-
// Persist to SettingsService if available
|
431
|
-
this.setApiKeyInSettings(apiKey).catch((error) => {
|
432
|
-
this.logger.debug(() => `Failed to persist API key to SettingsService: ${error}`);
|
433
|
-
});
|
434
|
-
// Create a new OpenAI client with the updated API key
|
435
|
-
const clientOptions = {
|
436
|
-
apiKey,
|
437
|
-
dangerouslyAllowBrowser: this.providerConfig?.allowBrowserEnvironment || false,
|
438
|
-
};
|
439
|
-
// Only include baseURL if it's defined
|
440
|
-
if (this.baseURL) {
|
441
|
-
clientOptions.baseURL = this.baseURL;
|
442
|
-
}
|
443
|
-
this.openai = new OpenAI(clientOptions);
|
444
|
-
this._cachedClientKey = apiKey; // Update cached key
|
445
|
-
}
|
446
|
-
setBaseUrl(baseUrl) {
|
447
|
-
// If no baseUrl is provided, clear to default (undefined)
|
448
|
-
this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
|
449
|
-
// Persist to SettingsService if available
|
450
|
-
this.setBaseUrlInSettings(this.baseURL).catch((error) => {
|
451
|
-
this.logger.debug(() => `Failed to persist base URL to SettingsService: ${error}`);
|
452
|
-
});
|
453
|
-
// Update OAuth configuration based on endpoint validation
|
454
|
-
// Enable OAuth for Qwen endpoints if we have an OAuth manager
|
455
|
-
const shouldEnableQwenOAuth = !!this.baseProviderConfig.oauthManager &&
|
456
|
-
(isQwenEndpoint(this.baseURL || '') ||
|
457
|
-
this.baseURL === 'https://portal.qwen.ai/v1');
|
458
|
-
this.updateOAuthConfig(shouldEnableQwenOAuth, shouldEnableQwenOAuth ? 'qwen' : undefined, this.baseProviderConfig.oauthManager);
|
459
|
-
// Call base provider implementation
|
460
|
-
super.setBaseUrl?.(baseUrl);
|
461
|
-
// Create a new OpenAI client with the updated (or cleared) base URL
|
462
|
-
const clientOptions = {
|
463
|
-
// Use existing key or empty string as placeholder
|
464
|
-
apiKey: this._cachedClientKey || 'placeholder',
|
465
|
-
dangerouslyAllowBrowser: this.providerConfig?.allowBrowserEnvironment || false,
|
466
|
-
};
|
467
|
-
// Only include baseURL if it's defined
|
468
|
-
if (this.baseURL) {
|
469
|
-
clientOptions.baseURL = this.baseURL;
|
470
|
-
}
|
471
|
-
this.openai = new OpenAI(clientOptions);
|
472
|
-
// Clear cached key to force re-resolution on next API call
|
473
|
-
this._cachedClientKey = undefined;
|
474
|
-
}
|
475
|
-
setConfig(config) {
|
476
|
-
this.providerConfig = config;
|
477
|
-
}
|
478
|
-
setToolFormatOverride(format) {
|
479
|
-
this.toolFormatOverride = format || undefined;
|
480
|
-
}
|
481
|
-
/**
|
482
|
-
* OpenAI always requires payment (API key)
|
483
|
-
*/
|
484
|
-
isPaidMode() {
|
485
|
-
return true;
|
486
|
-
}
|
487
|
-
clearState() {
|
488
|
-
// No state to clear in base OpenAI provider
|
489
|
-
}
|
490
|
-
/**
|
491
|
-
* Get the list of server tools supported by this provider
|
492
|
-
*/
|
493
233
|
getServerTools() {
|
234
|
+
// TODO: Implement server tools for OpenAI provider
|
494
235
|
return [];
|
495
236
|
}
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
async invokeServerTool(_toolName, _params, _config) {
|
500
|
-
throw new Error('Server tools not supported by OpenAI provider');
|
237
|
+
async invokeServerTool(toolName, _params, _config) {
|
238
|
+
// TODO: Implement server tool invocation for OpenAI provider
|
239
|
+
throw new Error(`Server tool '${toolName}' not supported by OpenAI provider`);
|
501
240
|
}
|
502
241
|
/**
|
503
|
-
*
|
504
|
-
*
|
242
|
+
* Normalize tool IDs from various formats to OpenAI format
|
243
|
+
* Handles IDs from OpenAI (call_xxx), Anthropic (toolu_xxx), and history (hist_tool_xxx)
|
505
244
|
*/
|
506
|
-
|
507
|
-
|
508
|
-
|
245
|
+
normalizeToOpenAIToolId(id) {
|
246
|
+
// If already in OpenAI format, return as-is
|
247
|
+
if (id.startsWith('call_')) {
|
248
|
+
return id;
|
509
249
|
}
|
510
|
-
|
511
|
-
|
250
|
+
// For history format, extract the UUID and add OpenAI prefix
|
251
|
+
if (id.startsWith('hist_tool_')) {
|
252
|
+
const uuid = id.substring('hist_tool_'.length);
|
253
|
+
return 'call_' + uuid;
|
512
254
|
}
|
513
|
-
//
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
* @returns Current parameters or undefined if not set
|
521
|
-
*/
|
522
|
-
getModelParams() {
|
523
|
-
return this.modelParams;
|
255
|
+
// For Anthropic format, extract the UUID and add OpenAI prefix
|
256
|
+
if (id.startsWith('toolu_')) {
|
257
|
+
const uuid = id.substring('toolu_'.length);
|
258
|
+
return 'call_' + uuid;
|
259
|
+
}
|
260
|
+
// Unknown format - assume it's a raw UUID
|
261
|
+
return 'call_' + id;
|
524
262
|
}
|
525
263
|
/**
|
526
|
-
*
|
264
|
+
* Normalize tool IDs from OpenAI format to history format
|
527
265
|
*/
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
if (savedModel) {
|
533
|
-
this.currentModel = savedModel;
|
534
|
-
}
|
535
|
-
// Load saved base URL if available
|
536
|
-
const savedBaseUrl = await this.getBaseUrlFromSettings();
|
537
|
-
if (savedBaseUrl !== undefined) {
|
538
|
-
this.baseURL = savedBaseUrl;
|
539
|
-
}
|
540
|
-
// Load saved model parameters if available
|
541
|
-
const savedParams = await this.getModelParamsFromSettings();
|
542
|
-
if (savedParams) {
|
543
|
-
this.modelParams = savedParams;
|
544
|
-
}
|
545
|
-
this.logger.debug(() => `Initialized from SettingsService - model: ${this.currentModel}, baseURL: ${this.baseURL}, params: ${JSON.stringify(this.modelParams)}`);
|
266
|
+
normalizeToHistoryToolId(id) {
|
267
|
+
// If already in history format, return as-is
|
268
|
+
if (id.startsWith('hist_tool_')) {
|
269
|
+
return id;
|
546
270
|
}
|
547
|
-
|
548
|
-
|
271
|
+
// For OpenAI format, extract the UUID and add history prefix
|
272
|
+
if (id.startsWith('call_')) {
|
273
|
+
const uuid = id.substring('call_'.length);
|
274
|
+
return 'hist_tool_' + uuid;
|
549
275
|
}
|
276
|
+
// For Anthropic format, extract the UUID and add history prefix
|
277
|
+
if (id.startsWith('toolu_')) {
|
278
|
+
const uuid = id.substring('toolu_'.length);
|
279
|
+
return 'hist_tool_' + uuid;
|
280
|
+
}
|
281
|
+
// Unknown format - assume it's a raw UUID
|
282
|
+
return 'hist_tool_' + id;
|
550
283
|
}
|
551
284
|
/**
|
552
|
-
*
|
553
|
-
*
|
285
|
+
* Generate chat completion with IContent interface
|
286
|
+
* Internally converts to OpenAI API format, but only yields IContent
|
287
|
+
* @param contents Array of content blocks (text and tool_call)
|
288
|
+
* @param tools Array of available tools
|
554
289
|
*/
|
555
|
-
async
|
556
|
-
|
290
|
+
async *generateChatCompletion(contents, tools) {
|
291
|
+
// Debug log what we receive
|
292
|
+
if (this.logger.enabled) {
|
293
|
+
this.logger.debug(() => `[OpenAIProvider] generateChatCompletion received tools:`, {
|
294
|
+
hasTools: !!tools,
|
295
|
+
toolsLength: tools?.length,
|
296
|
+
toolsType: typeof tools,
|
297
|
+
isArray: Array.isArray(tools),
|
298
|
+
firstToolName: tools?.[0]?.functionDeclarations?.[0]?.name,
|
299
|
+
toolsStructure: tools ? 'available' : 'undefined',
|
300
|
+
});
|
301
|
+
}
|
302
|
+
// Pass tools directly in Gemini format - they'll be converted in generateChatCompletionImpl
|
303
|
+
const generator = this.generateChatCompletionImpl(contents, tools, undefined, undefined, undefined);
|
304
|
+
for await (const item of generator) {
|
305
|
+
yield item;
|
306
|
+
}
|
557
307
|
}
|
558
308
|
/**
|
559
|
-
*
|
560
|
-
* @returns The detected tool format
|
309
|
+
* Convert IContent array to OpenAI ChatCompletionMessageParam array
|
561
310
|
*/
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
const modelName = this.currentModel.toLowerCase();
|
576
|
-
// Check for GLM-4.5 models (glm-4.5, glm-4-5)
|
577
|
-
if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {
|
578
|
-
return 'qwen';
|
579
|
-
}
|
580
|
-
// Check for qwen models
|
581
|
-
if (modelName.includes('qwen')) {
|
582
|
-
return 'qwen';
|
311
|
+
convertToOpenAIMessages(contents) {
|
312
|
+
const messages = [];
|
313
|
+
for (const content of contents) {
|
314
|
+
if (content.speaker === 'human') {
|
315
|
+
// Convert human messages to user messages
|
316
|
+
const textBlocks = content.blocks.filter((b) => b.type === 'text');
|
317
|
+
const text = textBlocks.map((b) => b.text).join('\n');
|
318
|
+
if (text) {
|
319
|
+
messages.push({
|
320
|
+
role: 'user',
|
321
|
+
content: text,
|
322
|
+
});
|
323
|
+
}
|
583
324
|
}
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
325
|
+
else if (content.speaker === 'ai') {
|
326
|
+
// Convert AI messages
|
327
|
+
const textBlocks = content.blocks.filter((b) => b.type === 'text');
|
328
|
+
const toolCalls = content.blocks.filter((b) => b.type === 'tool_call');
|
329
|
+
if (toolCalls.length > 0) {
|
330
|
+
// Assistant message with tool calls
|
331
|
+
const text = textBlocks.map((b) => b.text).join('\n');
|
332
|
+
messages.push({
|
333
|
+
role: 'assistant',
|
334
|
+
content: text || null,
|
335
|
+
tool_calls: toolCalls.map((tc) => ({
|
336
|
+
id: this.normalizeToOpenAIToolId(tc.id),
|
337
|
+
type: 'function',
|
338
|
+
function: {
|
339
|
+
name: tc.name,
|
340
|
+
arguments: typeof tc.parameters === 'string'
|
341
|
+
? tc.parameters
|
342
|
+
: JSON.stringify(tc.parameters),
|
343
|
+
},
|
344
|
+
})),
|
345
|
+
});
|
346
|
+
}
|
347
|
+
else if (textBlocks.length > 0) {
|
348
|
+
// Plain assistant message
|
349
|
+
const text = textBlocks.map((b) => b.text).join('\n');
|
350
|
+
messages.push({
|
351
|
+
role: 'assistant',
|
352
|
+
content: text,
|
353
|
+
});
|
354
|
+
}
|
593
355
|
}
|
594
|
-
if (
|
595
|
-
|
356
|
+
else if (content.speaker === 'tool') {
|
357
|
+
// Convert tool responses
|
358
|
+
const toolResponses = content.blocks.filter((b) => b.type === 'tool_response');
|
359
|
+
for (const tr of toolResponses) {
|
360
|
+
messages.push({
|
361
|
+
role: 'tool',
|
362
|
+
content: typeof tr.result === 'string'
|
363
|
+
? tr.result
|
364
|
+
: JSON.stringify(tr.result),
|
365
|
+
tool_call_id: this.normalizeToOpenAIToolId(tr.callId),
|
366
|
+
});
|
367
|
+
}
|
596
368
|
}
|
597
|
-
return 'openai';
|
598
369
|
}
|
370
|
+
return messages;
|
599
371
|
}
|
600
372
|
/**
|
601
|
-
*
|
602
|
-
* @param tools Array of tools (if any)
|
603
|
-
* @returns Appropriate tool_choice value for the current format
|
604
|
-
*/
|
605
|
-
getToolChoiceForFormat(tools) {
|
606
|
-
if (!tools || tools.length === 0) {
|
607
|
-
return undefined;
|
608
|
-
}
|
609
|
-
// For all formats, use 'auto' (standard behavior)
|
610
|
-
// Future enhancement: different formats may need different tool_choice values
|
611
|
-
return 'auto';
|
612
|
-
}
|
613
|
-
/**
|
614
|
-
* Format tools for API based on detected tool format
|
615
|
-
* @param tools Array of tools to format
|
616
|
-
* @returns Formatted tools for API consumption
|
617
|
-
*/
|
618
|
-
formatToolsForAPI(tools) {
|
619
|
-
// For now, always use OpenAI format through OpenRouter
|
620
|
-
// TODO: Investigate if OpenRouter needs special handling for GLM/Qwen
|
621
|
-
// const detectedFormat = this.detectToolFormat();
|
622
|
-
// if (detectedFormat === 'qwen') {
|
623
|
-
// // Convert OpenAI format to Qwen format: {name, description, parameters} without type/function wrapper
|
624
|
-
// return tools.map((tool) => ({
|
625
|
-
// name: tool.function.name,
|
626
|
-
// description: tool.function.description,
|
627
|
-
// parameters: tool.function.parameters,
|
628
|
-
// }));
|
629
|
-
// }
|
630
|
-
// For all formats, use the existing ToolFormatter
|
631
|
-
return this.toolFormatter.toProviderFormat(tools, 'openai');
|
632
|
-
}
|
633
|
-
/**
|
634
|
-
* Parse tool response from API (placeholder for future response parsing)
|
635
|
-
* @param response The raw API response
|
636
|
-
* @returns Parsed tool response
|
637
|
-
*/
|
638
|
-
parseToolResponse(response) {
|
639
|
-
// TODO: Implement response parsing based on detected format
|
640
|
-
// For now, return the response as-is
|
641
|
-
return response;
|
642
|
-
}
|
643
|
-
/**
|
644
|
-
* Validate authentication and message preconditions for API calls
|
373
|
+
* Internal implementation for chat completion
|
645
374
|
*/
|
646
|
-
async
|
647
|
-
//
|
648
|
-
const
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
375
|
+
async *generateChatCompletionImpl(contents, tools, maxTokens, abortSignal, modelName) {
|
376
|
+
// Always look up model from SettingsService
|
377
|
+
const model = modelName || this.getModel() || this.getDefaultModel();
|
378
|
+
// Convert IContent to OpenAI messages format
|
379
|
+
const messages = this.convertToOpenAIMessages(contents);
|
380
|
+
// Convert Gemini format tools directly to OpenAI format using the new method
|
381
|
+
const formattedTools = this.toolFormatter.convertGeminiToOpenAI(tools);
|
382
|
+
// Debug log the conversion result
|
383
|
+
if (this.logger.enabled) {
|
384
|
+
this.logger.debug(() => `[OpenAIProvider] Tool conversion summary:`, {
|
385
|
+
inputHadTools: !!tools,
|
386
|
+
inputToolsLength: tools?.length,
|
387
|
+
outputHasTools: !!formattedTools,
|
388
|
+
outputToolsLength: formattedTools?.length,
|
389
|
+
outputToolNames: formattedTools?.map((t) => t.function.name),
|
390
|
+
});
|
662
391
|
}
|
663
|
-
}
|
664
|
-
/**
|
665
|
-
* Prepare API request configuration
|
666
|
-
*/
|
667
|
-
prepareApiRequest(messages, tools) {
|
668
|
-
const parser = this.requiresTextToolCallParsing()
|
669
|
-
? new GemmaToolCallParser()
|
670
|
-
: null;
|
671
|
-
// Get current tool format (with override support)
|
672
|
-
const currentToolFormat = this.getToolFormat();
|
673
|
-
// Format tools using formatToolsForAPI method
|
674
|
-
const formattedTools = tools ? this.formatToolsForAPI(tools) : undefined;
|
675
|
-
// Get stream_options from ephemeral settings (not model params)
|
676
|
-
const streamOptions = this.providerConfig?.getEphemeralSettings?.()?.['stream-options'];
|
677
|
-
// Default stream_options to { include_usage: true } unless explicitly set
|
678
|
-
const finalStreamOptions = streamOptions !== undefined ? streamOptions : { include_usage: true };
|
679
392
|
// Get streaming setting from ephemeral settings (default: enabled)
|
680
393
|
const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
|
681
394
|
const streamingEnabled = streamingSetting !== 'disabled';
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
395
|
+
// Get the system prompt
|
396
|
+
const userMemory = this.globalConfig?.getUserMemory
|
397
|
+
? this.globalConfig.getUserMemory()
|
398
|
+
: '';
|
399
|
+
const systemPrompt = await getCoreSystemPromptAsync(userMemory, model, undefined);
|
400
|
+
// Add system prompt as the first message in the array
|
401
|
+
const messagesWithSystem = [
|
402
|
+
{ role: 'system', content: systemPrompt },
|
403
|
+
...messages,
|
404
|
+
];
|
405
|
+
// Build request - only include tools if they exist and are not empty
|
406
|
+
const requestBody = {
|
407
|
+
model,
|
408
|
+
messages: messagesWithSystem,
|
409
|
+
...(formattedTools && formattedTools.length > 0
|
410
|
+
? {
|
411
|
+
tools: formattedTools,
|
412
|
+
// Add tool_choice for Qwen/Cerebras to ensure proper tool calling
|
413
|
+
tool_choice: 'auto',
|
414
|
+
}
|
415
|
+
: {}),
|
416
|
+
max_tokens: maxTokens,
|
417
|
+
stream: streamingEnabled,
|
688
418
|
};
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
return await this.openai.chat.completions.create({
|
702
|
-
model: this.currentModel,
|
703
|
-
messages: messages,
|
704
|
-
stream: requestConfig.streamingEnabled,
|
705
|
-
...(requestConfig.streamingEnabled && requestConfig.finalStreamOptions
|
706
|
-
? { stream_options: requestConfig.finalStreamOptions }
|
707
|
-
: {}),
|
708
|
-
tools: requestConfig.formattedTools,
|
709
|
-
tool_choice: this.getToolChoiceForFormat(tools),
|
710
|
-
...this.modelParams,
|
419
|
+
// Debug log request summary for Cerebras/Qwen
|
420
|
+
if (this.logger.enabled &&
|
421
|
+
(model.toLowerCase().includes('qwen') ||
|
422
|
+
this.getBaseURL()?.includes('cerebras'))) {
|
423
|
+
this.logger.debug(() => `Request to ${this.getBaseURL()} for model ${model}:`, {
|
424
|
+
baseURL: this.getBaseURL(),
|
425
|
+
model,
|
426
|
+
streamingEnabled,
|
427
|
+
hasTools: 'tools' in requestBody,
|
428
|
+
toolCount: formattedTools?.length || 0,
|
429
|
+
messageCount: messages.length,
|
430
|
+
toolsInRequest: 'tools' in requestBody ? requestBody.tools?.length : 'not included',
|
711
431
|
});
|
712
432
|
}
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
}
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
const
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
433
|
+
// Get OpenAI client
|
434
|
+
const client = await this.getClient();
|
435
|
+
// Get retry settings from ephemeral settings
|
436
|
+
const ephemeralSettings = this.providerConfig?.getEphemeralSettings?.() || {};
|
437
|
+
const maxRetries = ephemeralSettings['retries'] ?? 6; // Default for OpenAI
|
438
|
+
const initialDelayMs = ephemeralSettings['retrywait'] ?? 4000; // Default for OpenAI
|
439
|
+
// Wrap the API call with retry logic using centralized retry utility
|
440
|
+
const response = await retryWithBackoff(() => client.chat.completions.create(requestBody, { signal: abortSignal }), {
|
441
|
+
maxAttempts: maxRetries,
|
442
|
+
initialDelayMs,
|
443
|
+
maxDelayMs: 30000, // 30 seconds
|
444
|
+
shouldRetry: this.shouldRetryResponse.bind(this),
|
445
|
+
});
|
446
|
+
// Check if response is streaming or not
|
447
|
+
if (streamingEnabled) {
|
448
|
+
// Process streaming response
|
449
|
+
let _accumulatedText = '';
|
450
|
+
const accumulatedToolCalls = [];
|
451
|
+
// Buffer for accumulating text chunks for providers that need it
|
452
|
+
let textBuffer = '';
|
453
|
+
const detectedFormat = this.detectToolFormat();
|
454
|
+
// Buffer text for Qwen format providers to avoid stanza formatting
|
455
|
+
const shouldBufferText = detectedFormat === 'qwen';
|
456
|
+
try {
|
457
|
+
// Handle streaming response
|
458
|
+
for await (const chunk of response) {
|
459
|
+
if (abortSignal?.aborted) {
|
460
|
+
break;
|
461
|
+
}
|
462
|
+
const choice = chunk.choices?.[0];
|
463
|
+
if (!choice)
|
464
|
+
continue;
|
465
|
+
// Check for finish_reason to detect proper stream ending
|
466
|
+
if (choice.finish_reason) {
|
467
|
+
this.logger.debug(() => `[Streaming] Stream finished with reason: ${choice.finish_reason}`, {
|
468
|
+
model,
|
469
|
+
finishReason: choice.finish_reason,
|
470
|
+
hasAccumulatedText: _accumulatedText.length > 0,
|
471
|
+
hasAccumulatedTools: accumulatedToolCalls.length > 0,
|
472
|
+
hasBufferedText: textBuffer.length > 0,
|
473
|
+
});
|
474
|
+
// If finish_reason is 'length', the response was cut off
|
475
|
+
if (choice.finish_reason === 'length') {
|
476
|
+
this.logger.debug(() => `Response truncated due to length limit for model ${model}`);
|
477
|
+
}
|
478
|
+
// Flush any buffered text when stream finishes
|
479
|
+
if (textBuffer.length > 0) {
|
480
|
+
yield {
|
481
|
+
speaker: 'ai',
|
482
|
+
blocks: [
|
483
|
+
{
|
484
|
+
type: 'text',
|
485
|
+
text: textBuffer,
|
486
|
+
},
|
487
|
+
],
|
488
|
+
};
|
489
|
+
textBuffer = '';
|
490
|
+
}
|
491
|
+
}
|
492
|
+
// Handle text content - buffer for Qwen format, emit immediately for others
|
493
|
+
const deltaContent = choice.delta?.content;
|
494
|
+
if (deltaContent) {
|
495
|
+
_accumulatedText += deltaContent;
|
496
|
+
// Debug log for providers that need buffering
|
497
|
+
if (shouldBufferText) {
|
498
|
+
this.logger.debug(() => `[Streaming] Chunk content for ${detectedFormat} format:`, {
|
499
|
+
deltaContent,
|
500
|
+
length: deltaContent.length,
|
501
|
+
hasNewline: deltaContent.includes('\n'),
|
502
|
+
escaped: JSON.stringify(deltaContent),
|
503
|
+
bufferSize: textBuffer.length,
|
504
|
+
});
|
505
|
+
// Buffer text to avoid stanza formatting
|
506
|
+
textBuffer += deltaContent;
|
507
|
+
// Emit buffered text when we have a complete sentence or paragraph
|
508
|
+
// Look for natural break points
|
509
|
+
if (textBuffer.includes('\n') ||
|
510
|
+
textBuffer.endsWith('. ') ||
|
511
|
+
textBuffer.endsWith('! ') ||
|
512
|
+
textBuffer.endsWith('? ') ||
|
513
|
+
textBuffer.length > 100) {
|
514
|
+
yield {
|
515
|
+
speaker: 'ai',
|
516
|
+
blocks: [
|
517
|
+
{
|
518
|
+
type: 'text',
|
519
|
+
text: textBuffer,
|
520
|
+
},
|
521
|
+
],
|
522
|
+
};
|
523
|
+
textBuffer = '';
|
524
|
+
}
|
525
|
+
}
|
526
|
+
else {
|
527
|
+
// For other providers, emit text immediately as before
|
528
|
+
yield {
|
529
|
+
speaker: 'ai',
|
530
|
+
blocks: [
|
531
|
+
{
|
532
|
+
type: 'text',
|
533
|
+
text: deltaContent,
|
534
|
+
},
|
535
|
+
],
|
536
|
+
};
|
537
|
+
}
|
538
|
+
}
|
539
|
+
// Handle tool calls
|
540
|
+
const deltaToolCalls = choice.delta?.tool_calls;
|
541
|
+
if (deltaToolCalls && deltaToolCalls.length > 0) {
|
542
|
+
for (const deltaToolCall of deltaToolCalls) {
|
543
|
+
if (deltaToolCall.index === undefined)
|
544
|
+
continue;
|
545
|
+
// Initialize or update accumulated tool call
|
546
|
+
if (!accumulatedToolCalls[deltaToolCall.index]) {
|
547
|
+
accumulatedToolCalls[deltaToolCall.index] = {
|
548
|
+
id: deltaToolCall.id || '',
|
549
|
+
type: 'function',
|
550
|
+
function: {
|
551
|
+
name: deltaToolCall.function?.name || '',
|
552
|
+
arguments: '',
|
553
|
+
},
|
554
|
+
};
|
555
|
+
}
|
556
|
+
const tc = accumulatedToolCalls[deltaToolCall.index];
|
557
|
+
if (tc) {
|
558
|
+
if (deltaToolCall.id)
|
559
|
+
tc.id = deltaToolCall.id;
|
560
|
+
if (deltaToolCall.function?.name)
|
561
|
+
tc.function.name = deltaToolCall.function.name;
|
562
|
+
if (deltaToolCall.function?.arguments) {
|
563
|
+
tc.function.arguments += deltaToolCall.function.arguments;
|
564
|
+
}
|
565
|
+
}
|
566
|
+
}
|
567
|
+
}
|
741
568
|
}
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
const lastMessages = messages.slice(-5);
|
747
|
-
lastMessages.forEach((msg, idx) => {
|
748
|
-
this.logger.error(() => ` [${messages.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)` : ''}`);
|
749
|
-
if (msg.tool_calls) {
|
750
|
-
msg.tool_calls.forEach((tc) => {
|
751
|
-
this.logger.error(() => ` - Tool call: ${tc.id} -> ${tc.function.name}`);
|
752
|
-
});
|
569
|
+
}
|
570
|
+
catch (error) {
|
571
|
+
if (abortSignal?.aborted) {
|
572
|
+
throw error;
|
753
573
|
}
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
/**
|
758
|
-
* Process non-streaming response
|
759
|
-
*/
|
760
|
-
processNonStreamingResponse(response) {
|
761
|
-
const choice = response.choices[0];
|
762
|
-
let fullContent = '';
|
763
|
-
const accumulatedToolCalls = [];
|
764
|
-
let usageData;
|
765
|
-
if (choice?.message.content) {
|
766
|
-
fullContent = choice.message.content;
|
767
|
-
}
|
768
|
-
if (choice?.message.tool_calls) {
|
769
|
-
// Convert tool calls to the standard format
|
770
|
-
for (const toolCall of choice.message.tool_calls) {
|
771
|
-
if (toolCall.type === 'function' && toolCall.function) {
|
772
|
-
// Don't fix double stringification here - it's handled later in the final processing
|
773
|
-
accumulatedToolCalls.push({
|
774
|
-
id: toolCall.id,
|
775
|
-
type: 'function',
|
776
|
-
function: toolCall.function,
|
777
|
-
});
|
574
|
+
else {
|
575
|
+
this.logger.error('Error processing streaming response:', error);
|
576
|
+
throw error;
|
778
577
|
}
|
779
578
|
}
|
780
|
-
|
781
|
-
|
782
|
-
usageData = {
|
783
|
-
prompt_tokens: response.usage.prompt_tokens,
|
784
|
-
completion_tokens: response.usage.completion_tokens,
|
785
|
-
total_tokens: response.usage.total_tokens,
|
786
|
-
};
|
787
|
-
}
|
788
|
-
return {
|
789
|
-
fullContent,
|
790
|
-
accumulatedToolCalls,
|
791
|
-
hasStreamedContent: false, // Non-streaming never has streamed content
|
792
|
-
usageData,
|
793
|
-
pendingWhitespace: null,
|
794
|
-
};
|
795
|
-
}
|
796
|
-
/**
|
797
|
-
* Process and build final response messages
|
798
|
-
*/
|
799
|
-
*processFinalResponse(processedData, parser) {
|
800
|
-
const { fullContent, accumulatedToolCalls, hasStreamedContent, usageData, pendingWhitespace, } = processedData;
|
801
|
-
// Flush any remaining pending whitespace for Qwen
|
802
|
-
let finalFullContent = fullContent;
|
803
|
-
if (pendingWhitespace && this.isUsingQwen() && !parser) {
|
804
|
-
this.logger.debug(() => `Flushing trailing pending whitespace (len=${pendingWhitespace?.length ?? 0}) at stream end`);
|
805
|
-
finalFullContent += pendingWhitespace;
|
806
|
-
}
|
807
|
-
// After stream ends, parse text-based tool calls if needed
|
808
|
-
if (parser && finalFullContent) {
|
809
|
-
const { cleanedContent, toolCalls } = parser.parse(finalFullContent);
|
810
|
-
if (toolCalls.length > 0) {
|
811
|
-
// Convert to standard format
|
812
|
-
const standardToolCalls = toolCalls.map((tc, index) => ({
|
813
|
-
id: `call_${Date.now()}_${index}`,
|
814
|
-
type: 'function',
|
815
|
-
function: {
|
816
|
-
name: tc.name,
|
817
|
-
arguments: JSON.stringify(tc.arguments),
|
818
|
-
},
|
819
|
-
}));
|
579
|
+
// Flush any remaining buffered text
|
580
|
+
if (textBuffer.length > 0) {
|
820
581
|
yield {
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
582
|
+
speaker: 'ai',
|
583
|
+
blocks: [
|
584
|
+
{
|
585
|
+
type: 'text',
|
586
|
+
text: textBuffer,
|
587
|
+
},
|
588
|
+
],
|
825
589
|
};
|
590
|
+
textBuffer = '';
|
826
591
|
}
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
592
|
+
// Emit accumulated tool calls as IContent if any
|
593
|
+
if (accumulatedToolCalls.length > 0) {
|
594
|
+
const blocks = [];
|
595
|
+
const detectedFormat = this.detectToolFormat();
|
596
|
+
for (const tc of accumulatedToolCalls) {
|
597
|
+
if (!tc)
|
598
|
+
continue;
|
599
|
+
// Process tool parameters with double-escape handling
|
600
|
+
const processedParameters = processToolParameters(tc.function.arguments || '', tc.function.name || '', detectedFormat);
|
601
|
+
blocks.push({
|
602
|
+
type: 'tool_call',
|
603
|
+
id: this.normalizeToHistoryToolId(tc.id),
|
604
|
+
name: tc.function.name || '',
|
605
|
+
parameters: processedParameters,
|
606
|
+
});
|
607
|
+
}
|
608
|
+
if (blocks.length > 0) {
|
609
|
+
yield {
|
610
|
+
speaker: 'ai',
|
611
|
+
blocks,
|
612
|
+
};
|
613
|
+
}
|
834
614
|
}
|
835
615
|
}
|
836
616
|
else {
|
837
|
-
//
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
617
|
+
// Handle non-streaming response
|
618
|
+
const completion = response;
|
619
|
+
const choice = completion.choices?.[0];
|
620
|
+
if (!choice) {
|
621
|
+
throw new Error('No choices in completion response');
|
622
|
+
}
|
623
|
+
const blocks = [];
|
624
|
+
// Handle text content
|
625
|
+
if (choice.message?.content) {
|
626
|
+
blocks.push({
|
627
|
+
type: 'text',
|
628
|
+
text: choice.message.content,
|
629
|
+
});
|
630
|
+
}
|
631
|
+
// Handle tool calls
|
632
|
+
if (choice.message?.tool_calls && choice.message.tool_calls.length > 0) {
|
633
|
+
const detectedFormat = this.detectToolFormat();
|
634
|
+
for (const toolCall of choice.message.tool_calls) {
|
635
|
+
if (toolCall.type === 'function') {
|
636
|
+
// Process tool parameters with double-escape handling
|
637
|
+
const processedParameters = processToolParameters(toolCall.function.arguments || '', toolCall.function.name || '', detectedFormat);
|
638
|
+
blocks.push({
|
639
|
+
type: 'tool_call',
|
640
|
+
id: this.normalizeToHistoryToolId(toolCall.id),
|
641
|
+
name: toolCall.function.name || '',
|
642
|
+
parameters: processedParameters,
|
643
|
+
});
|
644
|
+
}
|
849
645
|
}
|
850
|
-
// Build the final message based on provider-specific requirements
|
851
|
-
const finalMessage = this.buildFinalToolCallMessage(hasStreamedContent, finalFullContent, fixedToolCalls, usageData);
|
852
|
-
yield finalMessage;
|
853
646
|
}
|
854
|
-
|
855
|
-
|
647
|
+
// Emit the complete response as a single IContent
|
648
|
+
if (blocks.length > 0) {
|
856
649
|
yield {
|
857
|
-
|
858
|
-
|
859
|
-
usage: usageData,
|
650
|
+
speaker: 'ai',
|
651
|
+
blocks,
|
860
652
|
};
|
861
653
|
}
|
862
654
|
}
|
863
655
|
}
|
864
656
|
/**
|
865
|
-
*
|
866
|
-
* @
|
867
|
-
* @param pendingWhitespace Current buffered whitespace
|
868
|
-
* @param fullContent Accumulated full content
|
869
|
-
* @returns Object with updated state and whether to yield content
|
657
|
+
* Detects the tool call format based on the model being used
|
658
|
+
* @returns The detected tool format ('openai' or 'qwen')
|
870
659
|
*/
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
};
|
660
|
+
detectToolFormat() {
|
661
|
+
try {
|
662
|
+
// Try to get format from SettingsService if available
|
663
|
+
const settings = this.providerConfig?.getEphemeralSettings?.();
|
664
|
+
if (settings && settings['tool-format']) {
|
665
|
+
return settings['tool-format'];
|
666
|
+
}
|
879
667
|
}
|
880
|
-
|
881
|
-
|
882
|
-
// Buffer whitespace-only chunk
|
883
|
-
const newPendingWhitespace = (pendingWhitespace || '') + delta.content;
|
884
|
-
this.logger.debug(() => `[Whitespace Buffering] Buffered whitespace-only chunk (len=${delta.content?.length ?? 0}). pendingWhitespace now len=${newPendingWhitespace?.length ?? 0}`);
|
885
|
-
return {
|
886
|
-
shouldYield: false,
|
887
|
-
content: '',
|
888
|
-
updatedPendingWhitespace: newPendingWhitespace,
|
889
|
-
updatedFullContent: fullContent + delta.content,
|
890
|
-
};
|
668
|
+
catch (error) {
|
669
|
+
this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
|
891
670
|
}
|
892
|
-
//
|
893
|
-
|
894
|
-
|
895
|
-
return
|
896
|
-
shouldYield: true,
|
897
|
-
content: pendingWhitespace + delta.content,
|
898
|
-
updatedPendingWhitespace: null,
|
899
|
-
updatedFullContent: fullContent + pendingWhitespace + delta.content,
|
900
|
-
};
|
671
|
+
// Fallback detection without SettingsService - always look up current model
|
672
|
+
const modelName = (this.getModel() || this.getDefaultModel()).toLowerCase();
|
673
|
+
if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {
|
674
|
+
return 'qwen';
|
901
675
|
}
|
902
|
-
|
903
|
-
|
904
|
-
content: delta.content,
|
905
|
-
updatedPendingWhitespace: null,
|
906
|
-
updatedFullContent: fullContent + delta.content,
|
907
|
-
};
|
908
|
-
}
|
909
|
-
/**
|
910
|
-
* Process tool calls for Qwen models, fixing double stringification
|
911
|
-
* @param toolCalls The tool calls to process
|
912
|
-
* @returns Processed tool calls with fixes applied
|
913
|
-
*/
|
914
|
-
processQwenToolCalls(toolCalls) {
|
915
|
-
if (!this.isUsingQwen()) {
|
916
|
-
return toolCalls;
|
676
|
+
if (modelName.includes('qwen')) {
|
677
|
+
return 'qwen';
|
917
678
|
}
|
918
|
-
|
919
|
-
return toolCalls.map((toolCall, index) => {
|
920
|
-
this.logger.debug(() => `[Qwen Fix] Tool call ${index}: ${JSON.stringify({
|
921
|
-
name: toolCall.function.name,
|
922
|
-
argumentsType: typeof toolCall.function.arguments,
|
923
|
-
argumentsLength: toolCall.function.arguments?.length,
|
924
|
-
argumentsSample: toolCall.function.arguments?.substring(0, 100),
|
925
|
-
})}`);
|
926
|
-
return this.fixQwenDoubleStringification(toolCall);
|
927
|
-
});
|
679
|
+
return 'openai';
|
928
680
|
}
|
929
681
|
/**
|
930
|
-
*
|
931
|
-
* @param
|
932
|
-
* @
|
933
|
-
* @param toolCalls The tool calls to include
|
934
|
-
* @param usageData Optional usage statistics
|
935
|
-
* @returns The message to yield
|
682
|
+
* Parse tool response from API (placeholder for future response parsing)
|
683
|
+
* @param response The raw API response
|
684
|
+
* @returns Parsed tool response
|
936
685
|
*/
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
hasStreamedContent,
|
942
|
-
willSendSpace: hasStreamedContent,
|
943
|
-
});
|
944
|
-
}
|
945
|
-
const shouldOmitContent = hasStreamedContent && this.isUsingQwen() && !isCerebras;
|
946
|
-
this.logger.debug(() => '[Tool Call Handling] Deciding how to yield tool calls', {
|
947
|
-
hasStreamedContent,
|
948
|
-
isUsingQwen: this.isUsingQwen(),
|
949
|
-
isCerebras,
|
950
|
-
shouldOmitContent,
|
951
|
-
fullContentLength: fullContent.length,
|
952
|
-
toolCallCount: toolCalls?.length || 0,
|
953
|
-
});
|
954
|
-
if (shouldOmitContent || (isCerebras && hasStreamedContent)) {
|
955
|
-
// Send just a space to prevent stream stopping or duplication
|
956
|
-
if (isCerebras && hasStreamedContent) {
|
957
|
-
this.logger.debug(() => '[Cerebras] Sending minimal space content to prevent duplication');
|
958
|
-
}
|
959
|
-
return {
|
960
|
-
role: ContentGeneratorRole.ASSISTANT,
|
961
|
-
content: ' ',
|
962
|
-
tool_calls: toolCalls,
|
963
|
-
usage: usageData,
|
964
|
-
};
|
965
|
-
}
|
966
|
-
// Include full content with tool calls
|
967
|
-
return {
|
968
|
-
role: ContentGeneratorRole.ASSISTANT,
|
969
|
-
content: fullContent || '',
|
970
|
-
tool_calls: toolCalls,
|
971
|
-
usage: usageData,
|
972
|
-
};
|
686
|
+
parseToolResponse(response) {
|
687
|
+
// TODO: Implement response parsing based on detected format
|
688
|
+
// For now, return the response as-is
|
689
|
+
return response;
|
973
690
|
}
|
974
691
|
/**
|
975
|
-
*
|
976
|
-
*
|
977
|
-
* @
|
978
|
-
* @returns The fixed tool call or the original if no fix is needed
|
692
|
+
* Determines whether a response should be retried based on error codes
|
693
|
+
* @param error The error object from the API response
|
694
|
+
* @returns true if the request should be retried, false otherwise
|
979
695
|
*/
|
980
|
-
|
981
|
-
if
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
const parsedArgs = JSON.parse(toolCall.function.arguments);
|
988
|
-
let hasNestedStringification = false;
|
989
|
-
// Check each property to see if it's a stringified array/object/number
|
990
|
-
const fixedArgs = {};
|
991
|
-
for (const [key, value] of Object.entries(parsedArgs)) {
|
992
|
-
if (typeof value === 'string') {
|
993
|
-
const trimmed = value.trim();
|
994
|
-
// Check if it's a stringified number (integer or float)
|
995
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
996
|
-
const numValue = trimmed.includes('.')
|
997
|
-
? parseFloat(trimmed)
|
998
|
-
: parseInt(trimmed, 10);
|
999
|
-
fixedArgs[key] = numValue;
|
1000
|
-
hasNestedStringification = true;
|
1001
|
-
this.logger.debug(() => `[Qwen Fix] Fixed stringified number in property '${key}' for ${toolCall.function.name}: "${value}" -> ${numValue}`);
|
1002
|
-
}
|
1003
|
-
// Check if it looks like a stringified array or object
|
1004
|
-
// Also check for Python-style dictionaries with single quotes
|
1005
|
-
else if ((trimmed.startsWith('[') && trimmed.endsWith(']')) ||
|
1006
|
-
(trimmed.startsWith('{') && trimmed.endsWith('}'))) {
|
1007
|
-
try {
|
1008
|
-
// Try to parse it as JSON
|
1009
|
-
const nestedParsed = JSON.parse(value);
|
1010
|
-
fixedArgs[key] = nestedParsed;
|
1011
|
-
hasNestedStringification = true;
|
1012
|
-
this.logger.debug(() => `[Qwen Fix] Fixed nested stringification in property '${key}' for ${toolCall.function.name}`);
|
1013
|
-
}
|
1014
|
-
catch {
|
1015
|
-
// Try to convert Python-style to JSON (single quotes to double quotes)
|
1016
|
-
try {
|
1017
|
-
const jsonified = value
|
1018
|
-
.replace(/'/g, '"')
|
1019
|
-
.replace(/: True/g, ': true')
|
1020
|
-
.replace(/: False/g, ': false')
|
1021
|
-
.replace(/: None/g, ': null');
|
1022
|
-
const nestedParsed = JSON.parse(jsonified);
|
1023
|
-
fixedArgs[key] = nestedParsed;
|
1024
|
-
hasNestedStringification = true;
|
1025
|
-
this.logger.debug(() => `[Qwen Fix] Fixed Python-style nested stringification in property '${key}' for ${toolCall.function.name}`);
|
1026
|
-
}
|
1027
|
-
catch {
|
1028
|
-
// Not valid JSON even after conversion, keep as string
|
1029
|
-
fixedArgs[key] = value;
|
1030
|
-
}
|
1031
|
-
}
|
1032
|
-
}
|
1033
|
-
else {
|
1034
|
-
fixedArgs[key] = value;
|
1035
|
-
}
|
1036
|
-
}
|
1037
|
-
else {
|
1038
|
-
fixedArgs[key] = value;
|
1039
|
-
}
|
1040
|
-
}
|
1041
|
-
if (hasNestedStringification) {
|
1042
|
-
this.logger.debug(() => `[Qwen Fix] Fixed nested double-stringification for ${toolCall.function.name}`);
|
1043
|
-
return {
|
1044
|
-
...toolCall,
|
1045
|
-
function: {
|
1046
|
-
...toolCall.function,
|
1047
|
-
arguments: JSON.stringify(fixedArgs),
|
1048
|
-
},
|
1049
|
-
};
|
1050
|
-
}
|
1051
|
-
}
|
1052
|
-
catch (_e) {
|
1053
|
-
// If parsing fails, check for old-style double-stringification
|
1054
|
-
if (toolCall.function.arguments.startsWith('"') &&
|
1055
|
-
toolCall.function.arguments.endsWith('"')) {
|
1056
|
-
try {
|
1057
|
-
// Old fix: entire arguments were double-stringified
|
1058
|
-
const parsedArgs = JSON.parse(toolCall.function.arguments);
|
1059
|
-
this.logger.debug(() => `[Qwen Fix] Fixed whole-argument double-stringification for ${toolCall.function.name}`);
|
1060
|
-
return {
|
1061
|
-
...toolCall,
|
1062
|
-
function: {
|
1063
|
-
...toolCall.function,
|
1064
|
-
arguments: JSON.stringify(parsedArgs),
|
1065
|
-
},
|
1066
|
-
};
|
1067
|
-
}
|
1068
|
-
catch {
|
1069
|
-
// Leave as-is if we can't parse
|
1070
|
-
}
|
1071
|
-
}
|
696
|
+
shouldRetryResponse(error) {
|
697
|
+
// Don't retry if we're streaming chunks - just continue processing
|
698
|
+
if (error &&
|
699
|
+
typeof error === 'object' &&
|
700
|
+
'status' in error &&
|
701
|
+
error.status === 200) {
|
702
|
+
return false;
|
1072
703
|
}
|
1073
|
-
//
|
1074
|
-
|
1075
|
-
|
704
|
+
// Retry on 429 rate limit errors or 5xx server errors
|
705
|
+
const shouldRetry = Boolean(error &&
|
706
|
+
typeof error === 'object' &&
|
707
|
+
'status' in error &&
|
708
|
+
(error.status === 429 ||
|
709
|
+
(error.status >= 500 &&
|
710
|
+
error.status < 600)));
|
711
|
+
return shouldRetry;
|
1076
712
|
}
|
1077
713
|
}
|
1078
714
|
//# sourceMappingURL=OpenAIProvider.js.map
|