converse-mcp-server 2.9.7 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/converse.js +0 -0
- package/package.json +56 -56
- package/src/async/fileCache.js +36 -3
- package/src/providers/gemini-cli.js +67 -18
- package/src/tools/chat.js +80 -29
- package/src/tools/checkStatus.js +17 -2
- package/src/utils/idValidation.js +32 -0
package/bin/converse.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "converse-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Converse MCP Server - Converse with other LLMs with chat and consensus tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -11,6 +11,60 @@
|
|
|
11
11
|
"engines": {
|
|
12
12
|
"node": ">=20.0.0"
|
|
13
13
|
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"server",
|
|
17
|
+
"ai",
|
|
18
|
+
"chat",
|
|
19
|
+
"consensus",
|
|
20
|
+
"openai",
|
|
21
|
+
"google",
|
|
22
|
+
"gemini",
|
|
23
|
+
"grok"
|
|
24
|
+
],
|
|
25
|
+
"author": "Converse MCP Server",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://github.com/FallDownTheSystem/converse#readme",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/FallDownTheSystem/converse.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/FallDownTheSystem/converse/issues"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"src/",
|
|
37
|
+
"bin/",
|
|
38
|
+
"docs/",
|
|
39
|
+
"README.md",
|
|
40
|
+
".env.example"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.23",
|
|
44
|
+
"@anthropic-ai/sdk": "^0.72.0",
|
|
45
|
+
"@google/genai": "^1.38.0",
|
|
46
|
+
"@mistralai/mistralai": "^1.13.0",
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
48
|
+
"@openai/codex-sdk": "^0.92.0",
|
|
49
|
+
"ai": "^6.0.62",
|
|
50
|
+
"ai-sdk-provider-gemini-cli": "^2.0.1",
|
|
51
|
+
"cors": "^2.8.6",
|
|
52
|
+
"dotenv": "^17.2.3",
|
|
53
|
+
"express": "^5.2.1",
|
|
54
|
+
"lru-cache": "^11.2.5",
|
|
55
|
+
"nanoid": "^5.1.6",
|
|
56
|
+
"openai": "^6.17.0",
|
|
57
|
+
"p-limit": "^7.2.0",
|
|
58
|
+
"vite": "^7.3.1"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
62
|
+
"cross-env": "^10.1.0",
|
|
63
|
+
"eslint": "^9.39.2",
|
|
64
|
+
"prettier": "^3.8.1",
|
|
65
|
+
"rimraf": "^6.1.2",
|
|
66
|
+
"vitest": "^4.0.18"
|
|
67
|
+
},
|
|
14
68
|
"scripts": {
|
|
15
69
|
"kill-server": "node scripts/kill-server.js",
|
|
16
70
|
"start": "npm run kill-server && node src/index.js",
|
|
@@ -64,59 +118,5 @@
|
|
|
64
118
|
"validate:fix": "node scripts/validate.js --fix",
|
|
65
119
|
"validate:fast": "node scripts/validate.js --skip-tests --skip-lint",
|
|
66
120
|
"precommit": "npm run validate"
|
|
67
|
-
},
|
|
68
|
-
"keywords": [
|
|
69
|
-
"mcp",
|
|
70
|
-
"server",
|
|
71
|
-
"ai",
|
|
72
|
-
"chat",
|
|
73
|
-
"consensus",
|
|
74
|
-
"openai",
|
|
75
|
-
"google",
|
|
76
|
-
"gemini",
|
|
77
|
-
"grok"
|
|
78
|
-
],
|
|
79
|
-
"author": "Converse MCP Server",
|
|
80
|
-
"license": "MIT",
|
|
81
|
-
"homepage": "https://github.com/FallDownTheSystem/converse#readme",
|
|
82
|
-
"repository": {
|
|
83
|
-
"type": "git",
|
|
84
|
-
"url": "git+https://github.com/FallDownTheSystem/converse.git"
|
|
85
|
-
},
|
|
86
|
-
"bugs": {
|
|
87
|
-
"url": "https://github.com/FallDownTheSystem/converse/issues"
|
|
88
|
-
},
|
|
89
|
-
"files": [
|
|
90
|
-
"src/",
|
|
91
|
-
"bin/",
|
|
92
|
-
"docs/",
|
|
93
|
-
"README.md",
|
|
94
|
-
".env.example"
|
|
95
|
-
],
|
|
96
|
-
"dependencies": {
|
|
97
|
-
"@anthropic-ai/claude-agent-sdk": "^0.1.51",
|
|
98
|
-
"@anthropic-ai/sdk": "^0.70.0",
|
|
99
|
-
"@google/genai": "^1.30.0",
|
|
100
|
-
"@mistralai/mistralai": "^1.10.0",
|
|
101
|
-
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
102
|
-
"@openai/codex-sdk": "^0.63.0",
|
|
103
|
-
"ai": "^5.0.101",
|
|
104
|
-
"ai-sdk-provider-gemini-cli": "^1.4.0",
|
|
105
|
-
"cors": "^2.8.5",
|
|
106
|
-
"dotenv": "^17.2.3",
|
|
107
|
-
"express": "^5.1.0",
|
|
108
|
-
"lru-cache": "^11.2.2",
|
|
109
|
-
"nanoid": "^5.1.6",
|
|
110
|
-
"openai": "^6.9.1",
|
|
111
|
-
"p-limit": "^7.2.0",
|
|
112
|
-
"vite": "^7.2.2"
|
|
113
|
-
},
|
|
114
|
-
"devDependencies": {
|
|
115
|
-
"@vitest/coverage-v8": "^4.0.10",
|
|
116
|
-
"cross-env": "^10.1.0",
|
|
117
|
-
"eslint": "^9.39.1",
|
|
118
|
-
"prettier": "^3.6.2",
|
|
119
|
-
"rimraf": "^6.1.0",
|
|
120
|
-
"vitest": "^4.0.10"
|
|
121
121
|
}
|
|
122
|
-
}
|
|
122
|
+
}
|
package/src/async/fileCache.js
CHANGED
|
@@ -11,6 +11,7 @@ import { promises as fs } from 'fs';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { debugLog, debugError } from '../utils/console.js';
|
|
13
13
|
import { ConverseMCPError, ERROR_CODES } from '../utils/errorHandler.js';
|
|
14
|
+
import { isSafeIdSegment } from '../utils/idValidation.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* File cache specific error class
|
|
@@ -149,6 +150,14 @@ export class FileCache extends FileCacheInterface {
|
|
|
149
150
|
* @private
|
|
150
151
|
*/
|
|
151
152
|
getJobDir(jobId) {
|
|
153
|
+
if (!isSafeIdSegment(jobId)) {
|
|
154
|
+
throw new FileCacheError(
|
|
155
|
+
'Job ID contains unsafe characters',
|
|
156
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
157
|
+
{ jobId },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
152
161
|
const today = new Date().toISOString().split('T')[0]; // yyyy-mm-dd
|
|
153
162
|
return path.join(this.baseDir, today, jobId);
|
|
154
163
|
}
|
|
@@ -201,7 +210,15 @@ export class FileCache extends FileCacheInterface {
|
|
|
201
210
|
if (!jobId || typeof jobId !== 'string') {
|
|
202
211
|
throw new FileCacheError(
|
|
203
212
|
'Job ID must be a non-empty string',
|
|
204
|
-
ERROR_CODES.
|
|
213
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
214
|
+
{ jobId },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!isSafeIdSegment(jobId)) {
|
|
219
|
+
throw new FileCacheError(
|
|
220
|
+
'Job ID contains unsafe characters',
|
|
221
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
205
222
|
{ jobId },
|
|
206
223
|
);
|
|
207
224
|
}
|
|
@@ -265,7 +282,15 @@ export class FileCache extends FileCacheInterface {
|
|
|
265
282
|
if (!jobId || typeof jobId !== 'string') {
|
|
266
283
|
throw new FileCacheError(
|
|
267
284
|
'Job ID must be a non-empty string',
|
|
268
|
-
ERROR_CODES.
|
|
285
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
286
|
+
{ jobId },
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (!isSafeIdSegment(jobId)) {
|
|
291
|
+
throw new FileCacheError(
|
|
292
|
+
'Job ID contains unsafe characters',
|
|
293
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
269
294
|
{ jobId },
|
|
270
295
|
);
|
|
271
296
|
}
|
|
@@ -324,7 +349,15 @@ export class FileCache extends FileCacheInterface {
|
|
|
324
349
|
if (!jobId || typeof jobId !== 'string') {
|
|
325
350
|
throw new FileCacheError(
|
|
326
351
|
'Job ID must be a non-empty string',
|
|
327
|
-
ERROR_CODES.
|
|
352
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
353
|
+
{ jobId },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (!isSafeIdSegment(jobId)) {
|
|
358
|
+
throw new FileCacheError(
|
|
359
|
+
'Job ID contains unsafe characters',
|
|
360
|
+
ERROR_CODES.VALIDATION_ERROR,
|
|
328
361
|
{ jobId },
|
|
329
362
|
);
|
|
330
363
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Key features:
|
|
8
8
|
* - Uses OAuth authentication from Gemini CLI (no API keys needed)
|
|
9
9
|
* - Supports gemini-3-pro-preview model via Google Cloud Code endpoints
|
|
10
|
-
* - Uses AI SDK
|
|
10
|
+
* - Uses AI SDK v6 standard interfaces (generateText/streamText)
|
|
11
11
|
* - Compatible with both chat and consensus tools
|
|
12
12
|
*
|
|
13
13
|
* Authentication:
|
|
@@ -156,12 +156,13 @@ async function* createStreamingGenerator(
|
|
|
156
156
|
|
|
157
157
|
// Yield usage event
|
|
158
158
|
if (usage) {
|
|
159
|
+
const tokens = extractUsageTokens(usage);
|
|
159
160
|
yield {
|
|
160
161
|
type: 'usage',
|
|
161
162
|
usage: {
|
|
162
|
-
input_tokens:
|
|
163
|
-
output_tokens:
|
|
164
|
-
total_tokens:
|
|
163
|
+
input_tokens: tokens.input,
|
|
164
|
+
output_tokens: tokens.output,
|
|
165
|
+
total_tokens: tokens.total,
|
|
165
166
|
cached_input_tokens: 0,
|
|
166
167
|
},
|
|
167
168
|
};
|
|
@@ -171,7 +172,7 @@ async function* createStreamingGenerator(
|
|
|
171
172
|
yield {
|
|
172
173
|
type: 'end',
|
|
173
174
|
stop_reason: mapFinishReason(finishReason),
|
|
174
|
-
finish_reason: finishReason,
|
|
175
|
+
finish_reason: getRawFinishReason(finishReason),
|
|
175
176
|
};
|
|
176
177
|
} catch (error) {
|
|
177
178
|
if (signal?.aborted) {
|
|
@@ -182,10 +183,16 @@ async function* createStreamingGenerator(
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
/**
|
|
185
|
-
* Map AI SDK finish reasons to our StopReasons enum
|
|
186
|
+
* Map AI SDK v6 finish reasons to our StopReasons enum
|
|
187
|
+
* AI SDK v6 returns finishReason as { unified: 'stop', raw: 'STOP' }
|
|
188
|
+
* @param {Object|string} finishReason - The finish reason (object in v6, string in v5)
|
|
186
189
|
*/
|
|
187
190
|
function mapFinishReason(finishReason) {
|
|
188
|
-
|
|
191
|
+
// AI SDK v6 returns an object with 'unified' property
|
|
192
|
+
const reason =
|
|
193
|
+
typeof finishReason === 'object' ? finishReason?.unified : finishReason;
|
|
194
|
+
|
|
195
|
+
switch (reason) {
|
|
189
196
|
case 'stop':
|
|
190
197
|
return StopReasons.STOP;
|
|
191
198
|
case 'length':
|
|
@@ -203,12 +210,51 @@ function mapFinishReason(finishReason) {
|
|
|
203
210
|
}
|
|
204
211
|
|
|
205
212
|
/**
|
|
206
|
-
*
|
|
213
|
+
* Extract raw finish reason string for metadata
|
|
214
|
+
* AI SDK v6 returns finishReason as { unified: 'stop', raw: 'STOP' }
|
|
215
|
+
* @param {Object|string} finishReason - The finish reason
|
|
216
|
+
* @returns {string} The raw finish reason string
|
|
217
|
+
*/
|
|
218
|
+
function getRawFinishReason(finishReason) {
|
|
219
|
+
if (typeof finishReason === 'object') {
|
|
220
|
+
return finishReason?.unified || finishReason?.raw || 'stop';
|
|
221
|
+
}
|
|
222
|
+
return finishReason || 'stop';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract usage tokens from AI SDK v6 hierarchical structure
|
|
227
|
+
* AI SDK v6 usage: { inputTokens: { total: N }, outputTokens: { total: N } }
|
|
228
|
+
* AI SDK v5 usage: { promptTokens: N, completionTokens: N, totalTokens: N }
|
|
229
|
+
* @param {Object} usage - The usage object from AI SDK
|
|
230
|
+
* @returns {Object} Normalized token counts
|
|
231
|
+
*/
|
|
232
|
+
function extractUsageTokens(usage) {
|
|
233
|
+
if (!usage) {
|
|
234
|
+
return { input: 0, output: 0, total: 0 };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// AI SDK v6 hierarchical structure
|
|
238
|
+
if (usage.inputTokens && typeof usage.inputTokens === 'object') {
|
|
239
|
+
const input = usage.inputTokens.total || 0;
|
|
240
|
+
const output = usage.outputTokens?.total || 0;
|
|
241
|
+
return { input, output, total: input + output };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// AI SDK flat structure (backwards compatibility)
|
|
245
|
+
const input = usage.promptTokens || usage.inputTokens || 0;
|
|
246
|
+
const output = usage.completionTokens || usage.outputTokens || 0;
|
|
247
|
+
const total = usage.totalTokens || input + output;
|
|
248
|
+
return { input, output, total };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Convert messages from Converse internal format to AI SDK ModelMessage format
|
|
207
253
|
*
|
|
208
254
|
* Converse format (used by other providers like Anthropic):
|
|
209
255
|
* - Images: { type: 'image', source: { type: 'base64', media_type: '...', data: '...' } }
|
|
210
256
|
*
|
|
211
|
-
* AI SDK
|
|
257
|
+
* AI SDK ModelMessage format (required by generateText/streamText):
|
|
212
258
|
* - Images: { type: 'image', image: '...' } (base64 string, Buffer, or URL)
|
|
213
259
|
* - Text: { type: 'text', text: '...' }
|
|
214
260
|
*
|
|
@@ -216,7 +262,7 @@ function mapFinishReason(finishReason) {
|
|
|
216
262
|
* We must use 'image' property (not 'data') for the AI SDK to accept the message.
|
|
217
263
|
*
|
|
218
264
|
* @param {Array} messages - Messages in Converse internal format
|
|
219
|
-
* @returns {Array} Messages in AI SDK
|
|
265
|
+
* @returns {Array} Messages in AI SDK ModelMessage format
|
|
220
266
|
*/
|
|
221
267
|
function convertToModelMessages(messages) {
|
|
222
268
|
return messages.map((message) => {
|
|
@@ -233,11 +279,11 @@ function convertToModelMessages(messages) {
|
|
|
233
279
|
return part;
|
|
234
280
|
}
|
|
235
281
|
|
|
236
|
-
// Convert image from Converse format to AI SDK
|
|
282
|
+
// Convert image from Converse format to AI SDK ModelMessage format
|
|
237
283
|
if (part.type === 'image' && part.source) {
|
|
238
284
|
return {
|
|
239
285
|
type: 'image',
|
|
240
|
-
image: part.source.data, // AI SDK
|
|
286
|
+
image: part.source.data, // AI SDK expects 'image' property (not 'data')
|
|
241
287
|
};
|
|
242
288
|
}
|
|
243
289
|
|
|
@@ -329,7 +375,7 @@ export const geminiCliProvider = {
|
|
|
329
375
|
// Create model instance with SDK model name
|
|
330
376
|
const modelInstance = gemini(sdkModelName);
|
|
331
377
|
|
|
332
|
-
// Convert messages from Converse format to AI SDK
|
|
378
|
+
// Convert messages from Converse format to AI SDK ModelMessage format
|
|
333
379
|
const convertedMessages = convertToModelMessages(messages);
|
|
334
380
|
|
|
335
381
|
// Build AI SDK options
|
|
@@ -377,9 +423,12 @@ export const geminiCliProvider = {
|
|
|
377
423
|
|
|
378
424
|
const responseTime = Date.now() - startTime;
|
|
379
425
|
|
|
380
|
-
// Extract content from AI SDK
|
|
426
|
+
// Extract content from AI SDK v6 response format
|
|
381
427
|
const content = result.content?.[0]?.text || result.text || '';
|
|
382
428
|
|
|
429
|
+
// Extract usage tokens with AI SDK v6 compatibility
|
|
430
|
+
const tokens = extractUsageTokens(result.usage);
|
|
431
|
+
|
|
383
432
|
return {
|
|
384
433
|
content,
|
|
385
434
|
stop_reason: mapFinishReason(result.finishReason),
|
|
@@ -389,14 +438,14 @@ export const geminiCliProvider = {
|
|
|
389
438
|
model,
|
|
390
439
|
usage: result.usage
|
|
391
440
|
? {
|
|
392
|
-
input_tokens:
|
|
393
|
-
output_tokens:
|
|
394
|
-
total_tokens:
|
|
441
|
+
input_tokens: tokens.input,
|
|
442
|
+
output_tokens: tokens.output,
|
|
443
|
+
total_tokens: tokens.total,
|
|
395
444
|
cached_input_tokens: 0,
|
|
396
445
|
}
|
|
397
446
|
: null,
|
|
398
447
|
response_time_ms: responseTime,
|
|
399
|
-
finish_reason: result.finishReason
|
|
448
|
+
finish_reason: getRawFinishReason(result.finishReason),
|
|
400
449
|
},
|
|
401
450
|
};
|
|
402
451
|
} catch (error) {
|
package/src/tools/chat.js
CHANGED
|
@@ -21,6 +21,7 @@ import { applyTokenLimit, getTokenLimit } from '../utils/tokenLimiter.js';
|
|
|
21
21
|
import { validateAllPaths } from '../utils/fileValidator.js';
|
|
22
22
|
import { SummarizationService } from '../services/summarizationService.js';
|
|
23
23
|
import { exportConversation } from '../utils/conversationExporter.js';
|
|
24
|
+
import { isRecoverableError, retryWithBackoff } from '../utils/errorHandler.js';
|
|
24
25
|
|
|
25
26
|
const logger = createLogger('chat');
|
|
26
27
|
|
|
@@ -265,12 +266,14 @@ export async function chatTool(args, dependencies) {
|
|
|
265
266
|
|
|
266
267
|
messages.push(userMessage);
|
|
267
268
|
|
|
268
|
-
// Select provider
|
|
269
|
+
// Select provider(s)
|
|
269
270
|
let selectedProvider;
|
|
270
271
|
let providerName;
|
|
271
272
|
|
|
273
|
+
const providerCandidates = [];
|
|
274
|
+
|
|
272
275
|
if (model === 'auto') {
|
|
273
|
-
// Auto-select
|
|
276
|
+
// Auto-select providers in priority order
|
|
274
277
|
// Prioritize subscription-based providers (codex, gemini-cli, claude) over API-key providers
|
|
275
278
|
const providerOrder = [
|
|
276
279
|
'codex',
|
|
@@ -288,20 +291,17 @@ export async function chatTool(args, dependencies) {
|
|
|
288
291
|
for (const name of providerOrder) {
|
|
289
292
|
const provider = providers[name];
|
|
290
293
|
if (provider && provider.isAvailable && provider.isAvailable(config)) {
|
|
291
|
-
|
|
292
|
-
selectedProvider = provider;
|
|
293
|
-
break;
|
|
294
|
+
providerCandidates.push({ name, provider });
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
if (
|
|
298
|
+
if (providerCandidates.length === 0) {
|
|
298
299
|
return createToolError(
|
|
299
300
|
'No providers available. Please configure at least one API key.',
|
|
300
301
|
);
|
|
301
302
|
}
|
|
302
303
|
} else {
|
|
303
304
|
// Use specified provider/model
|
|
304
|
-
// Try to map model to provider
|
|
305
305
|
providerName = mapModelToProvider(model, providers);
|
|
306
306
|
selectedProvider = providers[providerName];
|
|
307
307
|
|
|
@@ -314,32 +314,60 @@ export async function chatTool(args, dependencies) {
|
|
|
314
314
|
`Provider ${providerName} is not available. Check API key configuration.`,
|
|
315
315
|
);
|
|
316
316
|
}
|
|
317
|
-
}
|
|
318
317
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const providerOptions = {
|
|
322
|
-
model: resolvedModel,
|
|
323
|
-
temperature,
|
|
324
|
-
reasoning_effort,
|
|
325
|
-
verbosity,
|
|
326
|
-
use_websearch,
|
|
327
|
-
config,
|
|
328
|
-
continuation_id, // Pass for thread resumption
|
|
329
|
-
continuationStore, // Pass store for state management
|
|
330
|
-
};
|
|
318
|
+
providerCandidates.push({ name: providerName, provider: selectedProvider });
|
|
319
|
+
}
|
|
331
320
|
|
|
332
|
-
// Call provider
|
|
321
|
+
// Call provider with recovery (retry and, for auto, failover)
|
|
333
322
|
let response;
|
|
334
323
|
const startTime = Date.now();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
324
|
+
let lastError;
|
|
325
|
+
let resolvedModel;
|
|
326
|
+
|
|
327
|
+
for (const candidate of providerCandidates) {
|
|
328
|
+
providerName = candidate.name;
|
|
329
|
+
selectedProvider = candidate.provider;
|
|
330
|
+
|
|
331
|
+
// Resolve model name and prepare provider options
|
|
332
|
+
resolvedModel = resolveAutoModel(model, providerName);
|
|
333
|
+
const providerOptions = {
|
|
334
|
+
model: resolvedModel,
|
|
335
|
+
temperature,
|
|
336
|
+
reasoning_effort,
|
|
337
|
+
verbosity,
|
|
338
|
+
use_websearch,
|
|
339
|
+
config,
|
|
340
|
+
continuation_id, // Pass for thread resumption
|
|
341
|
+
continuationStore, // Pass store for state management
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
response = await retryWithBackoff(
|
|
346
|
+
() => selectedProvider.invoke(messages, providerOptions),
|
|
347
|
+
getProviderRetryOptions(config, providerName),
|
|
348
|
+
);
|
|
349
|
+
break;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
lastError = error;
|
|
352
|
+
logger.error('Provider error', {
|
|
353
|
+
error,
|
|
354
|
+
data: { provider: providerName },
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (
|
|
358
|
+
model !== 'auto' ||
|
|
359
|
+
!shouldFailoverToNextProvider(error) ||
|
|
360
|
+
candidate === providerCandidates[providerCandidates.length - 1]
|
|
361
|
+
) {
|
|
362
|
+
return createToolError(`Provider error: ${error.message}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!response) {
|
|
368
|
+
return createToolError(
|
|
369
|
+
`Provider error: ${(lastError && lastError.message) || 'Unknown error'}`,
|
|
370
|
+
);
|
|
343
371
|
}
|
|
344
372
|
const executionTime = (Date.now() - startTime) / 1000; // Convert to seconds
|
|
345
373
|
|
|
@@ -472,6 +500,29 @@ function resolveAutoModel(model, providerName) {
|
|
|
472
500
|
return defaults[providerName] || 'gpt-5';
|
|
473
501
|
}
|
|
474
502
|
|
|
503
|
+
function getProviderRetryOptions(config, providerName) {
|
|
504
|
+
const nodeEnv = config?.environment?.nodeEnv || process.env.NODE_ENV;
|
|
505
|
+
const isTest = nodeEnv === 'test';
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
retries: isTest ? 1 : 3,
|
|
509
|
+
delay: isTest ? 0 : 500,
|
|
510
|
+
maxDelay: isTest ? 0 : 10000,
|
|
511
|
+
operation: `provider-invoke:${providerName}`,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function shouldFailoverToNextProvider(error) {
|
|
516
|
+
if (isRecoverableError(error)) {
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const message = (error && error.message) || '';
|
|
521
|
+
return /(api key|authentication|unauthorized|forbidden|invalid|not available)/i.test(
|
|
522
|
+
message,
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
475
526
|
export function mapModelToProvider(model, providers) {
|
|
476
527
|
const modelLower = model.toLowerCase();
|
|
477
528
|
|
package/src/tools/checkStatus.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
formatJobListHumanReadable,
|
|
18
18
|
formatConversationHistory,
|
|
19
19
|
} from '../utils/formatStatus.js';
|
|
20
|
+
import { isSafeIdSegment } from '../utils/idValidation.js';
|
|
20
21
|
|
|
21
22
|
const logger = createLogger('check-status');
|
|
22
23
|
|
|
@@ -36,8 +37,22 @@ export async function checkStatusTool(args, dependencies) {
|
|
|
36
37
|
const { continuation_id, full_history = false } = args;
|
|
37
38
|
|
|
38
39
|
// Validate arguments
|
|
39
|
-
if (continuation_id
|
|
40
|
-
|
|
40
|
+
if (continuation_id !== undefined) {
|
|
41
|
+
if (typeof continuation_id !== 'string') {
|
|
42
|
+
return createToolError('continuation_id must be a string');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (continuation_id.length === 0) {
|
|
46
|
+
return createToolError(
|
|
47
|
+
'Invalid continuation_id: must be a non-empty string',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!isSafeIdSegment(continuation_id)) {
|
|
52
|
+
return createToolError(
|
|
53
|
+
'Invalid continuation_id: contains unsafe characters',
|
|
54
|
+
);
|
|
55
|
+
}
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
const asyncJobStore = getAsyncJobStore();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID validation helpers
|
|
3
|
+
*
|
|
4
|
+
* These are intentionally conservative and are primarily used to ensure IDs that
|
|
5
|
+
* are used as filesystem path segments cannot escape their intended directory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check whether an ID is safe to use as a single filesystem path segment.
|
|
10
|
+
*
|
|
11
|
+
* Allowed characters: A–Z a–z 0–9 _ -
|
|
12
|
+
* Disallowed: path separators, dots, whitespace, and other punctuation.
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} id
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @param {number} [options.maxLength]
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function isSafeIdSegment(id, options = {}) {
|
|
20
|
+
const { maxLength = 128 } = options;
|
|
21
|
+
|
|
22
|
+
if (typeof id !== 'string') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (id.length === 0 || id.length > maxLength) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return /^[A-Za-z0-9_-]+$/.test(id);
|
|
31
|
+
}
|
|
32
|
+
|