@vybestack/llxprt-code-core 0.1.23-nightly.250905.97906524 → 0.2.2
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 +535 -948
- 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/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 +159 -37
- 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/write-file.js +5 -2
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/types/modelParams.d.ts +8 -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,157 @@
|
|
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';
|
28
29
|
export class OpenAIProvider extends BaseProvider {
|
30
|
+
name = 'openai';
|
29
31
|
logger;
|
30
|
-
openai;
|
31
|
-
currentModel = process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
|
32
|
-
baseURL;
|
33
|
-
providerConfig;
|
34
32
|
toolFormatter;
|
35
|
-
toolFormatOverride;
|
36
|
-
modelParams;
|
37
33
|
_cachedClient;
|
38
34
|
_cachedClientKey;
|
39
35
|
constructor(apiKey, baseURL, config, oauthManager) {
|
36
|
+
// Normalize empty string to undefined for proper precedence handling
|
37
|
+
const normalizedApiKey = apiKey && apiKey.trim() !== '' ? apiKey : undefined;
|
38
|
+
// Detect if this is a Qwen endpoint
|
39
|
+
const isQwenEndpoint = !!(baseURL &&
|
40
|
+
(baseURL.includes('dashscope.aliyuncs.com') ||
|
41
|
+
baseURL.includes('api.qwen.com') ||
|
42
|
+
baseURL.includes('qwen')));
|
40
43
|
// 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 = {
|
44
|
+
super({
|
60
45
|
name: 'openai',
|
61
|
-
apiKey,
|
46
|
+
apiKey: normalizedApiKey,
|
62
47
|
baseURL,
|
63
|
-
envKeyNames: ['OPENAI_API_KEY'],
|
64
|
-
isOAuthEnabled:
|
65
|
-
oauthProvider:
|
48
|
+
envKeyNames: ['OPENAI_API_KEY'], // Support environment variable fallback
|
49
|
+
isOAuthEnabled: isQwenEndpoint && !!oauthManager,
|
50
|
+
oauthProvider: isQwenEndpoint ? 'qwen' : undefined,
|
66
51
|
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;
|
52
|
+
}, config);
|
73
53
|
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);
|
54
|
+
// new DebugLogger('llxprt:core:toolformatter'), // TODO: Fix ToolFormatter constructor
|
55
|
+
// Setup debug logger
|
56
|
+
this.logger = new DebugLogger('llxprt:provider:openai');
|
112
57
|
}
|
113
58
|
/**
|
114
|
-
*
|
59
|
+
* Create HTTP/HTTPS agents with socket configuration for local AI servers
|
60
|
+
* Returns undefined if no socket settings are configured
|
115
61
|
*/
|
116
|
-
|
117
|
-
//
|
118
|
-
const
|
119
|
-
|
62
|
+
createHttpAgents() {
|
63
|
+
// Get socket configuration from ephemeral settings
|
64
|
+
const settings = this.providerConfig?.getEphemeralSettings?.() || {};
|
65
|
+
// Check if any socket settings are explicitly configured
|
66
|
+
const hasSocketSettings = 'socket-timeout' in settings ||
|
67
|
+
'socket-keepalive' in settings ||
|
68
|
+
'socket-nodelay' in settings;
|
69
|
+
// Only create custom agents if socket settings are configured
|
70
|
+
if (!hasSocketSettings) {
|
71
|
+
return undefined;
|
72
|
+
}
|
73
|
+
// Socket configuration with defaults for when settings ARE configured
|
74
|
+
const socketTimeout = settings['socket-timeout'] || 60000; // 60 seconds default
|
75
|
+
const socketKeepAlive = settings['socket-keepalive'] !== false; // true by default
|
76
|
+
const socketNoDelay = settings['socket-nodelay'] !== false; // true by default
|
77
|
+
// Create HTTP agent with socket options
|
78
|
+
const httpAgent = new http.Agent({
|
79
|
+
keepAlive: socketKeepAlive,
|
80
|
+
keepAliveMsecs: 1000,
|
81
|
+
timeout: socketTimeout,
|
82
|
+
});
|
83
|
+
// Create HTTPS agent with socket options
|
84
|
+
const httpsAgent = new https.Agent({
|
85
|
+
keepAlive: socketKeepAlive,
|
86
|
+
keepAliveMsecs: 1000,
|
87
|
+
timeout: socketTimeout,
|
88
|
+
});
|
89
|
+
// Apply TCP_NODELAY if enabled (reduces latency for local servers)
|
90
|
+
if (socketNoDelay) {
|
91
|
+
const originalCreateConnection = httpAgent.createConnection;
|
92
|
+
httpAgent.createConnection = function (options, callback) {
|
93
|
+
const socket = originalCreateConnection.call(this, options, callback);
|
94
|
+
if (socket instanceof net.Socket) {
|
95
|
+
socket.setNoDelay(true);
|
96
|
+
}
|
97
|
+
return socket;
|
98
|
+
};
|
99
|
+
const originalHttpsCreateConnection = httpsAgent.createConnection;
|
100
|
+
httpsAgent.createConnection = function (options, callback) {
|
101
|
+
const socket = originalHttpsCreateConnection.call(this, options, callback);
|
102
|
+
if (socket instanceof net.Socket) {
|
103
|
+
socket.setNoDelay(true);
|
104
|
+
}
|
105
|
+
return socket;
|
106
|
+
};
|
107
|
+
}
|
108
|
+
return { httpAgent, httpsAgent };
|
120
109
|
}
|
121
110
|
/**
|
122
|
-
*
|
111
|
+
* Get or create OpenAI client instance
|
112
|
+
* Will use the API key from resolved auth
|
113
|
+
* @returns OpenAI client instance
|
123
114
|
*/
|
124
|
-
async
|
115
|
+
async getClient() {
|
125
116
|
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;
|
117
|
+
// Use the unified getBaseURL() method from BaseProvider
|
118
|
+
const baseURL = this.getBaseURL();
|
119
|
+
const clientKey = `${baseURL}-${resolvedKey}`;
|
120
|
+
// Return cached client if available and auth hasn't changed
|
121
|
+
if (this._cachedClient && this._cachedClientKey === clientKey) {
|
122
|
+
return this._cachedClient;
|
123
|
+
}
|
124
|
+
// Create HTTP agents with socket configuration (if configured)
|
125
|
+
const agents = this.createHttpAgents();
|
126
|
+
// Build client options - OpenAI SDK accepts httpAgent/httpsAgent at runtime
|
127
|
+
// even though they're not in the TypeScript definitions
|
128
|
+
const baseOptions = {
|
129
|
+
apiKey: resolvedKey || '',
|
130
|
+
baseURL,
|
131
|
+
};
|
132
|
+
// Add socket configuration if available
|
133
|
+
const clientOptions = agents
|
134
|
+
? {
|
135
|
+
...baseOptions,
|
136
|
+
httpAgent: agents.httpAgent,
|
137
|
+
httpsAgent: agents.httpsAgent,
|
170
138
|
}
|
171
|
-
|
139
|
+
: baseOptions;
|
140
|
+
// Create new client with current auth and optional socket configuration
|
141
|
+
// Cast to unknown then to the expected type to bypass TypeScript's structural checking
|
142
|
+
this._cachedClient = new OpenAI(clientOptions);
|
143
|
+
this._cachedClientKey = clientKey;
|
144
|
+
return this._cachedClient;
|
172
145
|
}
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
const
|
179
|
-
|
180
|
-
if (
|
146
|
+
/**
|
147
|
+
* Check if OAuth is supported for this provider
|
148
|
+
* Qwen endpoints support OAuth, standard OpenAI does not
|
149
|
+
*/
|
150
|
+
supportsOAuth() {
|
151
|
+
const baseURL = this.getBaseURL();
|
152
|
+
// Check if this is a Qwen endpoint that supports OAuth
|
153
|
+
if (baseURL &&
|
154
|
+
(baseURL.includes('dashscope.aliyuncs.com') ||
|
155
|
+
baseURL.includes('api.qwen.com') ||
|
156
|
+
baseURL.includes('qwen'))) {
|
181
157
|
return true;
|
182
158
|
}
|
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';
|
159
|
+
// Standard OpenAI endpoints don't support OAuth
|
160
|
+
return false;
|
206
161
|
}
|
207
162
|
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
163
|
try {
|
218
|
-
//
|
219
|
-
|
220
|
-
const
|
164
|
+
// Always try to fetch models, regardless of auth status
|
165
|
+
// Local endpoints often work without authentication
|
166
|
+
const client = await this.getClient();
|
167
|
+
const response = await client.models.list();
|
221
168
|
const models = [];
|
222
169
|
for await (const model of response) {
|
223
|
-
// Filter out non-chat models (embeddings, audio, image,
|
170
|
+
// Filter out non-chat models (embeddings, audio, image, vision, DALL·E, etc.)
|
224
171
|
if (!/embedding|whisper|audio|tts|image|vision|dall[- ]?e|moderation/i.test(model.id)) {
|
225
172
|
models.push({
|
226
173
|
id: model.id,
|
@@ -235,844 +182,484 @@ export class OpenAIProvider extends BaseProvider {
|
|
235
182
|
catch (error) {
|
236
183
|
this.logger.debug(() => `Error fetching models from OpenAI: ${error}`);
|
237
184
|
// 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();
|
185
|
+
return this.getFallbackModels();
|
186
|
+
}
|
187
|
+
}
|
188
|
+
getFallbackModels() {
|
189
|
+
return [
|
190
|
+
{
|
191
|
+
id: 'gpt-5',
|
192
|
+
name: 'GPT-5',
|
193
|
+
provider: 'openai',
|
194
|
+
supportedToolFormats: ['openai'],
|
195
|
+
},
|
196
|
+
{
|
197
|
+
id: 'gpt-4.1',
|
198
|
+
name: 'GPT-4.1',
|
199
|
+
provider: 'openai',
|
200
|
+
supportedToolFormats: ['openai'],
|
201
|
+
},
|
202
|
+
{
|
203
|
+
id: 'gpt-4o',
|
204
|
+
name: 'GPT-4o',
|
205
|
+
provider: 'openai',
|
206
|
+
supportedToolFormats: ['openai'],
|
207
|
+
},
|
208
|
+
{
|
209
|
+
id: 'o3',
|
210
|
+
name: 'O3',
|
211
|
+
provider: 'openai',
|
212
|
+
supportedToolFormats: ['openai'],
|
213
|
+
},
|
214
|
+
{
|
215
|
+
id: 'o4-mini',
|
216
|
+
name: 'O4 Mini',
|
217
|
+
provider: 'openai',
|
218
|
+
supportedToolFormats: ['openai'],
|
219
|
+
},
|
220
|
+
{
|
221
|
+
id: 'gpt-3.5-turbo',
|
222
|
+
name: 'GPT-3.5 Turbo (Legacy)',
|
223
|
+
provider: 'openai',
|
224
|
+
supportedToolFormats: ['openai'],
|
225
|
+
},
|
226
|
+
];
|
418
227
|
}
|
419
228
|
getDefaultModel() {
|
420
|
-
// Return
|
421
|
-
// This can be overridden based on configuration or endpoint
|
422
|
-
if (this.isUsingQwen()) {
|
423
|
-
return 'qwen3-coder-plus';
|
424
|
-
}
|
229
|
+
// Return hardcoded default - do NOT call getModel() to avoid circular dependency
|
425
230
|
return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
|
426
231
|
}
|
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
232
|
getServerTools() {
|
233
|
+
// TODO: Implement server tools for OpenAI provider
|
494
234
|
return [];
|
495
235
|
}
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
async invokeServerTool(_toolName, _params, _config) {
|
500
|
-
throw new Error('Server tools not supported by OpenAI provider');
|
236
|
+
async invokeServerTool(toolName, _params, _config) {
|
237
|
+
// TODO: Implement server tool invocation for OpenAI provider
|
238
|
+
throw new Error(`Server tool '${toolName}' not supported by OpenAI provider`);
|
501
239
|
}
|
502
240
|
/**
|
503
|
-
*
|
504
|
-
*
|
241
|
+
* Normalize tool IDs from various formats to OpenAI format
|
242
|
+
* Handles IDs from OpenAI (call_xxx), Anthropic (toolu_xxx), and history (hist_tool_xxx)
|
505
243
|
*/
|
506
|
-
|
507
|
-
|
508
|
-
|
244
|
+
normalizeToOpenAIToolId(id) {
|
245
|
+
// If already in OpenAI format, return as-is
|
246
|
+
if (id.startsWith('call_')) {
|
247
|
+
return id;
|
509
248
|
}
|
510
|
-
|
511
|
-
|
249
|
+
// For history format, extract the UUID and add OpenAI prefix
|
250
|
+
if (id.startsWith('hist_tool_')) {
|
251
|
+
const uuid = id.substring('hist_tool_'.length);
|
252
|
+
return 'call_' + uuid;
|
512
253
|
}
|
513
|
-
//
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
}
|
518
|
-
/**
|
519
|
-
* Get current model parameters
|
520
|
-
* @returns Current parameters or undefined if not set
|
521
|
-
*/
|
522
|
-
getModelParams() {
|
523
|
-
return this.modelParams;
|
524
|
-
}
|
525
|
-
/**
|
526
|
-
* Initialize provider configuration from SettingsService
|
527
|
-
*/
|
528
|
-
async initializeFromSettings() {
|
529
|
-
try {
|
530
|
-
// Load saved model if available
|
531
|
-
const savedModel = await this.getModelFromSettings();
|
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)}`);
|
254
|
+
// For Anthropic format, extract the UUID and add OpenAI prefix
|
255
|
+
if (id.startsWith('toolu_')) {
|
256
|
+
const uuid = id.substring('toolu_'.length);
|
257
|
+
return 'call_' + uuid;
|
546
258
|
}
|
547
|
-
|
548
|
-
|
549
|
-
}
|
550
|
-
}
|
551
|
-
/**
|
552
|
-
* Check if the provider is authenticated using any available method
|
553
|
-
* Uses the base provider's isAuthenticated implementation
|
554
|
-
*/
|
555
|
-
async isAuthenticated() {
|
556
|
-
return super.isAuthenticated();
|
259
|
+
// Unknown format - assume it's a raw UUID
|
260
|
+
return 'call_' + id;
|
557
261
|
}
|
558
262
|
/**
|
559
|
-
*
|
560
|
-
* @returns The detected tool format
|
263
|
+
* Normalize tool IDs from OpenAI format to history format
|
561
264
|
*/
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
// Note: This is synchronous access to cached settings, not async
|
567
|
-
const currentSettings = settingsService['settings'];
|
568
|
-
const providerSettings = currentSettings?.providers?.[this.name];
|
569
|
-
const toolFormatOverride = providerSettings?.toolFormat;
|
570
|
-
// If explicitly set to a specific format (not 'auto'), use it
|
571
|
-
if (toolFormatOverride && toolFormatOverride !== 'auto') {
|
572
|
-
return toolFormatOverride;
|
573
|
-
}
|
574
|
-
// Auto-detect based on model name if set to 'auto' or not set
|
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';
|
583
|
-
}
|
584
|
-
// Default to 'openai' format
|
585
|
-
return 'openai';
|
265
|
+
normalizeToHistoryToolId(id) {
|
266
|
+
// If already in history format, return as-is
|
267
|
+
if (id.startsWith('hist_tool_')) {
|
268
|
+
return id;
|
586
269
|
}
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {
|
592
|
-
return 'qwen';
|
593
|
-
}
|
594
|
-
if (modelName.includes('qwen')) {
|
595
|
-
return 'qwen';
|
596
|
-
}
|
597
|
-
return 'openai';
|
270
|
+
// For OpenAI format, extract the UUID and add history prefix
|
271
|
+
if (id.startsWith('call_')) {
|
272
|
+
const uuid = id.substring('call_'.length);
|
273
|
+
return 'hist_tool_' + uuid;
|
598
274
|
}
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
* @returns Appropriate tool_choice value for the current format
|
604
|
-
*/
|
605
|
-
getToolChoiceForFormat(tools) {
|
606
|
-
if (!tools || tools.length === 0) {
|
607
|
-
return undefined;
|
275
|
+
// For Anthropic format, extract the UUID and add history prefix
|
276
|
+
if (id.startsWith('toolu_')) {
|
277
|
+
const uuid = id.substring('toolu_'.length);
|
278
|
+
return 'hist_tool_' + uuid;
|
608
279
|
}
|
609
|
-
//
|
610
|
-
|
611
|
-
return 'auto';
|
280
|
+
// Unknown format - assume it's a raw UUID
|
281
|
+
return 'hist_tool_' + id;
|
612
282
|
}
|
613
283
|
/**
|
614
|
-
*
|
615
|
-
*
|
616
|
-
* @
|
284
|
+
* Generate chat completion with IContent interface
|
285
|
+
* Internally converts to OpenAI API format, but only yields IContent
|
286
|
+
* @param contents Array of content blocks (text and tool_call)
|
287
|
+
* @param tools Array of available tools
|
617
288
|
*/
|
618
|
-
|
619
|
-
//
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
//
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
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;
|
289
|
+
async *generateChatCompletion(contents, tools) {
|
290
|
+
// Debug log what we receive
|
291
|
+
this.logger.debug(() => `[OpenAIProvider] generateChatCompletion received tools:`, {
|
292
|
+
hasTools: !!tools,
|
293
|
+
toolsLength: tools?.length,
|
294
|
+
toolsType: typeof tools,
|
295
|
+
isArray: Array.isArray(tools),
|
296
|
+
firstToolName: tools?.[0]?.functionDeclarations?.[0]?.name,
|
297
|
+
toolsStructure: tools
|
298
|
+
? JSON.stringify(tools).substring(0, 200)
|
299
|
+
: 'undefined',
|
300
|
+
});
|
301
|
+
// Pass tools directly in Gemini format - they'll be converted in generateChatCompletionImpl
|
302
|
+
const generator = this.generateChatCompletionImpl(contents, tools, undefined, undefined, undefined);
|
303
|
+
for await (const item of generator) {
|
304
|
+
yield item;
|
305
|
+
}
|
642
306
|
}
|
643
307
|
/**
|
644
|
-
*
|
308
|
+
* Convert IContent array to OpenAI ChatCompletionMessageParam array
|
645
309
|
*/
|
646
|
-
|
647
|
-
|
648
|
-
const
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
310
|
+
convertToOpenAIMessages(contents) {
|
311
|
+
const messages = [];
|
312
|
+
for (const content of contents) {
|
313
|
+
if (content.speaker === 'human') {
|
314
|
+
// Convert human messages to user messages
|
315
|
+
const textBlocks = content.blocks.filter((b) => b.type === 'text');
|
316
|
+
const text = textBlocks.map((b) => b.text).join('\n');
|
317
|
+
if (text) {
|
318
|
+
messages.push({
|
319
|
+
role: 'user',
|
320
|
+
content: text,
|
321
|
+
});
|
322
|
+
}
|
323
|
+
}
|
324
|
+
else if (content.speaker === 'ai') {
|
325
|
+
// Convert AI messages
|
326
|
+
const textBlocks = content.blocks.filter((b) => b.type === 'text');
|
327
|
+
const toolCalls = content.blocks.filter((b) => b.type === 'tool_call');
|
328
|
+
if (toolCalls.length > 0) {
|
329
|
+
// Assistant message with tool calls
|
330
|
+
const text = textBlocks.map((b) => b.text).join('\n');
|
331
|
+
messages.push({
|
332
|
+
role: 'assistant',
|
333
|
+
content: text || null,
|
334
|
+
tool_calls: toolCalls.map((tc) => ({
|
335
|
+
id: this.normalizeToOpenAIToolId(tc.id),
|
336
|
+
type: 'function',
|
337
|
+
function: {
|
338
|
+
name: tc.name,
|
339
|
+
arguments: typeof tc.parameters === 'string'
|
340
|
+
? tc.parameters
|
341
|
+
: JSON.stringify(tc.parameters),
|
342
|
+
},
|
343
|
+
})),
|
344
|
+
});
|
345
|
+
}
|
346
|
+
else if (textBlocks.length > 0) {
|
347
|
+
// Plain assistant message
|
348
|
+
const text = textBlocks.map((b) => b.text).join('\n');
|
349
|
+
messages.push({
|
350
|
+
role: 'assistant',
|
351
|
+
content: text,
|
352
|
+
});
|
353
|
+
}
|
354
|
+
}
|
355
|
+
else if (content.speaker === 'tool') {
|
356
|
+
// Convert tool responses
|
357
|
+
const toolResponses = content.blocks.filter((b) => b.type === 'tool_response');
|
358
|
+
for (const tr of toolResponses) {
|
359
|
+
messages.push({
|
360
|
+
role: 'tool',
|
361
|
+
content: typeof tr.result === 'string'
|
362
|
+
? tr.result
|
363
|
+
: JSON.stringify(tr.result),
|
364
|
+
tool_call_id: this.normalizeToOpenAIToolId(tr.callId),
|
365
|
+
});
|
366
|
+
}
|
653
367
|
}
|
654
|
-
throw new Error('OpenAI API key is required to generate completions');
|
655
|
-
}
|
656
|
-
// Validate tool messages have required tool_call_id
|
657
|
-
const toolMessages = messages.filter((msg) => msg.role === 'tool');
|
658
|
-
const missingIds = toolMessages.filter((msg) => !msg.tool_call_id);
|
659
|
-
if (missingIds.length > 0) {
|
660
|
-
this.logger.error(() => `FATAL: Tool messages missing tool_call_id: ${JSON.stringify(missingIds)}`);
|
661
|
-
throw new Error(`OpenAI API requires tool_call_id for all tool messages. Found ${missingIds.length} tool message(s) without IDs.`);
|
662
368
|
}
|
369
|
+
return messages;
|
663
370
|
}
|
664
371
|
/**
|
665
|
-
*
|
372
|
+
* Internal implementation for chat completion
|
666
373
|
*/
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
374
|
+
async *generateChatCompletionImpl(contents, tools, maxTokens, abortSignal, modelName) {
|
375
|
+
// Always look up model from SettingsService
|
376
|
+
const model = modelName || this.getModel() || this.getDefaultModel();
|
377
|
+
// Convert IContent to OpenAI messages format
|
378
|
+
const messages = this.convertToOpenAIMessages(contents);
|
379
|
+
// Debug log what we're about to convert
|
380
|
+
this.logger.debug(() => `[OpenAIProvider] Before convertGeminiToOpenAI:`, {
|
381
|
+
inputTools: tools ? JSON.stringify(tools).substring(0, 500) : 'undefined',
|
382
|
+
hasTools: !!tools,
|
383
|
+
toolsLength: tools?.length,
|
384
|
+
firstToolStructure: tools?.[0]
|
385
|
+
? JSON.stringify(tools[0]).substring(0, 300)
|
386
|
+
: 'undefined',
|
387
|
+
});
|
388
|
+
// Convert Gemini format tools directly to OpenAI format using the new method
|
389
|
+
const formattedTools = this.toolFormatter.convertGeminiToOpenAI(tools);
|
390
|
+
// Debug log the conversion result
|
391
|
+
this.logger.debug(() => `[OpenAIProvider] After convertGeminiToOpenAI:`, {
|
392
|
+
inputHadTools: !!tools,
|
393
|
+
outputHasTools: !!formattedTools,
|
394
|
+
outputToolsLength: formattedTools?.length,
|
395
|
+
outputFirstTool: formattedTools?.[0],
|
396
|
+
outputToolNames: formattedTools?.map((t) => t.function.name),
|
397
|
+
firstToolParameters: formattedTools?.[0]
|
398
|
+
? JSON.stringify(formattedTools[0].function.parameters)
|
399
|
+
: 'undefined',
|
400
|
+
});
|
679
401
|
// Get streaming setting from ephemeral settings (default: enabled)
|
680
402
|
const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
|
681
403
|
const streamingEnabled = streamingSetting !== 'disabled';
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
404
|
+
// Get the system prompt
|
405
|
+
const userMemory = this.globalConfig?.getUserMemory
|
406
|
+
? this.globalConfig.getUserMemory()
|
407
|
+
: '';
|
408
|
+
const systemPrompt = await getCoreSystemPromptAsync(userMemory, model, undefined);
|
409
|
+
// Add system prompt as the first message in the array
|
410
|
+
const messagesWithSystem = [
|
411
|
+
{ role: 'system', content: systemPrompt },
|
412
|
+
...messages,
|
413
|
+
];
|
414
|
+
// Build request - only include tools if they exist and are not empty
|
415
|
+
const requestBody = {
|
416
|
+
model,
|
417
|
+
messages: messagesWithSystem,
|
418
|
+
...(formattedTools && formattedTools.length > 0
|
419
|
+
? { tools: formattedTools }
|
420
|
+
: {}),
|
421
|
+
max_tokens: maxTokens,
|
422
|
+
stream: streamingEnabled,
|
688
423
|
};
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
...(requestConfig.streamingEnabled && requestConfig.finalStreamOptions
|
706
|
-
? { stream_options: requestConfig.finalStreamOptions }
|
707
|
-
: {}),
|
708
|
-
tools: requestConfig.formattedTools,
|
709
|
-
tool_choice: this.getToolChoiceForFormat(tools),
|
710
|
-
...this.modelParams,
|
424
|
+
// Debug log the full request for Cerebras/Qwen
|
425
|
+
if (model.toLowerCase().includes('qwen') ||
|
426
|
+
this.getBaseURL()?.includes('cerebras')) {
|
427
|
+
this.logger.debug(() => `Full request to ${this.getBaseURL()} for model ${model}:`, {
|
428
|
+
baseURL: this.getBaseURL(),
|
429
|
+
model,
|
430
|
+
streamingEnabled,
|
431
|
+
hasTools: 'tools' in requestBody,
|
432
|
+
toolCount: formattedTools?.length || 0,
|
433
|
+
messageCount: messages.length,
|
434
|
+
toolsInRequest: 'tools' in requestBody ? requestBody.tools?.length : 'not included',
|
435
|
+
requestBody: {
|
436
|
+
...requestBody,
|
437
|
+
messages: messages.slice(-2), // Only log last 2 messages for brevity
|
438
|
+
tools: requestBody.tools?.slice(0, 2), // Only log first 2 tools for brevity if they exist
|
439
|
+
},
|
711
440
|
});
|
712
441
|
}
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
* Handle and log API errors
|
720
|
-
*/
|
721
|
-
handleApiError(error, messages) {
|
722
|
-
const errorStatus = error?.status ||
|
723
|
-
error?.response?.status;
|
724
|
-
const errorLabel = errorStatus === 400 ? '[API Error 400]' : '[API Error]';
|
725
|
-
this.logger.error(() => `${errorLabel} Error caught in API call:\n` +
|
726
|
-
` Error: ${error}\n` +
|
727
|
-
` Type: ${error?.constructor?.name}\n` +
|
728
|
-
` Status: ${errorStatus}\n` +
|
729
|
-
` Response data: ${JSON.stringify(error?.response?.data, null, 2)}`);
|
730
|
-
// Log the last few messages to understand what's being sent
|
731
|
-
if (errorStatus === 400) {
|
732
|
-
// Log additional diagnostics for 400 errors
|
733
|
-
const hasPendingToolCalls = messages.some((msg, idx) => {
|
734
|
-
if (msg.role === 'assistant' && msg.tool_calls) {
|
735
|
-
// Check if there's a matching tool response
|
736
|
-
const toolCallIds = msg.tool_calls.map((tc) => tc.id);
|
737
|
-
const hasResponses = toolCallIds.every((id) => messages
|
738
|
-
.slice(idx + 1)
|
739
|
-
.some((m) => m.role === 'tool' && m.tool_call_id === id));
|
740
|
-
return !hasResponses;
|
741
|
-
}
|
742
|
-
return false;
|
442
|
+
// Get OpenAI client
|
443
|
+
const client = await this.getClient();
|
444
|
+
// Wrap the API call with retry logic
|
445
|
+
const makeApiCall = async () => {
|
446
|
+
const response = await client.chat.completions.create(requestBody, {
|
447
|
+
signal: abortSignal,
|
743
448
|
});
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
449
|
+
return response;
|
450
|
+
};
|
451
|
+
let retryCount = 0;
|
452
|
+
const maxRetries = 5;
|
453
|
+
let response;
|
454
|
+
while (retryCount <= maxRetries) {
|
455
|
+
try {
|
456
|
+
response = await makeApiCall();
|
457
|
+
break; // Success, exit retry loop
|
458
|
+
}
|
459
|
+
catch (error) {
|
460
|
+
if (retryCount === maxRetries) {
|
461
|
+
throw error; // Max retries reached, re-throw error
|
753
462
|
}
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
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;
|
463
|
+
retryCount++;
|
464
|
+
this.logger.debug(() => `API call failed (attempt ${retryCount}), retrying...`, error);
|
465
|
+
// Exponential backoff: 4s, 8s, 16s, 32s, 64s
|
466
|
+
const delay = 4000 * Math.pow(2, retryCount - 1);
|
467
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
468
|
+
}
|
767
469
|
}
|
768
|
-
if (
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
470
|
+
if (!response) {
|
471
|
+
throw new Error('Failed to get response after retries');
|
472
|
+
}
|
473
|
+
// Check if response is streaming or not
|
474
|
+
if (streamingEnabled) {
|
475
|
+
// Process streaming response
|
476
|
+
let _accumulatedText = '';
|
477
|
+
const accumulatedToolCalls = [];
|
478
|
+
try {
|
479
|
+
// Handle streaming response
|
480
|
+
for await (const chunk of response) {
|
481
|
+
if (abortSignal?.aborted) {
|
482
|
+
break;
|
483
|
+
}
|
484
|
+
const choice = chunk.choices?.[0];
|
485
|
+
if (!choice)
|
486
|
+
continue;
|
487
|
+
// Handle text content - emit immediately without buffering
|
488
|
+
const deltaContent = choice.delta?.content;
|
489
|
+
if (deltaContent) {
|
490
|
+
_accumulatedText += deltaContent;
|
491
|
+
// Emit text immediately without buffering
|
492
|
+
yield {
|
493
|
+
speaker: 'ai',
|
494
|
+
blocks: [
|
495
|
+
{
|
496
|
+
type: 'text',
|
497
|
+
text: deltaContent,
|
498
|
+
},
|
499
|
+
],
|
500
|
+
};
|
501
|
+
}
|
502
|
+
// Handle tool calls
|
503
|
+
const deltaToolCalls = choice.delta?.tool_calls;
|
504
|
+
if (deltaToolCalls && deltaToolCalls.length > 0) {
|
505
|
+
for (const deltaToolCall of deltaToolCalls) {
|
506
|
+
if (deltaToolCall.index === undefined)
|
507
|
+
continue;
|
508
|
+
// Initialize or update accumulated tool call
|
509
|
+
if (!accumulatedToolCalls[deltaToolCall.index]) {
|
510
|
+
accumulatedToolCalls[deltaToolCall.index] = {
|
511
|
+
id: deltaToolCall.id || '',
|
512
|
+
type: 'function',
|
513
|
+
function: {
|
514
|
+
name: deltaToolCall.function?.name || '',
|
515
|
+
arguments: '',
|
516
|
+
},
|
517
|
+
};
|
518
|
+
}
|
519
|
+
const tc = accumulatedToolCalls[deltaToolCall.index];
|
520
|
+
if (tc) {
|
521
|
+
if (deltaToolCall.id)
|
522
|
+
tc.id = deltaToolCall.id;
|
523
|
+
if (deltaToolCall.function?.name)
|
524
|
+
tc.function.name = deltaToolCall.function.name;
|
525
|
+
if (deltaToolCall.function?.arguments) {
|
526
|
+
tc.function.arguments += deltaToolCall.function.arguments;
|
527
|
+
}
|
528
|
+
}
|
529
|
+
}
|
530
|
+
}
|
778
531
|
}
|
779
532
|
}
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
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
|
-
}));
|
820
|
-
yield {
|
821
|
-
role: ContentGeneratorRole.ASSISTANT,
|
822
|
-
content: cleanedContent,
|
823
|
-
tool_calls: standardToolCalls,
|
824
|
-
usage: usageData,
|
825
|
-
};
|
533
|
+
catch (error) {
|
534
|
+
if (abortSignal?.aborted) {
|
535
|
+
throw error;
|
536
|
+
}
|
537
|
+
else {
|
538
|
+
this.logger.error('Error processing streaming response:', error);
|
539
|
+
throw error;
|
540
|
+
}
|
826
541
|
}
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
542
|
+
// No need to flush buffer since we're emitting immediately
|
543
|
+
// Emit accumulated tool calls as IContent if any
|
544
|
+
if (accumulatedToolCalls.length > 0) {
|
545
|
+
const blocks = [];
|
546
|
+
const detectedFormat = this.detectToolFormat();
|
547
|
+
for (const tc of accumulatedToolCalls) {
|
548
|
+
if (!tc)
|
549
|
+
continue;
|
550
|
+
// Process tool parameters with double-escape handling
|
551
|
+
const processedParameters = processToolParameters(tc.function.arguments || '', tc.function.name || '', detectedFormat);
|
552
|
+
blocks.push({
|
553
|
+
type: 'tool_call',
|
554
|
+
id: this.normalizeToHistoryToolId(tc.id),
|
555
|
+
name: tc.function.name || '',
|
556
|
+
parameters: processedParameters,
|
557
|
+
});
|
558
|
+
}
|
559
|
+
if (blocks.length > 0) {
|
560
|
+
yield {
|
561
|
+
speaker: 'ai',
|
562
|
+
blocks,
|
563
|
+
};
|
564
|
+
}
|
834
565
|
}
|
835
566
|
}
|
836
567
|
else {
|
837
|
-
//
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
568
|
+
// Handle non-streaming response
|
569
|
+
const completion = response;
|
570
|
+
const choice = completion.choices?.[0];
|
571
|
+
if (!choice) {
|
572
|
+
throw new Error('No choices in completion response');
|
573
|
+
}
|
574
|
+
const blocks = [];
|
575
|
+
// Handle text content
|
576
|
+
if (choice.message?.content) {
|
577
|
+
blocks.push({
|
578
|
+
type: 'text',
|
579
|
+
text: choice.message.content,
|
580
|
+
});
|
581
|
+
}
|
582
|
+
// Handle tool calls
|
583
|
+
if (choice.message?.tool_calls && choice.message.tool_calls.length > 0) {
|
584
|
+
const detectedFormat = this.detectToolFormat();
|
585
|
+
for (const toolCall of choice.message.tool_calls) {
|
586
|
+
if (toolCall.type === 'function') {
|
587
|
+
// Process tool parameters with double-escape handling
|
588
|
+
const processedParameters = processToolParameters(toolCall.function.arguments || '', toolCall.function.name || '', detectedFormat);
|
589
|
+
blocks.push({
|
590
|
+
type: 'tool_call',
|
591
|
+
id: this.normalizeToHistoryToolId(toolCall.id),
|
592
|
+
name: toolCall.function.name || '',
|
593
|
+
parameters: processedParameters,
|
594
|
+
});
|
595
|
+
}
|
849
596
|
}
|
850
|
-
// Build the final message based on provider-specific requirements
|
851
|
-
const finalMessage = this.buildFinalToolCallMessage(hasStreamedContent, finalFullContent, fixedToolCalls, usageData);
|
852
|
-
yield finalMessage;
|
853
597
|
}
|
854
|
-
|
855
|
-
|
598
|
+
// Emit the complete response as a single IContent
|
599
|
+
if (blocks.length > 0) {
|
856
600
|
yield {
|
857
|
-
|
858
|
-
|
859
|
-
usage: usageData,
|
601
|
+
speaker: 'ai',
|
602
|
+
blocks,
|
860
603
|
};
|
861
604
|
}
|
862
605
|
}
|
863
606
|
}
|
864
607
|
/**
|
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
|
608
|
+
* Detects the tool call format based on the model being used
|
609
|
+
* @returns The detected tool format ('openai' or 'qwen')
|
870
610
|
*/
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
};
|
611
|
+
detectToolFormat() {
|
612
|
+
try {
|
613
|
+
// Try to get format from SettingsService if available
|
614
|
+
const settings = this.providerConfig?.getEphemeralSettings?.();
|
615
|
+
if (settings && settings['tool-format']) {
|
616
|
+
return settings['tool-format'];
|
617
|
+
}
|
879
618
|
}
|
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
|
-
};
|
619
|
+
catch (error) {
|
620
|
+
this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
|
891
621
|
}
|
892
|
-
//
|
893
|
-
|
894
|
-
|
895
|
-
return
|
896
|
-
shouldYield: true,
|
897
|
-
content: pendingWhitespace + delta.content,
|
898
|
-
updatedPendingWhitespace: null,
|
899
|
-
updatedFullContent: fullContent + pendingWhitespace + delta.content,
|
900
|
-
};
|
622
|
+
// Fallback detection without SettingsService - always look up current model
|
623
|
+
const modelName = (this.getModel() || this.getDefaultModel()).toLowerCase();
|
624
|
+
if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {
|
625
|
+
return 'qwen';
|
901
626
|
}
|
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;
|
627
|
+
if (modelName.includes('qwen')) {
|
628
|
+
return 'qwen';
|
917
629
|
}
|
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
|
-
});
|
630
|
+
return 'openai';
|
928
631
|
}
|
929
632
|
/**
|
930
|
-
*
|
931
|
-
* @param
|
932
|
-
* @
|
933
|
-
* @param toolCalls The tool calls to include
|
934
|
-
* @param usageData Optional usage statistics
|
935
|
-
* @returns The message to yield
|
633
|
+
* Parse tool response from API (placeholder for future response parsing)
|
634
|
+
* @param response The raw API response
|
635
|
+
* @returns Parsed tool response
|
936
636
|
*/
|
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
|
-
};
|
637
|
+
parseToolResponse(response) {
|
638
|
+
// TODO: Implement response parsing based on detected format
|
639
|
+
// For now, return the response as-is
|
640
|
+
return response;
|
973
641
|
}
|
974
642
|
/**
|
975
|
-
*
|
976
|
-
*
|
977
|
-
* @
|
978
|
-
* @returns The fixed tool call or the original if no fix is needed
|
643
|
+
* Determines whether a response should be retried based on error codes
|
644
|
+
* @param error The error object from the API response
|
645
|
+
* @returns true if the request should be retried, false otherwise
|
979
646
|
*/
|
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
|
-
}
|
647
|
+
shouldRetryResponse(error) {
|
648
|
+
// Don't retry if we're streaming chunks - just continue processing
|
649
|
+
if (error &&
|
650
|
+
typeof error === 'object' &&
|
651
|
+
'status' in error &&
|
652
|
+
error.status === 200) {
|
653
|
+
return false;
|
1072
654
|
}
|
1073
|
-
//
|
1074
|
-
|
1075
|
-
|
655
|
+
// Retry on 429 rate limit errors or 5xx server errors
|
656
|
+
const shouldRetry = Boolean(error &&
|
657
|
+
typeof error === 'object' &&
|
658
|
+
'status' in error &&
|
659
|
+
(error.status === 429 ||
|
660
|
+
(error.status >= 500 &&
|
661
|
+
error.status < 600)));
|
662
|
+
return shouldRetry;
|
1076
663
|
}
|
1077
664
|
}
|
1078
665
|
//# sourceMappingURL=OpenAIProvider.js.map
|