@vybestack/llxprt-code-core 0.1.23-nightly.250905.97906524 → 0.2.2-nightly.250908.7b895396

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) 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 +582 -946
  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/settings/SettingsService.js.map +1 -1
  93. package/dist/src/telemetry/types.d.ts +16 -4
  94. package/dist/src/telemetry/types.js.map +1 -1
  95. package/dist/src/tools/IToolFormatter.d.ts +2 -2
  96. package/dist/src/tools/ToolFormatter.d.ts +42 -4
  97. package/dist/src/tools/ToolFormatter.js +151 -64
  98. package/dist/src/tools/ToolFormatter.js.map +1 -1
  99. package/dist/src/tools/doubleEscapeUtils.d.ts +57 -0
  100. package/dist/src/tools/doubleEscapeUtils.js +241 -0
  101. package/dist/src/tools/doubleEscapeUtils.js.map +1 -0
  102. package/dist/src/tools/read-file.js +5 -2
  103. package/dist/src/tools/read-file.js.map +1 -1
  104. package/dist/src/tools/todo-schemas.d.ts +4 -4
  105. package/dist/src/tools/tool-registry.d.ts +8 -1
  106. package/dist/src/tools/tool-registry.js +79 -23
  107. package/dist/src/tools/tool-registry.js.map +1 -1
  108. package/dist/src/tools/write-file.js +5 -2
  109. package/dist/src/tools/write-file.js.map +1 -1
  110. package/dist/src/types/modelParams.d.ts +12 -0
  111. package/dist/src/utils/bfsFileSearch.js +2 -6
  112. package/dist/src/utils/bfsFileSearch.js.map +1 -1
  113. package/package.json +8 -7
  114. package/dist/src/core/ContentGeneratorAdapter.d.ts +0 -37
  115. package/dist/src/core/ContentGeneratorAdapter.js +0 -58
  116. package/dist/src/core/ContentGeneratorAdapter.js.map +0 -1
  117. package/dist/src/providers/IMessage.d.ts +0 -38
  118. package/dist/src/providers/IMessage.js +0 -17
  119. package/dist/src/providers/IMessage.js.map +0 -1
  120. package/dist/src/providers/adapters/GeminiCompatibleWrapper.d.ts +0 -69
  121. package/dist/src/providers/adapters/GeminiCompatibleWrapper.js +0 -577
  122. package/dist/src/providers/adapters/GeminiCompatibleWrapper.js.map +0 -1
@@ -17,210 +17,158 @@
17
17
  * @plan PLAN-20250120-DEBUGLOGGING.P15
18
18
  * @requirement REQ-INT-001.1
19
19
  */
20
- import { DebugLogger } from '../../debug/index.js';
21
- import { ContentGeneratorRole } from '../ContentGeneratorRole.js';
22
- import { GemmaToolCallParser } from '../../parsers/TextToolCallParser.js';
23
- import { ToolFormatter } from '../../tools/ToolFormatter.js';
24
20
  import OpenAI from 'openai';
21
+ import * as http from 'http';
22
+ import * as https from 'https';
23
+ import * as net from 'net';
25
24
  import { BaseProvider } from '../BaseProvider.js';
26
- import { 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';
29
+ import { retryWithBackoff } from '../../utils/retry.js';
28
30
  export class OpenAIProvider extends BaseProvider {
31
+ name = 'openai';
29
32
  logger;
30
- openai;
31
- currentModel = process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
32
- baseURL;
33
- providerConfig;
34
33
  toolFormatter;
35
- toolFormatOverride;
36
- modelParams;
37
34
  _cachedClient;
38
35
  _cachedClientKey;
39
36
  constructor(apiKey, baseURL, config, oauthManager) {
37
+ // Normalize empty string to undefined for proper precedence handling
38
+ const normalizedApiKey = apiKey && apiKey.trim() !== '' ? apiKey : undefined;
39
+ // Detect if this is a Qwen endpoint
40
+ const isQwenEndpoint = !!(baseURL &&
41
+ (baseURL.includes('dashscope.aliyuncs.com') ||
42
+ baseURL.includes('api.qwen.com') ||
43
+ baseURL.includes('qwen')));
40
44
  // Initialize base provider with auth configuration
41
- // 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 = {
45
+ super({
60
46
  name: 'openai',
61
- apiKey,
47
+ apiKey: normalizedApiKey,
62
48
  baseURL,
63
- envKeyNames: ['OPENAI_API_KEY'],
64
- isOAuthEnabled: shouldEnableQwenOAuth,
65
- oauthProvider: shouldEnableQwenOAuth ? 'qwen' : undefined,
49
+ envKeyNames: ['OPENAI_API_KEY'], // Support environment variable fallback
50
+ isOAuthEnabled: isQwenEndpoint && !!oauthManager,
51
+ oauthProvider: isQwenEndpoint ? 'qwen' : undefined,
66
52
  oauthManager,
67
- };
68
- super(baseConfig);
69
- this.logger = new DebugLogger('llxprt:providers:openai');
70
- this.logger.debug(() => `Constructor - baseURL: ${baseURL}, apiKey: ${apiKey?.substring(0, 10) || 'none'}, oauthManager: ${!!oauthManager}, shouldEnableQwenOAuth: ${shouldEnableQwenOAuth}`);
71
- this.baseURL = baseURL;
72
- this.providerConfig = config;
53
+ }, config);
73
54
  this.toolFormatter = new ToolFormatter();
74
- // 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);
55
+ // new DebugLogger('llxprt:core:toolformatter'), // TODO: Fix ToolFormatter constructor
56
+ // Setup debug logger
57
+ this.logger = new DebugLogger('llxprt:provider:openai');
112
58
  }
113
59
  /**
114
- * Helper method to determine if we're using Qwen (via OAuth or direct endpoint)
60
+ * Create HTTP/HTTPS agents with socket configuration for local AI servers
61
+ * Returns undefined if no socket settings are configured
115
62
  */
116
- isUsingQwen() {
117
- // Check if we're using qwen format based on tool format detection
118
- const toolFormat = this.detectToolFormat();
119
- return toolFormat === 'qwen';
63
+ createHttpAgents() {
64
+ // Get socket configuration from ephemeral settings
65
+ const settings = this.providerConfig?.getEphemeralSettings?.() || {};
66
+ // Check if any socket settings are explicitly configured
67
+ const hasSocketSettings = 'socket-timeout' in settings ||
68
+ 'socket-keepalive' in settings ||
69
+ 'socket-nodelay' in settings;
70
+ // Only create custom agents if socket settings are configured
71
+ if (!hasSocketSettings) {
72
+ return undefined;
73
+ }
74
+ // Socket configuration with defaults for when settings ARE configured
75
+ const socketTimeout = settings['socket-timeout'] || 60000; // 60 seconds default
76
+ const socketKeepAlive = settings['socket-keepalive'] !== false; // true by default
77
+ const socketNoDelay = settings['socket-nodelay'] !== false; // true by default
78
+ // Create HTTP agent with socket options
79
+ const httpAgent = new http.Agent({
80
+ keepAlive: socketKeepAlive,
81
+ keepAliveMsecs: 1000,
82
+ timeout: socketTimeout,
83
+ });
84
+ // Create HTTPS agent with socket options
85
+ const httpsAgent = new https.Agent({
86
+ keepAlive: socketKeepAlive,
87
+ keepAliveMsecs: 1000,
88
+ timeout: socketTimeout,
89
+ });
90
+ // Apply TCP_NODELAY if enabled (reduces latency for local servers)
91
+ if (socketNoDelay) {
92
+ const originalCreateConnection = httpAgent.createConnection;
93
+ httpAgent.createConnection = function (options, callback) {
94
+ const socket = originalCreateConnection.call(this, options, callback);
95
+ if (socket instanceof net.Socket) {
96
+ socket.setNoDelay(true);
97
+ }
98
+ return socket;
99
+ };
100
+ const originalHttpsCreateConnection = httpsAgent.createConnection;
101
+ httpsAgent.createConnection = function (options, callback) {
102
+ const socket = originalHttpsCreateConnection.call(this, options, callback);
103
+ if (socket instanceof net.Socket) {
104
+ socket.setNoDelay(true);
105
+ }
106
+ return socket;
107
+ };
108
+ }
109
+ return { httpAgent, httpsAgent };
120
110
  }
121
111
  /**
122
- * Update the OpenAI client with resolved authentication if needed
112
+ * Get or create OpenAI client instance
113
+ * Will use the API key from resolved auth
114
+ * @returns OpenAI client instance
123
115
  */
124
- async updateClientWithResolvedAuth() {
116
+ async getClient() {
125
117
  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;
118
+ // Use the unified getBaseURL() method from BaseProvider
119
+ const baseURL = this.getBaseURL();
120
+ const clientKey = `${baseURL}-${resolvedKey}`;
121
+ // Return cached client if available and auth hasn't changed
122
+ if (this._cachedClient && this._cachedClientKey === clientKey) {
123
+ return this._cachedClient;
124
+ }
125
+ // Create HTTP agents with socket configuration (if configured)
126
+ const agents = this.createHttpAgents();
127
+ // Build client options - OpenAI SDK accepts httpAgent/httpsAgent at runtime
128
+ // even though they're not in the TypeScript definitions
129
+ const baseOptions = {
130
+ apiKey: resolvedKey || '',
131
+ baseURL,
132
+ };
133
+ // Add socket configuration if available
134
+ const clientOptions = agents
135
+ ? {
136
+ ...baseOptions,
137
+ httpAgent: agents.httpAgent,
138
+ httpsAgent: agents.httpsAgent,
170
139
  }
171
- }
140
+ : baseOptions;
141
+ // Create new client with current auth and optional socket configuration
142
+ // Cast to unknown then to the expected type to bypass TypeScript's structural checking
143
+ this._cachedClient = new OpenAI(clientOptions);
144
+ this._cachedClientKey = clientKey;
145
+ return this._cachedClient;
172
146
  }
173
- 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)) {
147
+ /**
148
+ * Check if OAuth is supported for this provider
149
+ * Qwen endpoints support OAuth, standard OpenAI does not
150
+ */
151
+ supportsOAuth() {
152
+ const baseURL = this.getBaseURL();
153
+ // Check if this is a Qwen endpoint that supports OAuth
154
+ if (baseURL &&
155
+ (baseURL.includes('dashscope.aliyuncs.com') ||
156
+ baseURL.includes('api.qwen.com') ||
157
+ baseURL.includes('qwen'))) {
181
158
  return true;
182
159
  }
183
- 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';
160
+ // Standard OpenAI endpoints don't support OAuth
161
+ return false;
206
162
  }
207
163
  async getModels() {
208
- // Check if API key is available (using resolved authentication)
209
- const apiKey = await this.getAuthToken();
210
- if (!apiKey) {
211
- const endpoint = this.baseURL || 'https://api.openai.com/v1';
212
- if (this.isOAuthEnabled() && !this.supportsOAuth()) {
213
- throw new Error(generateOAuthEndpointMismatchError(endpoint, 'qwen'));
214
- }
215
- throw new Error('OpenAI API key is required to fetch models');
216
- }
217
164
  try {
218
- // Get resolved authentication and update client if needed
219
- await this.updateClientWithResolvedAuth();
220
- const response = await this.openai.models.list();
165
+ // Always try to fetch models, regardless of auth status
166
+ // Local endpoints often work without authentication
167
+ const client = await this.getClient();
168
+ const response = await client.models.list();
221
169
  const models = [];
222
170
  for await (const model of response) {
223
- // Filter out non-chat models (embeddings, audio, image, moderation, DALL·E, etc.)
171
+ // Filter out non-chat models (embeddings, audio, image, vision, DALL·E, etc.)
224
172
  if (!/embedding|whisper|audio|tts|image|vision|dall[- ]?e|moderation/i.test(model.id)) {
225
173
  models.push({
226
174
  id: model.id,
@@ -235,844 +183,532 @@ export class OpenAIProvider extends BaseProvider {
235
183
  catch (error) {
236
184
  this.logger.debug(() => `Error fetching models from OpenAI: ${error}`);
237
185
  // Return a hardcoded list as fallback
238
- // 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();
186
+ return this.getFallbackModels();
187
+ }
188
+ }
189
+ getFallbackModels() {
190
+ return [
191
+ {
192
+ id: 'gpt-5',
193
+ name: 'GPT-5',
194
+ provider: 'openai',
195
+ supportedToolFormats: ['openai'],
196
+ },
197
+ {
198
+ id: 'gpt-4.1',
199
+ name: 'GPT-4.1',
200
+ provider: 'openai',
201
+ supportedToolFormats: ['openai'],
202
+ },
203
+ {
204
+ id: 'gpt-4o',
205
+ name: 'GPT-4o',
206
+ provider: 'openai',
207
+ supportedToolFormats: ['openai'],
208
+ },
209
+ {
210
+ id: 'o3',
211
+ name: 'O3',
212
+ provider: 'openai',
213
+ supportedToolFormats: ['openai'],
214
+ },
215
+ {
216
+ id: 'o4-mini',
217
+ name: 'O4 Mini',
218
+ provider: 'openai',
219
+ supportedToolFormats: ['openai'],
220
+ },
221
+ {
222
+ id: 'gpt-3.5-turbo',
223
+ name: 'GPT-3.5 Turbo (Legacy)',
224
+ provider: 'openai',
225
+ supportedToolFormats: ['openai'],
226
+ },
227
+ ];
418
228
  }
419
229
  getDefaultModel() {
420
- // Return 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
- }
230
+ // Return hardcoded default - do NOT call getModel() to avoid circular dependency
425
231
  return process.env.LLXPRT_DEFAULT_MODEL || 'gpt-5';
426
232
  }
427
- setApiKey(apiKey) {
428
- // Call base provider implementation
429
- super.setApiKey(apiKey);
430
- // Persist to SettingsService if available
431
- this.setApiKeyInSettings(apiKey).catch((error) => {
432
- this.logger.debug(() => `Failed to persist API key to SettingsService: ${error}`);
433
- });
434
- // Create a new OpenAI client with the updated API key
435
- const clientOptions = {
436
- apiKey,
437
- dangerouslyAllowBrowser: this.providerConfig?.allowBrowserEnvironment || false,
438
- };
439
- // Only include baseURL if it's defined
440
- if (this.baseURL) {
441
- clientOptions.baseURL = this.baseURL;
442
- }
443
- this.openai = new OpenAI(clientOptions);
444
- this._cachedClientKey = apiKey; // Update cached key
445
- }
446
- setBaseUrl(baseUrl) {
447
- // If no baseUrl is provided, clear to default (undefined)
448
- this.baseURL = baseUrl && baseUrl.trim() !== '' ? baseUrl : undefined;
449
- // Persist to SettingsService if available
450
- this.setBaseUrlInSettings(this.baseURL).catch((error) => {
451
- this.logger.debug(() => `Failed to persist base URL to SettingsService: ${error}`);
452
- });
453
- // Update OAuth configuration based on endpoint validation
454
- // Enable OAuth for Qwen endpoints if we have an OAuth manager
455
- const shouldEnableQwenOAuth = !!this.baseProviderConfig.oauthManager &&
456
- (isQwenEndpoint(this.baseURL || '') ||
457
- this.baseURL === 'https://portal.qwen.ai/v1');
458
- this.updateOAuthConfig(shouldEnableQwenOAuth, shouldEnableQwenOAuth ? 'qwen' : undefined, this.baseProviderConfig.oauthManager);
459
- // Call base provider implementation
460
- super.setBaseUrl?.(baseUrl);
461
- // Create a new OpenAI client with the updated (or cleared) base URL
462
- const clientOptions = {
463
- // Use existing key or empty string as placeholder
464
- apiKey: this._cachedClientKey || 'placeholder',
465
- dangerouslyAllowBrowser: this.providerConfig?.allowBrowserEnvironment || false,
466
- };
467
- // Only include baseURL if it's defined
468
- if (this.baseURL) {
469
- clientOptions.baseURL = this.baseURL;
470
- }
471
- this.openai = new OpenAI(clientOptions);
472
- // Clear cached key to force re-resolution on next API call
473
- this._cachedClientKey = undefined;
474
- }
475
- setConfig(config) {
476
- this.providerConfig = config;
477
- }
478
- setToolFormatOverride(format) {
479
- this.toolFormatOverride = format || undefined;
480
- }
481
- /**
482
- * OpenAI always requires payment (API key)
483
- */
484
- isPaidMode() {
485
- return true;
486
- }
487
- clearState() {
488
- // No state to clear in base OpenAI provider
489
- }
490
- /**
491
- * Get the list of server tools supported by this provider
492
- */
493
233
  getServerTools() {
234
+ // TODO: Implement server tools for OpenAI provider
494
235
  return [];
495
236
  }
496
- /**
497
- * 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');
237
+ async invokeServerTool(toolName, _params, _config) {
238
+ // TODO: Implement server tool invocation for OpenAI provider
239
+ throw new Error(`Server tool '${toolName}' not supported by OpenAI provider`);
501
240
  }
502
241
  /**
503
- * Set model parameters to be included in API calls
504
- * @param params Parameters to merge with existing, or undefined to clear all
242
+ * Normalize tool IDs from various formats to OpenAI format
243
+ * Handles IDs from OpenAI (call_xxx), Anthropic (toolu_xxx), and history (hist_tool_xxx)
505
244
  */
506
- setModelParams(params) {
507
- if (params === undefined) {
508
- this.modelParams = undefined;
245
+ normalizeToOpenAIToolId(id) {
246
+ // If already in OpenAI format, return as-is
247
+ if (id.startsWith('call_')) {
248
+ return id;
509
249
  }
510
- else {
511
- this.modelParams = { ...this.modelParams, ...params };
250
+ // For history format, extract the UUID and add OpenAI prefix
251
+ if (id.startsWith('hist_tool_')) {
252
+ const uuid = id.substring('hist_tool_'.length);
253
+ return 'call_' + uuid;
512
254
  }
513
- // 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;
255
+ // For Anthropic format, extract the UUID and add OpenAI prefix
256
+ if (id.startsWith('toolu_')) {
257
+ const uuid = id.substring('toolu_'.length);
258
+ return 'call_' + uuid;
259
+ }
260
+ // Unknown format - assume it's a raw UUID
261
+ return 'call_' + id;
524
262
  }
525
263
  /**
526
- * Initialize provider configuration from SettingsService
264
+ * Normalize tool IDs from OpenAI format to history format
527
265
  */
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)}`);
266
+ normalizeToHistoryToolId(id) {
267
+ // If already in history format, return as-is
268
+ if (id.startsWith('hist_tool_')) {
269
+ return id;
546
270
  }
547
- catch (error) {
548
- this.logger.debug(() => `Failed to initialize OpenAI provider from SettingsService: ${error}`);
271
+ // For OpenAI format, extract the UUID and add history prefix
272
+ if (id.startsWith('call_')) {
273
+ const uuid = id.substring('call_'.length);
274
+ return 'hist_tool_' + uuid;
549
275
  }
276
+ // For Anthropic format, extract the UUID and add history prefix
277
+ if (id.startsWith('toolu_')) {
278
+ const uuid = id.substring('toolu_'.length);
279
+ return 'hist_tool_' + uuid;
280
+ }
281
+ // Unknown format - assume it's a raw UUID
282
+ return 'hist_tool_' + id;
550
283
  }
551
284
  /**
552
- * Check if the provider is authenticated using any available method
553
- * Uses the base provider's isAuthenticated implementation
285
+ * Generate chat completion with IContent interface
286
+ * Internally converts to OpenAI API format, but only yields IContent
287
+ * @param contents Array of content blocks (text and tool_call)
288
+ * @param tools Array of available tools
554
289
  */
555
- async isAuthenticated() {
556
- return super.isAuthenticated();
290
+ async *generateChatCompletion(contents, tools) {
291
+ // Debug log what we receive
292
+ if (this.logger.enabled) {
293
+ this.logger.debug(() => `[OpenAIProvider] generateChatCompletion received tools:`, {
294
+ hasTools: !!tools,
295
+ toolsLength: tools?.length,
296
+ toolsType: typeof tools,
297
+ isArray: Array.isArray(tools),
298
+ firstToolName: tools?.[0]?.functionDeclarations?.[0]?.name,
299
+ toolsStructure: tools ? 'available' : 'undefined',
300
+ });
301
+ }
302
+ // Pass tools directly in Gemini format - they'll be converted in generateChatCompletionImpl
303
+ const generator = this.generateChatCompletionImpl(contents, tools, undefined, undefined, undefined);
304
+ for await (const item of generator) {
305
+ yield item;
306
+ }
557
307
  }
558
308
  /**
559
- * Detect the appropriate tool format for the current model/configuration
560
- * @returns The detected tool format
309
+ * Convert IContent array to OpenAI ChatCompletionMessageParam array
561
310
  */
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';
311
+ convertToOpenAIMessages(contents) {
312
+ const messages = [];
313
+ for (const content of contents) {
314
+ if (content.speaker === 'human') {
315
+ // Convert human messages to user messages
316
+ const textBlocks = content.blocks.filter((b) => b.type === 'text');
317
+ const text = textBlocks.map((b) => b.text).join('\n');
318
+ if (text) {
319
+ messages.push({
320
+ role: 'user',
321
+ content: text,
322
+ });
323
+ }
583
324
  }
584
- // Default to 'openai' format
585
- return 'openai';
586
- }
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';
325
+ else if (content.speaker === 'ai') {
326
+ // Convert AI messages
327
+ const textBlocks = content.blocks.filter((b) => b.type === 'text');
328
+ const toolCalls = content.blocks.filter((b) => b.type === 'tool_call');
329
+ if (toolCalls.length > 0) {
330
+ // Assistant message with tool calls
331
+ const text = textBlocks.map((b) => b.text).join('\n');
332
+ messages.push({
333
+ role: 'assistant',
334
+ content: text || null,
335
+ tool_calls: toolCalls.map((tc) => ({
336
+ id: this.normalizeToOpenAIToolId(tc.id),
337
+ type: 'function',
338
+ function: {
339
+ name: tc.name,
340
+ arguments: typeof tc.parameters === 'string'
341
+ ? tc.parameters
342
+ : JSON.stringify(tc.parameters),
343
+ },
344
+ })),
345
+ });
346
+ }
347
+ else if (textBlocks.length > 0) {
348
+ // Plain assistant message
349
+ const text = textBlocks.map((b) => b.text).join('\n');
350
+ messages.push({
351
+ role: 'assistant',
352
+ content: text,
353
+ });
354
+ }
593
355
  }
594
- if (modelName.includes('qwen')) {
595
- return 'qwen';
356
+ else if (content.speaker === 'tool') {
357
+ // Convert tool responses
358
+ const toolResponses = content.blocks.filter((b) => b.type === 'tool_response');
359
+ for (const tr of toolResponses) {
360
+ messages.push({
361
+ role: 'tool',
362
+ content: typeof tr.result === 'string'
363
+ ? tr.result
364
+ : JSON.stringify(tr.result),
365
+ tool_call_id: this.normalizeToOpenAIToolId(tr.callId),
366
+ });
367
+ }
596
368
  }
597
- return 'openai';
598
369
  }
370
+ return messages;
599
371
  }
600
372
  /**
601
- * 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;
608
- }
609
- // For all formats, use 'auto' (standard behavior)
610
- // Future enhancement: different formats may need different tool_choice values
611
- return 'auto';
612
- }
613
- /**
614
- * Format tools for API based on detected tool format
615
- * @param tools Array of tools to format
616
- * @returns Formatted tools for API consumption
617
- */
618
- formatToolsForAPI(tools) {
619
- // For now, always use OpenAI format through OpenRouter
620
- // TODO: Investigate if OpenRouter needs special handling for GLM/Qwen
621
- // const detectedFormat = this.detectToolFormat();
622
- // if (detectedFormat === 'qwen') {
623
- // // Convert OpenAI format to Qwen format: {name, description, parameters} without type/function wrapper
624
- // return tools.map((tool) => ({
625
- // name: tool.function.name,
626
- // description: tool.function.description,
627
- // parameters: tool.function.parameters,
628
- // }));
629
- // }
630
- // For all formats, use the existing ToolFormatter
631
- return this.toolFormatter.toProviderFormat(tools, 'openai');
632
- }
633
- /**
634
- * Parse tool response from API (placeholder for future response parsing)
635
- * @param response The raw API response
636
- * @returns Parsed tool response
637
- */
638
- parseToolResponse(response) {
639
- // TODO: Implement response parsing based on detected format
640
- // For now, return the response as-is
641
- return response;
642
- }
643
- /**
644
- * Validate authentication and message preconditions for API calls
373
+ * Internal implementation for chat completion
645
374
  */
646
- async 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'));
653
- }
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.`);
375
+ async *generateChatCompletionImpl(contents, tools, maxTokens, abortSignal, modelName) {
376
+ // Always look up model from SettingsService
377
+ const model = modelName || this.getModel() || this.getDefaultModel();
378
+ // Convert IContent to OpenAI messages format
379
+ const messages = this.convertToOpenAIMessages(contents);
380
+ // Convert Gemini format tools directly to OpenAI format using the new method
381
+ const formattedTools = this.toolFormatter.convertGeminiToOpenAI(tools);
382
+ // Debug log the conversion result
383
+ if (this.logger.enabled) {
384
+ this.logger.debug(() => `[OpenAIProvider] Tool conversion summary:`, {
385
+ inputHadTools: !!tools,
386
+ inputToolsLength: tools?.length,
387
+ outputHasTools: !!formattedTools,
388
+ outputToolsLength: formattedTools?.length,
389
+ outputToolNames: formattedTools?.map((t) => t.function.name),
390
+ });
662
391
  }
663
- }
664
- /**
665
- * Prepare API request configuration
666
- */
667
- prepareApiRequest(messages, tools) {
668
- const parser = this.requiresTextToolCallParsing()
669
- ? new GemmaToolCallParser()
670
- : null;
671
- // Get current tool format (with override support)
672
- const currentToolFormat = this.getToolFormat();
673
- // Format tools using formatToolsForAPI method
674
- const formattedTools = tools ? this.formatToolsForAPI(tools) : undefined;
675
- // Get stream_options from ephemeral settings (not model params)
676
- const streamOptions = this.providerConfig?.getEphemeralSettings?.()?.['stream-options'];
677
- // Default stream_options to { include_usage: true } unless explicitly set
678
- const finalStreamOptions = streamOptions !== undefined ? streamOptions : { include_usage: true };
679
392
  // Get streaming setting from ephemeral settings (default: enabled)
680
393
  const streamingSetting = this.providerConfig?.getEphemeralSettings?.()?.['streaming'];
681
394
  const streamingEnabled = streamingSetting !== 'disabled';
682
- return {
683
- parser,
684
- currentToolFormat,
685
- formattedTools,
686
- finalStreamOptions,
687
- streamingEnabled,
395
+ // Get the system prompt
396
+ const userMemory = this.globalConfig?.getUserMemory
397
+ ? this.globalConfig.getUserMemory()
398
+ : '';
399
+ const systemPrompt = await getCoreSystemPromptAsync(userMemory, model, undefined);
400
+ // Add system prompt as the first message in the array
401
+ const messagesWithSystem = [
402
+ { role: 'system', content: systemPrompt },
403
+ ...messages,
404
+ ];
405
+ // Build request - only include tools if they exist and are not empty
406
+ const requestBody = {
407
+ model,
408
+ messages: messagesWithSystem,
409
+ ...(formattedTools && formattedTools.length > 0
410
+ ? {
411
+ tools: formattedTools,
412
+ // Add tool_choice for Qwen/Cerebras to ensure proper tool calling
413
+ tool_choice: 'auto',
414
+ }
415
+ : {}),
416
+ max_tokens: maxTokens,
417
+ stream: streamingEnabled,
688
418
  };
689
- }
690
- /**
691
- * 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,
419
+ // Debug log request summary for Cerebras/Qwen
420
+ if (this.logger.enabled &&
421
+ (model.toLowerCase().includes('qwen') ||
422
+ this.getBaseURL()?.includes('cerebras'))) {
423
+ this.logger.debug(() => `Request to ${this.getBaseURL()} for model ${model}:`, {
424
+ baseURL: this.getBaseURL(),
425
+ model,
426
+ streamingEnabled,
427
+ hasTools: 'tools' in requestBody,
428
+ toolCount: formattedTools?.length || 0,
429
+ messageCount: messages.length,
430
+ toolsInRequest: 'tools' in requestBody ? requestBody.tools?.length : 'not included',
711
431
  });
712
432
  }
713
- 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;
433
+ // Get OpenAI client
434
+ const client = await this.getClient();
435
+ // Get retry settings from ephemeral settings
436
+ const ephemeralSettings = this.providerConfig?.getEphemeralSettings?.() || {};
437
+ const maxRetries = ephemeralSettings['retries'] ?? 6; // Default for OpenAI
438
+ const initialDelayMs = ephemeralSettings['retrywait'] ?? 4000; // Default for OpenAI
439
+ // Wrap the API call with retry logic using centralized retry utility
440
+ const response = await retryWithBackoff(() => client.chat.completions.create(requestBody, { signal: abortSignal }), {
441
+ maxAttempts: maxRetries,
442
+ initialDelayMs,
443
+ maxDelayMs: 30000, // 30 seconds
444
+ shouldRetry: this.shouldRetryResponse.bind(this),
445
+ });
446
+ // Check if response is streaming or not
447
+ if (streamingEnabled) {
448
+ // Process streaming response
449
+ let _accumulatedText = '';
450
+ const accumulatedToolCalls = [];
451
+ // Buffer for accumulating text chunks for providers that need it
452
+ let textBuffer = '';
453
+ const detectedFormat = this.detectToolFormat();
454
+ // Buffer text for Qwen format providers to avoid stanza formatting
455
+ const shouldBufferText = detectedFormat === 'qwen';
456
+ try {
457
+ // Handle streaming response
458
+ for await (const chunk of response) {
459
+ if (abortSignal?.aborted) {
460
+ break;
461
+ }
462
+ const choice = chunk.choices?.[0];
463
+ if (!choice)
464
+ continue;
465
+ // Check for finish_reason to detect proper stream ending
466
+ if (choice.finish_reason) {
467
+ this.logger.debug(() => `[Streaming] Stream finished with reason: ${choice.finish_reason}`, {
468
+ model,
469
+ finishReason: choice.finish_reason,
470
+ hasAccumulatedText: _accumulatedText.length > 0,
471
+ hasAccumulatedTools: accumulatedToolCalls.length > 0,
472
+ hasBufferedText: textBuffer.length > 0,
473
+ });
474
+ // If finish_reason is 'length', the response was cut off
475
+ if (choice.finish_reason === 'length') {
476
+ this.logger.debug(() => `Response truncated due to length limit for model ${model}`);
477
+ }
478
+ // Flush any buffered text when stream finishes
479
+ if (textBuffer.length > 0) {
480
+ yield {
481
+ speaker: 'ai',
482
+ blocks: [
483
+ {
484
+ type: 'text',
485
+ text: textBuffer,
486
+ },
487
+ ],
488
+ };
489
+ textBuffer = '';
490
+ }
491
+ }
492
+ // Handle text content - buffer for Qwen format, emit immediately for others
493
+ const deltaContent = choice.delta?.content;
494
+ if (deltaContent) {
495
+ _accumulatedText += deltaContent;
496
+ // Debug log for providers that need buffering
497
+ if (shouldBufferText) {
498
+ this.logger.debug(() => `[Streaming] Chunk content for ${detectedFormat} format:`, {
499
+ deltaContent,
500
+ length: deltaContent.length,
501
+ hasNewline: deltaContent.includes('\n'),
502
+ escaped: JSON.stringify(deltaContent),
503
+ bufferSize: textBuffer.length,
504
+ });
505
+ // Buffer text to avoid stanza formatting
506
+ textBuffer += deltaContent;
507
+ // Emit buffered text when we have a complete sentence or paragraph
508
+ // Look for natural break points
509
+ if (textBuffer.includes('\n') ||
510
+ textBuffer.endsWith('. ') ||
511
+ textBuffer.endsWith('! ') ||
512
+ textBuffer.endsWith('? ') ||
513
+ textBuffer.length > 100) {
514
+ yield {
515
+ speaker: 'ai',
516
+ blocks: [
517
+ {
518
+ type: 'text',
519
+ text: textBuffer,
520
+ },
521
+ ],
522
+ };
523
+ textBuffer = '';
524
+ }
525
+ }
526
+ else {
527
+ // For other providers, emit text immediately as before
528
+ yield {
529
+ speaker: 'ai',
530
+ blocks: [
531
+ {
532
+ type: 'text',
533
+ text: deltaContent,
534
+ },
535
+ ],
536
+ };
537
+ }
538
+ }
539
+ // Handle tool calls
540
+ const deltaToolCalls = choice.delta?.tool_calls;
541
+ if (deltaToolCalls && deltaToolCalls.length > 0) {
542
+ for (const deltaToolCall of deltaToolCalls) {
543
+ if (deltaToolCall.index === undefined)
544
+ continue;
545
+ // Initialize or update accumulated tool call
546
+ if (!accumulatedToolCalls[deltaToolCall.index]) {
547
+ accumulatedToolCalls[deltaToolCall.index] = {
548
+ id: deltaToolCall.id || '',
549
+ type: 'function',
550
+ function: {
551
+ name: deltaToolCall.function?.name || '',
552
+ arguments: '',
553
+ },
554
+ };
555
+ }
556
+ const tc = accumulatedToolCalls[deltaToolCall.index];
557
+ if (tc) {
558
+ if (deltaToolCall.id)
559
+ tc.id = deltaToolCall.id;
560
+ if (deltaToolCall.function?.name)
561
+ tc.function.name = deltaToolCall.function.name;
562
+ if (deltaToolCall.function?.arguments) {
563
+ tc.function.arguments += deltaToolCall.function.arguments;
564
+ }
565
+ }
566
+ }
567
+ }
741
568
  }
742
- return false;
743
- });
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
- });
569
+ }
570
+ catch (error) {
571
+ if (abortSignal?.aborted) {
572
+ throw error;
753
573
  }
754
- });
755
- }
756
- }
757
- /**
758
- * Process non-streaming response
759
- */
760
- processNonStreamingResponse(response) {
761
- const choice = response.choices[0];
762
- let fullContent = '';
763
- const accumulatedToolCalls = [];
764
- let usageData;
765
- if (choice?.message.content) {
766
- fullContent = choice.message.content;
767
- }
768
- if (choice?.message.tool_calls) {
769
- // Convert tool calls to the standard format
770
- for (const toolCall of choice.message.tool_calls) {
771
- if (toolCall.type === 'function' && toolCall.function) {
772
- // Don't fix double stringification here - it's handled later in the final processing
773
- accumulatedToolCalls.push({
774
- id: toolCall.id,
775
- type: 'function',
776
- function: toolCall.function,
777
- });
574
+ else {
575
+ this.logger.error('Error processing streaming response:', error);
576
+ throw error;
778
577
  }
779
578
  }
780
- }
781
- 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
- }));
579
+ // Flush any remaining buffered text
580
+ if (textBuffer.length > 0) {
820
581
  yield {
821
- role: ContentGeneratorRole.ASSISTANT,
822
- content: cleanedContent,
823
- tool_calls: standardToolCalls,
824
- usage: usageData,
582
+ speaker: 'ai',
583
+ blocks: [
584
+ {
585
+ type: 'text',
586
+ text: textBuffer,
587
+ },
588
+ ],
825
589
  };
590
+ textBuffer = '';
826
591
  }
827
- else {
828
- // No tool calls found, yield cleaned content
829
- yield {
830
- role: ContentGeneratorRole.ASSISTANT,
831
- content: cleanedContent,
832
- usage: usageData,
833
- };
592
+ // Emit accumulated tool calls as IContent if any
593
+ if (accumulatedToolCalls.length > 0) {
594
+ const blocks = [];
595
+ const detectedFormat = this.detectToolFormat();
596
+ for (const tc of accumulatedToolCalls) {
597
+ if (!tc)
598
+ continue;
599
+ // Process tool parameters with double-escape handling
600
+ const processedParameters = processToolParameters(tc.function.arguments || '', tc.function.name || '', detectedFormat);
601
+ blocks.push({
602
+ type: 'tool_call',
603
+ id: this.normalizeToHistoryToolId(tc.id),
604
+ name: tc.function.name || '',
605
+ parameters: processedParameters,
606
+ });
607
+ }
608
+ if (blocks.length > 0) {
609
+ yield {
610
+ speaker: 'ai',
611
+ blocks,
612
+ };
613
+ }
834
614
  }
835
615
  }
836
616
  else {
837
- // 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
- })}`);
617
+ // Handle non-streaming response
618
+ const completion = response;
619
+ const choice = completion.choices?.[0];
620
+ if (!choice) {
621
+ throw new Error('No choices in completion response');
622
+ }
623
+ const blocks = [];
624
+ // Handle text content
625
+ if (choice.message?.content) {
626
+ blocks.push({
627
+ type: 'text',
628
+ text: choice.message.content,
629
+ });
630
+ }
631
+ // Handle tool calls
632
+ if (choice.message?.tool_calls && choice.message.tool_calls.length > 0) {
633
+ const detectedFormat = this.detectToolFormat();
634
+ for (const toolCall of choice.message.tool_calls) {
635
+ if (toolCall.type === 'function') {
636
+ // Process tool parameters with double-escape handling
637
+ const processedParameters = processToolParameters(toolCall.function.arguments || '', toolCall.function.name || '', detectedFormat);
638
+ blocks.push({
639
+ type: 'tool_call',
640
+ id: this.normalizeToHistoryToolId(toolCall.id),
641
+ name: toolCall.function.name || '',
642
+ parameters: processedParameters,
643
+ });
644
+ }
849
645
  }
850
- // Build the final message based on provider-specific requirements
851
- const finalMessage = this.buildFinalToolCallMessage(hasStreamedContent, finalFullContent, fixedToolCalls, usageData);
852
- yield finalMessage;
853
646
  }
854
- else if (usageData) {
855
- // Always emit usage data so downstream consumers can update stats
647
+ // Emit the complete response as a single IContent
648
+ if (blocks.length > 0) {
856
649
  yield {
857
- role: ContentGeneratorRole.ASSISTANT,
858
- content: '',
859
- usage: usageData,
650
+ speaker: 'ai',
651
+ blocks,
860
652
  };
861
653
  }
862
654
  }
863
655
  }
864
656
  /**
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
657
+ * Detects the tool call format based on the model being used
658
+ * @returns The detected tool format ('openai' or 'qwen')
870
659
  */
871
- handleQwenStreamingWhitespace(delta, pendingWhitespace, fullContent) {
872
- if (!delta.content) {
873
- return {
874
- shouldYield: false,
875
- content: '',
876
- updatedPendingWhitespace: pendingWhitespace,
877
- updatedFullContent: fullContent,
878
- };
660
+ detectToolFormat() {
661
+ try {
662
+ // Try to get format from SettingsService if available
663
+ const settings = this.providerConfig?.getEphemeralSettings?.();
664
+ if (settings && settings['tool-format']) {
665
+ return settings['tool-format'];
666
+ }
879
667
  }
880
- 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
- };
668
+ catch (error) {
669
+ this.logger.debug(() => `Failed to detect tool format from SettingsService: ${error}`);
891
670
  }
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
- };
671
+ // Fallback detection without SettingsService - always look up current model
672
+ const modelName = (this.getModel() || this.getDefaultModel()).toLowerCase();
673
+ if (modelName.includes('glm-4.5') || modelName.includes('glm-4-5')) {
674
+ return 'qwen';
901
675
  }
902
- 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;
676
+ if (modelName.includes('qwen')) {
677
+ return 'qwen';
917
678
  }
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
- });
679
+ return 'openai';
928
680
  }
929
681
  /**
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
682
+ * Parse tool response from API (placeholder for future response parsing)
683
+ * @param response The raw API response
684
+ * @returns Parsed tool response
936
685
  */
937
- 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
- };
686
+ parseToolResponse(response) {
687
+ // TODO: Implement response parsing based on detected format
688
+ // For now, return the response as-is
689
+ return response;
973
690
  }
974
691
  /**
975
- * 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
692
+ * Determines whether a response should be retried based on error codes
693
+ * @param error The error object from the API response
694
+ * @returns true if the request should be retried, false otherwise
979
695
  */
980
- 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
- }
696
+ shouldRetryResponse(error) {
697
+ // Don't retry if we're streaming chunks - just continue processing
698
+ if (error &&
699
+ typeof error === 'object' &&
700
+ 'status' in error &&
701
+ error.status === 200) {
702
+ return false;
1072
703
  }
1073
- // No fix needed
1074
- this.logger.debug(() => `[Qwen Fix] No double-stringification detected for ${toolCall.function.name}, keeping original`);
1075
- return toolCall;
704
+ // Retry on 429 rate limit errors or 5xx server errors
705
+ const shouldRetry = Boolean(error &&
706
+ typeof error === 'object' &&
707
+ 'status' in error &&
708
+ (error.status === 429 ||
709
+ (error.status >= 500 &&
710
+ error.status < 600)));
711
+ return shouldRetry;
1076
712
  }
1077
713
  }
1078
714
  //# sourceMappingURL=OpenAIProvider.js.map