@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.
Files changed (118) hide show
  1. package/dist/src/adapters/IStreamAdapter.d.ts +3 -3
  2. package/dist/src/auth/precedence.d.ts +1 -1
  3. package/dist/src/auth/precedence.js +9 -4
  4. package/dist/src/auth/precedence.js.map +1 -1
  5. package/dist/src/auth/types.d.ts +4 -4
  6. package/dist/src/code_assist/codeAssist.js +8 -6
  7. package/dist/src/code_assist/codeAssist.js.map +1 -1
  8. package/dist/src/code_assist/setup.js +9 -7
  9. package/dist/src/code_assist/setup.js.map +1 -1
  10. package/dist/src/config/index.d.ts +7 -0
  11. package/dist/src/config/index.js +8 -0
  12. package/dist/src/config/index.js.map +1 -0
  13. package/dist/src/core/client.d.ts +9 -21
  14. package/dist/src/core/client.js +55 -156
  15. package/dist/src/core/client.js.map +1 -1
  16. package/dist/src/core/compression-config.d.ts +1 -1
  17. package/dist/src/core/compression-config.js +4 -5
  18. package/dist/src/core/compression-config.js.map +1 -1
  19. package/dist/src/core/coreToolScheduler.js +50 -15
  20. package/dist/src/core/coreToolScheduler.js.map +1 -1
  21. package/dist/src/core/geminiChat.d.ts +51 -2
  22. package/dist/src/core/geminiChat.js +616 -106
  23. package/dist/src/core/geminiChat.js.map +1 -1
  24. package/dist/src/core/nonInteractiveToolExecutor.js +70 -19
  25. package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
  26. package/dist/src/core/prompts.js +34 -26
  27. package/dist/src/core/prompts.js.map +1 -1
  28. package/dist/src/core/turn.d.ts +1 -0
  29. package/dist/src/core/turn.js +8 -6
  30. package/dist/src/core/turn.js.map +1 -1
  31. package/dist/src/index.d.ts +1 -2
  32. package/dist/src/index.js +2 -2
  33. package/dist/src/index.js.map +1 -1
  34. package/dist/src/prompt-config/TemplateEngine.js +17 -0
  35. package/dist/src/prompt-config/TemplateEngine.js.map +1 -1
  36. package/dist/src/prompt-config/defaults/core-defaults.js +39 -32
  37. package/dist/src/prompt-config/defaults/core-defaults.js.map +1 -1
  38. package/dist/src/prompt-config/defaults/core.md +2 -0
  39. package/dist/src/prompt-config/defaults/provider-defaults.js +34 -27
  40. package/dist/src/prompt-config/defaults/provider-defaults.js.map +1 -1
  41. package/dist/src/prompt-config/defaults/providers/gemini/core.md +229 -43
  42. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/core.md +12 -0
  43. package/dist/src/prompt-config/defaults/providers/gemini/models/gemini-2.5-flash/gemini-2-5-flash/core.md +12 -0
  44. package/dist/src/prompt-config/types.d.ts +2 -0
  45. package/dist/src/providers/BaseProvider.d.ts +32 -6
  46. package/dist/src/providers/BaseProvider.js +79 -22
  47. package/dist/src/providers/BaseProvider.js.map +1 -1
  48. package/dist/src/providers/IProvider.d.ts +9 -3
  49. package/dist/src/providers/LoggingProviderWrapper.d.ts +10 -3
  50. package/dist/src/providers/LoggingProviderWrapper.js +33 -27
  51. package/dist/src/providers/LoggingProviderWrapper.js.map +1 -1
  52. package/dist/src/providers/ProviderContentGenerator.d.ts +2 -2
  53. package/dist/src/providers/ProviderContentGenerator.js +9 -6
  54. package/dist/src/providers/ProviderContentGenerator.js.map +1 -1
  55. package/dist/src/providers/anthropic/AnthropicProvider.d.ts +27 -21
  56. package/dist/src/providers/anthropic/AnthropicProvider.js +473 -472
  57. package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
  58. package/dist/src/providers/gemini/GeminiProvider.d.ts +14 -9
  59. package/dist/src/providers/gemini/GeminiProvider.js +202 -486
  60. package/dist/src/providers/gemini/GeminiProvider.js.map +1 -1
  61. package/dist/src/providers/openai/ConversationCache.d.ts +3 -3
  62. package/dist/src/providers/openai/IChatGenerateParams.d.ts +9 -4
  63. package/dist/src/providers/openai/OpenAIProvider.d.ts +44 -115
  64. package/dist/src/providers/openai/OpenAIProvider.js +535 -948
  65. package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
  66. package/dist/src/providers/openai/buildResponsesRequest.d.ts +3 -3
  67. package/dist/src/providers/openai/buildResponsesRequest.js +67 -37
  68. package/dist/src/providers/openai/buildResponsesRequest.js.map +1 -1
  69. package/dist/src/providers/openai/estimateRemoteTokens.d.ts +2 -2
  70. package/dist/src/providers/openai/estimateRemoteTokens.js +21 -8
  71. package/dist/src/providers/openai/estimateRemoteTokens.js.map +1 -1
  72. package/dist/src/providers/openai/parseResponsesStream.d.ts +6 -2
  73. package/dist/src/providers/openai/parseResponsesStream.js +99 -391
  74. package/dist/src/providers/openai/parseResponsesStream.js.map +1 -1
  75. package/dist/src/providers/openai/syntheticToolResponses.d.ts +5 -5
  76. package/dist/src/providers/openai/syntheticToolResponses.js +102 -91
  77. package/dist/src/providers/openai/syntheticToolResponses.js.map +1 -1
  78. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.d.ts +18 -20
  79. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js +250 -239
  80. package/dist/src/providers/openai-responses/OpenAIResponsesProvider.js.map +1 -1
  81. package/dist/src/providers/tokenizers/OpenAITokenizer.js +3 -3
  82. package/dist/src/providers/tokenizers/OpenAITokenizer.js.map +1 -1
  83. package/dist/src/providers/types.d.ts +1 -1
  84. package/dist/src/services/history/ContentConverters.d.ts +6 -1
  85. package/dist/src/services/history/ContentConverters.js +155 -18
  86. package/dist/src/services/history/ContentConverters.js.map +1 -1
  87. package/dist/src/services/history/HistoryService.d.ts +52 -0
  88. package/dist/src/services/history/HistoryService.js +245 -93
  89. package/dist/src/services/history/HistoryService.js.map +1 -1
  90. package/dist/src/services/history/IContent.d.ts +4 -0
  91. package/dist/src/services/history/IContent.js.map +1 -1
  92. package/dist/src/telemetry/types.d.ts +16 -4
  93. package/dist/src/telemetry/types.js.map +1 -1
  94. package/dist/src/tools/IToolFormatter.d.ts +2 -2
  95. package/dist/src/tools/ToolFormatter.d.ts +42 -4
  96. package/dist/src/tools/ToolFormatter.js +159 -37
  97. package/dist/src/tools/ToolFormatter.js.map +1 -1
  98. package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
  99. package/dist/src/tools/doubleEscapeUtils.js +241 -0
  100. package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
  101. package/dist/src/tools/read-file.js +5 -2
  102. package/dist/src/tools/read-file.js.map +1 -1
  103. package/dist/src/tools/todo-schemas.d.ts +4 -4
  104. package/dist/src/tools/write-file.js +5 -2
  105. package/dist/src/tools/write-file.js.map +1 -1
  106. package/dist/src/types/modelParams.d.ts +8 -0
  107. package/dist/src/utils/bfsFileSearch.js +2 -6
  108. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  109. package/package.json +8 -7
  110. package/dist/src/core/ContentGeneratorAdapter.d.ts +0 -37
  111. package/dist/src/core/ContentGeneratorAdapter.js +0 -58
  112. package/dist/src/core/ContentGeneratorAdapter.js.map +0 -1
  113. package/dist/src/providers/IMessage.d.ts +0 -38
  114. package/dist/src/providers/IMessage.js +0 -17
  115. package/dist/src/providers/IMessage.js.map +0 -1
  116. package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
  117. package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
  118. 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 { isQwenEndpoint, generateOAuthEndpointMismatchError, } from '../../config/endpoints.js';
27
- import { getSettingsService } from '../../settings/settingsServiceInstance.js';
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
- // Check if we should enable OAuth for Qwen
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: shouldEnableQwenOAuth,
65
- oauthProvider: shouldEnableQwenOAuth ? 'qwen' : undefined,
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
- // Initialize from SettingsService
75
- this.initializeFromSettings().catch((error) => {
76
- this.logger.debug(() => `Failed to initialize from SettingsService: ${error}`);
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
- * Helper method to determine if we're using Qwen (via OAuth or direct endpoint)
59
+ * Create HTTP/HTTPS agents with socket configuration for local AI servers
60
+ * Returns undefined if no socket settings are configured
115
61
  */
116
- isUsingQwen() {
117
- // Check if we're using qwen format based on tool format detection
118
- const toolFormat = this.detectToolFormat();
119
- return toolFormat === 'qwen';
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
- * Update the OpenAI client with resolved authentication if needed
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 updateClientWithResolvedAuth() {
115
+ async getClient() {
125
116
  const resolvedKey = await this.getAuthToken();
126
- if (!resolvedKey) {
127
- // Provide specific error message based on endpoint validation
128
- const endpoint = this.baseURL || 'https://api.openai.com/v1';
129
- if (this.isOAuthEnabled() && !this.supportsOAuth()) {
130
- throw new Error(generateOAuthEndpointMismatchError(endpoint, 'qwen'));
131
- }
132
- throw new Error('No authentication available for OpenAI API calls');
133
- }
134
- // Check if we're using Qwen OAuth and need to update the baseURL
135
- let effectiveBaseURL = this.baseURL;
136
- this.logger.debug(() => `updateClientWithResolvedAuth - OAuth enabled: ${this.isOAuthEnabled()}, OAuth provider: ${this.baseProviderConfig.oauthProvider}, baseURL: ${this.baseURL}, resolvedKey: ${resolvedKey?.substring(0, 10)}...`);
137
- if (this.isOAuthEnabled() &&
138
- this.baseProviderConfig.oauthProvider === 'qwen') {
139
- // Get the OAuth token to check for resource_url
140
- const oauthManager = this.baseProviderConfig.oauthManager;
141
- if (oauthManager?.getOAuthToken) {
142
- const oauthToken = await oauthManager.getOAuthToken('qwen');
143
- this.logger.debug(() => `OAuth token retrieved:\n` +
144
- ` resource_url: ${oauthToken?.resource_url}\n` +
145
- ` access_token: ${oauthToken?.access_token?.substring(0, 10)}...`);
146
- if (oauthToken?.resource_url) {
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
- requiresTextToolCallParsing() {
174
- if (this.providerConfig?.enableTextToolCallParsing === false) {
175
- return false;
176
- }
177
- // Check if current tool format requires text-based parsing
178
- const currentFormat = this.getToolFormat();
179
- const textBasedFormats = ['hermes', 'xml', 'llama'];
180
- if (textBasedFormats.includes(currentFormat)) {
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
- const configuredModels = this.providerConfig?.textToolCallModels || [];
184
- return configuredModels.includes(this.currentModel);
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
- // Get resolved authentication and update client if needed
219
- await this.updateClientWithResolvedAuth();
220
- const response = await this.openai.models.list();
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, moderation, DALL·E, etc.)
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
- // Check if this is a Qwen endpoint
239
- if (isQwenEndpoint(this.baseURL || '')) {
240
- return [
241
- {
242
- id: 'qwen3-coder-plus',
243
- name: 'qwen3-coder-plus',
244
- provider: 'openai',
245
- supportedToolFormats: ['openai'],
246
- },
247
- ];
248
- }
249
- // Default OpenAI models
250
- return [
251
- {
252
- id: 'gpt-4o',
253
- name: 'gpt-4o',
254
- provider: 'openai',
255
- supportedToolFormats: ['openai'],
256
- },
257
- {
258
- id: 'gpt-4o-mini',
259
- name: 'gpt-4o-mini',
260
- provider: 'openai',
261
- supportedToolFormats: ['openai'],
262
- },
263
- {
264
- id: 'gpt-4-turbo',
265
- name: 'gpt-4-turbo',
266
- provider: 'openai',
267
- supportedToolFormats: ['openai'],
268
- },
269
- {
270
- id: 'gpt-3.5-turbo',
271
- name: 'gpt-3.5-turbo',
272
- provider: 'openai',
273
- supportedToolFormats: ['openai'],
274
- },
275
- ];
276
- }
277
- }
278
- async *generateChatCompletion(messages, tools, _toolFormat) {
279
- // 1. Validate authentication and messages
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 the default model for this provider
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
- * Invoke a server tool (native provider tool)
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
- * Set model parameters to be included in API calls
504
- * @param params Parameters to merge with existing, or undefined to clear all
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
- setModelParams(params) {
507
- if (params === undefined) {
508
- this.modelParams = undefined;
244
+ normalizeToOpenAIToolId(id) {
245
+ // If already in OpenAI format, return as-is
246
+ if (id.startsWith('call_')) {
247
+ return id;
509
248
  }
510
- else {
511
- this.modelParams = { ...this.modelParams, ...params };
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
- // Persist to SettingsService if available
514
- this.setModelParamsInSettings(this.modelParams).catch((error) => {
515
- this.logger.debug(() => `Failed to persist model params to SettingsService: ${error}`);
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
- catch (error) {
548
- this.logger.debug(() => `Failed to initialize OpenAI provider from SettingsService: ${error}`);
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
- * Detect the appropriate tool format for the current model/configuration
560
- * @returns The detected tool format
263
+ * Normalize tool IDs from OpenAI format to history format
561
264
  */
562
- detectToolFormat() {
563
- try {
564
- const settingsService = getSettingsService();
565
- // First check SettingsService for toolFormat override in provider settings
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
- catch (error) {
588
- this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
589
- // Fallback detection without SettingsService
590
- const modelName = this.currentModel.toLowerCase();
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
- * Get appropriate tool_choice value based on detected tool format
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;
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
- // For all formats, use 'auto' (standard behavior)
610
- // Future enhancement: different formats may need different tool_choice values
611
- return 'auto';
280
+ // Unknown format - assume it's a raw UUID
281
+ return 'hist_tool_' + id;
612
282
  }
613
283
  /**
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
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
- 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;
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
- * Validate authentication and message preconditions for API calls
308
+ * Convert IContent array to OpenAI ChatCompletionMessageParam array
645
309
  */
646
- async validateRequestPreconditions(messages) {
647
- // Check if API key is available (using resolved authentication)
648
- const apiKey = await this.getAuthToken();
649
- if (!apiKey) {
650
- const endpoint = this.baseURL || 'https://api.openai.com/v1';
651
- if (this.isOAuthEnabled() && !this.supportsOAuth()) {
652
- throw new Error(generateOAuthEndpointMismatchError(endpoint, 'qwen'));
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
- * Prepare API request configuration
372
+ * Internal implementation for chat completion
666
373
  */
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 };
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
- return {
683
- parser,
684
- currentToolFormat,
685
- formattedTools,
686
- finalStreamOptions,
687
- streamingEnabled,
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
- * Execute API call with error handling
692
- */
693
- async executeApiCall(messages, tools, requestConfig) {
694
- // Get resolved authentication and update client if needed
695
- await this.updateClientWithResolvedAuth();
696
- this.logger.debug(() => `About to make API call with model: ${this.currentModel}, baseURL: ${this.openai.baseURL}, apiKey: ${this.openai.apiKey?.substring(0, 10)}..., streaming: ${requestConfig.streamingEnabled}, messages (${messages.length} total): ${messages
697
- .map((m) => `${m.role}${m.role === 'system' ? ` (length: ${m.content?.length})` : ''}`)
698
- .join(', ')}`);
699
- try {
700
- // Build request params with exact order from original
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,
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
- catch (error) {
714
- this.handleApiError(error, messages);
715
- throw error; // Re-throw after logging
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
- this.logger.error(() => `${errorLabel} Last 5 messages being sent:\n` +
745
- ` Has pending tool calls without responses: ${hasPendingToolCalls}`);
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
- });
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
- * 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;
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 (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
- });
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
- if (response.usage) {
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
- }));
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
- else {
828
- // No tool calls found, yield cleaned content
829
- yield {
830
- role: ContentGeneratorRole.ASSISTANT,
831
- content: cleanedContent,
832
- usage: usageData,
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
- // Standard OpenAI tool call handling
838
- if (accumulatedToolCalls.length > 0) {
839
- // Process tool calls with Qwen-specific fixes if needed
840
- const fixedToolCalls = this.processQwenToolCalls(accumulatedToolCalls);
841
- if (this.isUsingQwen()) {
842
- this.logger.debug(() => `Final message with tool calls: ${JSON.stringify({
843
- contentLength: finalFullContent.length,
844
- content: finalFullContent.substring(0, 200) +
845
- (finalFullContent.length > 200 ? '...' : ''),
846
- toolCallCount: accumulatedToolCalls.length,
847
- hasStreamedContent,
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
- else if (usageData) {
855
- // Always emit usage data so downstream consumers can update stats
598
+ // Emit the complete response as a single IContent
599
+ if (blocks.length > 0) {
856
600
  yield {
857
- role: ContentGeneratorRole.ASSISTANT,
858
- content: '',
859
- usage: usageData,
601
+ speaker: 'ai',
602
+ blocks,
860
603
  };
861
604
  }
862
605
  }
863
606
  }
864
607
  /**
865
- * Handle Qwen-specific whitespace buffering during streaming
866
- * @param delta The stream delta containing content
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
- handleQwenStreamingWhitespace(delta, pendingWhitespace, fullContent) {
872
- if (!delta.content) {
873
- return {
874
- shouldYield: false,
875
- content: '',
876
- updatedPendingWhitespace: pendingWhitespace,
877
- updatedFullContent: fullContent,
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
- const isWhitespaceOnly = delta.content.trim() === '';
881
- if (isWhitespaceOnly) {
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
- // Non-whitespace content - flush any pending whitespace first
893
- if (pendingWhitespace) {
894
- this.logger.debug(() => `Flushing pending whitespace (len=${pendingWhitespace?.length ?? 0}) before non-empty chunk`);
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
- return {
903
- shouldYield: true,
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
- this.logger.debug(() => `[Qwen Fix] Processing ${toolCalls.length} tool calls for double-stringification fix`);
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
- * Determine how to yield the final message with tool calls based on provider quirks
931
- * @param hasStreamedContent Whether content was already streamed
932
- * @param fullContent The complete content
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
- buildFinalToolCallMessage(hasStreamedContent, fullContent, toolCalls, usageData) {
938
- const isCerebras = this.baseURL?.toLowerCase().includes('cerebras.ai');
939
- if (isCerebras) {
940
- this.logger.debug(() => '[Cerebras] Special handling for Cerebras provider after tool responses', {
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
- * Fix Qwen's double stringification of tool call arguments
976
- * Qwen models stringify array/object values WITHIN the JSON arguments
977
- * @param toolCall The tool call to fix
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
- fixQwenDoubleStringification(toolCall) {
981
- if (!toolCall.function.arguments ||
982
- typeof toolCall.function.arguments !== 'string') {
983
- return toolCall;
984
- }
985
- try {
986
- // First, parse the arguments to get the JSON object
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
- // No fix needed
1074
- this.logger.debug(() => `[Qwen Fix] No double-stringification detected for ${toolCall.function.name}, keeping original`);
1075
- return toolCall;
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