converse-mcp-server 3.7.0 → 4.0.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/.env.example +17 -3
- package/README.md +67 -49
- package/docs/API.md +55 -18
- package/docs/EXAMPLES.md +38 -0
- package/docs/PROVIDERS.md +92 -52
- package/package.json +10 -10
- package/src/config.js +29 -5
- package/src/prompts/helpPrompt.js +27 -2
- package/src/providers/anthropic.js +5 -1
- package/src/providers/claude.js +39 -88
- package/src/providers/codex.js +73 -144
- package/src/providers/copilot.js +42 -149
- package/src/providers/deepseek.js +1 -0
- package/src/providers/gemini-cli.js +64 -97
- package/src/providers/google.js +5 -1
- package/src/providers/mistral.js +5 -1
- package/src/providers/openai-compatible.js +4 -1
- package/src/providers/openai.js +5 -1
- package/src/providers/openrouter.js +1 -0
- package/src/providers/xai.js +5 -1
- package/src/services/summarizationService.js +45 -49
- package/src/tools/chat.js +56 -52
- package/src/tools/modes/roundtable.js +60 -49
- package/src/utils/localProviderAuth.js +63 -0
- package/src/utils/modelCatalog.js +38 -0
- package/src/utils/modelRouting.js +580 -343
|
@@ -9,20 +9,21 @@
|
|
|
9
9
|
import { createLogger } from '../utils/logger.js';
|
|
10
10
|
import { debugLog, debugError } from '../utils/console.js';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { resolveModelSpec } from '../utils/modelRouting.js';
|
|
13
13
|
|
|
14
14
|
const logger = createLogger('summarization');
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
// Fast models for summarization tasks, tried in order (GPT-5-nano first for
|
|
17
|
+
// speed). Namespaced so each names exactly one provider.
|
|
18
|
+
export const FAST_MODELS = [
|
|
19
|
+
'openai:gpt-5-nano',
|
|
20
|
+
'google:gemini-2.5-flash',
|
|
21
|
+
'xai:grok-4.5',
|
|
22
|
+
'anthropic:claude-haiku-4-5-20251001',
|
|
23
|
+
'mistral:mistral-small-2603',
|
|
24
|
+
'deepseek:deepseek-v4-flash',
|
|
25
|
+
'openrouter:z-ai/glm-5.2',
|
|
26
|
+
];
|
|
26
27
|
|
|
27
28
|
export class SummarizationService {
|
|
28
29
|
constructor(providers, config) {
|
|
@@ -51,16 +52,12 @@ export class SummarizationService {
|
|
|
51
52
|
|
|
52
53
|
try {
|
|
53
54
|
// Select fast model if not specified
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!provider || !provider.isAvailable(this.config)) {
|
|
59
|
-
debugLog(
|
|
60
|
-
`Summarization: Provider ${providerName} not available for title generation`,
|
|
61
|
-
);
|
|
55
|
+
const selected = this._resolve(model || this._selectFastModel());
|
|
56
|
+
if (!selected) {
|
|
57
|
+
debugLog('Summarization: No available model for title generation');
|
|
62
58
|
return this._fallbackTitle(prompt);
|
|
63
59
|
}
|
|
60
|
+
const { provider, resolvedModel: selectedModel } = selected;
|
|
64
61
|
|
|
65
62
|
// Create messages for title generation
|
|
66
63
|
const messages = [
|
|
@@ -112,16 +109,12 @@ export class SummarizationService {
|
|
|
112
109
|
|
|
113
110
|
try {
|
|
114
111
|
// Select fast model if not specified
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (!provider || !provider.isAvailable(this.config)) {
|
|
120
|
-
debugLog(
|
|
121
|
-
`Summarization: Provider ${providerName} not available for streaming summary`,
|
|
122
|
-
);
|
|
112
|
+
const selected = this._resolve(model || this._selectFastModel());
|
|
113
|
+
if (!selected) {
|
|
114
|
+
debugLog('Summarization: No available model for streaming summary');
|
|
123
115
|
return this._fallbackStreamingSummary(content, currentFocus);
|
|
124
116
|
}
|
|
117
|
+
const { provider, resolvedModel: selectedModel } = selected;
|
|
125
118
|
|
|
126
119
|
// Create messages for streaming summary
|
|
127
120
|
const messages = [
|
|
@@ -175,16 +168,12 @@ export class SummarizationService {
|
|
|
175
168
|
|
|
176
169
|
try {
|
|
177
170
|
// Select fast model if not specified
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (!provider || !provider.isAvailable(this.config)) {
|
|
183
|
-
debugLog(
|
|
184
|
-
`Summarization: Provider ${providerName} not available for final summary`,
|
|
185
|
-
);
|
|
171
|
+
const selected = this._resolve(model || this._selectFastModel());
|
|
172
|
+
if (!selected) {
|
|
173
|
+
debugLog('Summarization: No available model for final summary');
|
|
186
174
|
return this._fallbackFinalSummary(content);
|
|
187
175
|
}
|
|
176
|
+
const { provider, resolvedModel: selectedModel } = selected;
|
|
188
177
|
|
|
189
178
|
// Create messages for final summary
|
|
190
179
|
const messages = [
|
|
@@ -221,6 +210,20 @@ export class SummarizationService {
|
|
|
221
210
|
}
|
|
222
211
|
}
|
|
223
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Route a model spec to its first available provider.
|
|
215
|
+
* @private
|
|
216
|
+
* @returns {{ provider: object, providerName: string, resolvedModel: string }|null}
|
|
217
|
+
*/
|
|
218
|
+
_resolve(spec) {
|
|
219
|
+
const resolution = resolveModelSpec(spec, this.providers, this.config);
|
|
220
|
+
if (resolution.status !== 'ok') {
|
|
221
|
+
debugLog(`Summarization: ${resolution.error}`);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
return resolution;
|
|
225
|
+
}
|
|
226
|
+
|
|
224
227
|
/**
|
|
225
228
|
* Select the best available fast model
|
|
226
229
|
* @private
|
|
@@ -228,14 +231,10 @@ export class SummarizationService {
|
|
|
228
231
|
_selectFastModel() {
|
|
229
232
|
// If a model is configured, try to use it first
|
|
230
233
|
if (this.configuredModel) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
this.providers,
|
|
234
|
-
);
|
|
235
|
-
const provider = this.providers[providerName];
|
|
236
|
-
if (provider && provider.isAvailable(this.config)) {
|
|
234
|
+
const resolution = resolveModelSpec(this.configuredModel, this.providers, this.config);
|
|
235
|
+
if (resolution.status === 'ok') {
|
|
237
236
|
debugLog(
|
|
238
|
-
`Summarization: Using configured model ${this.configuredModel} from ${providerName}`,
|
|
237
|
+
`Summarization: Using configured model ${this.configuredModel} from ${resolution.providerName}`,
|
|
239
238
|
);
|
|
240
239
|
return this.configuredModel;
|
|
241
240
|
}
|
|
@@ -244,13 +243,10 @@ export class SummarizationService {
|
|
|
244
243
|
);
|
|
245
244
|
}
|
|
246
245
|
|
|
247
|
-
//
|
|
248
|
-
for (const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
debugLog(
|
|
252
|
-
`Summarization: Selected fast model ${fastModel} from ${providerName}`,
|
|
253
|
-
);
|
|
246
|
+
// Return the first fast model whose provider is available
|
|
247
|
+
for (const fastModel of FAST_MODELS) {
|
|
248
|
+
if (resolveModelSpec(fastModel, this.providers, this.config).status === 'ok') {
|
|
249
|
+
debugLog(`Summarization: Selected fast model ${fastModel}`);
|
|
254
250
|
return fastModel;
|
|
255
251
|
}
|
|
256
252
|
}
|
package/src/tools/chat.js
CHANGED
|
@@ -29,9 +29,8 @@ import { SummarizationService } from '../services/summarizationService.js';
|
|
|
29
29
|
import { exportConversation } from '../utils/conversationExporter.js';
|
|
30
30
|
import { EFFORT_LADDER } from '../utils/reasoningEffort.js';
|
|
31
31
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
getAvailableProviders,
|
|
32
|
+
getAutoCandidates,
|
|
33
|
+
getAutoModelSpecs,
|
|
35
34
|
resolveModelSpec,
|
|
36
35
|
} from '../utils/modelRouting.js';
|
|
37
36
|
import {
|
|
@@ -857,24 +856,52 @@ function buildAsyncResult(pipeline, title, finalSummary) {
|
|
|
857
856
|
// --- Model resolution helpers ------------------------------------------------
|
|
858
857
|
|
|
859
858
|
/**
|
|
860
|
-
*
|
|
861
|
-
* chat-mode failover). Skips text-only providers when the request has images.
|
|
859
|
+
* Convert router candidates into the engine's call-plan candidate shape.
|
|
862
860
|
*/
|
|
863
|
-
function
|
|
864
|
-
return
|
|
865
|
-
name,
|
|
866
|
-
providerInstance:
|
|
867
|
-
resolvedModel:
|
|
868
|
-
displayModel
|
|
861
|
+
function toPlanCandidates(candidates, displayModel) {
|
|
862
|
+
return candidates.map((c) => ({
|
|
863
|
+
name: c.providerName,
|
|
864
|
+
providerInstance: c.provider,
|
|
865
|
+
resolvedModel: c.resolvedModel,
|
|
866
|
+
displayModel,
|
|
867
|
+
resolveOptions: c.options,
|
|
869
868
|
}));
|
|
870
869
|
}
|
|
871
870
|
|
|
871
|
+
/**
|
|
872
|
+
* Resolve one explicit spec into a call plan, or a pre-failed entry carrying
|
|
873
|
+
* the router's error (unknown names include "did you mean" suggestions). A
|
|
874
|
+
* bare model name served by several providers yields a multi-candidate plan
|
|
875
|
+
* that fails over in provider priority order.
|
|
876
|
+
*/
|
|
877
|
+
function resolveExplicitPlan(spec, providers, config) {
|
|
878
|
+
const resolution = resolveModelSpec(spec, providers, config);
|
|
879
|
+
if (resolution.status !== 'ok') {
|
|
880
|
+
return {
|
|
881
|
+
preFailed: {
|
|
882
|
+
model: spec,
|
|
883
|
+
...(resolution.providerName && { provider: resolution.providerName }),
|
|
884
|
+
error: resolution.error,
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
plan: {
|
|
890
|
+
modelSpec: spec,
|
|
891
|
+
displayModel: spec,
|
|
892
|
+
threadKey: spec,
|
|
893
|
+
candidates: toPlanCandidates(resolution.candidates, spec),
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
872
898
|
/**
|
|
873
899
|
* Resolve chat-mode call plans. Each "auto" spec (whether the list is exactly
|
|
874
900
|
* ["auto"] or "auto" appears alongside explicit models) yields a plan with the
|
|
875
901
|
* full provider-priority candidate list (failover); explicit models yield one
|
|
876
|
-
*
|
|
877
|
-
*
|
|
902
|
+
* plan each, with failover candidates when a bare name is served by several
|
|
903
|
+
* providers. Unavailable/unknown explicit models are returned as pre-failed
|
|
904
|
+
* entries (surfaced as per-model failures).
|
|
878
905
|
*/
|
|
879
906
|
function resolveChatCallPlans(models, providers, config, hasImages) {
|
|
880
907
|
const callPlans = [];
|
|
@@ -882,7 +909,10 @@ function resolveChatCallPlans(models, providers, config, hasImages) {
|
|
|
882
909
|
|
|
883
910
|
for (const spec of models) {
|
|
884
911
|
if (String(spec).toLowerCase() === 'auto') {
|
|
885
|
-
const candidates =
|
|
912
|
+
const candidates = toPlanCandidates(
|
|
913
|
+
getAutoCandidates(providers, config, { hasImages }),
|
|
914
|
+
'auto',
|
|
915
|
+
);
|
|
886
916
|
if (candidates.length === 0) {
|
|
887
917
|
// A single ["auto"] with no providers is a hard error; an "auto" entry
|
|
888
918
|
// in a multi-model list becomes a per-model failure instead.
|
|
@@ -909,28 +939,11 @@ function resolveChatCallPlans(models, providers, config, hasImages) {
|
|
|
909
939
|
continue;
|
|
910
940
|
}
|
|
911
941
|
|
|
912
|
-
const {
|
|
913
|
-
if (
|
|
914
|
-
preFailed.push(
|
|
915
|
-
model: spec,
|
|
916
|
-
provider: providerName,
|
|
917
|
-
error: `Provider not found for model: ${spec}`,
|
|
918
|
-
});
|
|
919
|
-
} else if (status === 'unavailable') {
|
|
920
|
-
preFailed.push({
|
|
921
|
-
model: spec,
|
|
922
|
-
provider: providerName,
|
|
923
|
-
error: getProviderUnavailableMessage(providerName),
|
|
924
|
-
});
|
|
942
|
+
const { plan, preFailed: failed } = resolveExplicitPlan(spec, providers, config);
|
|
943
|
+
if (failed) {
|
|
944
|
+
preFailed.push(failed);
|
|
925
945
|
} else {
|
|
926
|
-
callPlans.push(
|
|
927
|
-
modelSpec: spec,
|
|
928
|
-
displayModel: spec,
|
|
929
|
-
threadKey: spec,
|
|
930
|
-
candidates: [
|
|
931
|
-
{ name: providerName, providerInstance: provider, resolvedModel, displayModel: spec, resolveOptions: options },
|
|
932
|
-
],
|
|
933
|
-
});
|
|
946
|
+
callPlans.push(plan);
|
|
934
947
|
}
|
|
935
948
|
}
|
|
936
949
|
return { callPlans, preFailed, error: null };
|
|
@@ -938,15 +951,15 @@ function resolveChatCallPlans(models, providers, config, hasImages) {
|
|
|
938
951
|
|
|
939
952
|
/**
|
|
940
953
|
* Resolve consensus-mode call plans. Single "auto" expands to the first 3
|
|
941
|
-
* available providers' default models; each spec
|
|
954
|
+
* available providers' default models (as `namespace:model` specs); each spec
|
|
955
|
+
* becomes one plan.
|
|
942
956
|
*/
|
|
943
957
|
function resolveConsensusCallPlans(models, providers, config, images) {
|
|
944
958
|
const hasImages = Array.isArray(images) && images.length > 0;
|
|
945
959
|
|
|
946
960
|
let modelsToProcess = models;
|
|
947
961
|
if (models.length === 1 && String(models[0]).toLowerCase() === 'auto') {
|
|
948
|
-
|
|
949
|
-
modelsToProcess = available.map((name) => getDefaultModelForProvider(name));
|
|
962
|
+
modelsToProcess = getAutoModelSpecs(providers, config, { hasImages, limit: 3 });
|
|
950
963
|
}
|
|
951
964
|
|
|
952
965
|
const resolved = [];
|
|
@@ -956,20 +969,11 @@ function resolveConsensusCallPlans(models, providers, config, images) {
|
|
|
956
969
|
preFailed.push({ model: spec || 'unknown', error: 'Invalid model specification' });
|
|
957
970
|
continue;
|
|
958
971
|
}
|
|
959
|
-
const {
|
|
960
|
-
if (
|
|
961
|
-
preFailed.push(
|
|
962
|
-
} else if (status === 'unavailable') {
|
|
963
|
-
preFailed.push({ model: spec, provider: providerName, error: getProviderUnavailableMessage(providerName) });
|
|
972
|
+
const { plan, preFailed: failed } = resolveExplicitPlan(spec, providers, config);
|
|
973
|
+
if (failed) {
|
|
974
|
+
preFailed.push(failed);
|
|
964
975
|
} else {
|
|
965
|
-
resolved.push(
|
|
966
|
-
modelSpec: spec,
|
|
967
|
-
displayModel: spec,
|
|
968
|
-
threadKey: spec,
|
|
969
|
-
candidates: [
|
|
970
|
-
{ name: providerName, providerInstance: provider, resolvedModel, displayModel: spec, resolveOptions: options },
|
|
971
|
-
],
|
|
972
|
-
});
|
|
976
|
+
resolved.push(plan);
|
|
973
977
|
}
|
|
974
978
|
}
|
|
975
979
|
return { resolved, preFailed };
|
|
@@ -1161,7 +1165,7 @@ chatTool.inputSchema = {
|
|
|
1161
1165
|
items: { type: 'string' },
|
|
1162
1166
|
minItems: 1,
|
|
1163
1167
|
description:
|
|
1164
|
-
'Models to use. Examples: ["auto"] (recommended), ["codex"], ["codex", "gemini", "claude"]. In mode "chat" each model answers independently; in "consensus" they refine after seeing each other; in "roundtable" they speak in the given ORDER, each seeing the transcript. Default: ["auto"].',
|
|
1168
|
+
'Models to use. Examples: ["auto"] (recommended), ["codex"], ["codex", "gemini", "claude"], ["codex:astra"], ["gpt-6-astra"]. Forms: "provider" (its default model), "provider:model" (that provider only), or a bare "model" (served by the first configured provider that offers it, local CLI providers first, failing over to the next). Providers: codex, gemini (agy), claude, copilot, openai, google, xai, anthropic, mistral, deepseek, openrouter. Unknown names are rejected with suggestions. In mode "chat" each model answers independently; in "consensus" they refine after seeing each other; in "roundtable" they speak in the given ORDER, each seeing the transcript. Default: ["auto"].',
|
|
1165
1169
|
},
|
|
1166
1170
|
mode: {
|
|
1167
1171
|
type: 'string',
|
|
@@ -20,12 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import { debugLog } from '../../utils/console.js';
|
|
22
22
|
import { acquireProviderStream } from './streamShared.js';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
getProviderUnavailableMessage,
|
|
26
|
-
getAvailableProviders,
|
|
27
|
-
resolveModelSpec,
|
|
28
|
-
} from '../../utils/modelRouting.js';
|
|
23
|
+
import { getAutoModelSpecs, resolveModelSpec } from '../../utils/modelRouting.js';
|
|
24
|
+
import { shouldFailoverToNextProvider } from './parallel.js';
|
|
29
25
|
|
|
30
26
|
/**
|
|
31
27
|
* Render a stored transcript (from prior laps or a prior chat/consensus thread)
|
|
@@ -169,7 +165,9 @@ export function formatLapTranscript(lapTurns) {
|
|
|
169
165
|
* Resolve the ordered model list into a turn plan. Unlike the parallel engine,
|
|
170
166
|
* unknown or unavailable models are NOT dropped — they are recorded with a
|
|
171
167
|
* preFailReason so they keep their position in the order (and produce a failed
|
|
172
|
-
* turn).
|
|
168
|
+
* turn). A bare model name served by several providers carries every serving
|
|
169
|
+
* provider in `candidates`, tried in order when a turn fails with a
|
|
170
|
+
* failover-worthy error.
|
|
173
171
|
* @param {Array<string>} models - Ordered model list
|
|
174
172
|
* @param {object} providers - Provider instances
|
|
175
173
|
* @param {object} config - Configuration
|
|
@@ -181,16 +179,14 @@ export function resolveTurnPlan(models, providers, config, hasImages = false) {
|
|
|
181
179
|
// (a single-model round-table is valid). Multiple explicit models resolve per-entry.
|
|
182
180
|
let modelsToProcess = models;
|
|
183
181
|
if (models.length === 1 && String(models[0]).toLowerCase() === 'auto') {
|
|
184
|
-
const [firstAvailable] =
|
|
182
|
+
const [firstAvailable] = getAutoModelSpecs(providers, config, {
|
|
185
183
|
hasImages,
|
|
186
184
|
limit: 1,
|
|
187
185
|
});
|
|
188
186
|
|
|
189
187
|
// If a provider is available, use its default model. Otherwise keep "auto"
|
|
190
188
|
// so it resolves to a turn that fails cleanly (all-fail laps must complete).
|
|
191
|
-
modelsToProcess = firstAvailable
|
|
192
|
-
? [getDefaultModelForProvider(firstAvailable)]
|
|
193
|
-
: ['auto'];
|
|
189
|
+
modelsToProcess = firstAvailable ? [firstAvailable] : ['auto'];
|
|
194
190
|
}
|
|
195
191
|
|
|
196
192
|
return modelsToProcess.map((modelName) => {
|
|
@@ -200,39 +196,31 @@ export function resolveTurnPlan(models, providers, config, hasImages = false) {
|
|
|
200
196
|
provider: null,
|
|
201
197
|
providerInstance: null,
|
|
202
198
|
resolvedModel: null,
|
|
199
|
+
candidates: [],
|
|
203
200
|
preFailReason: 'Invalid model specification',
|
|
204
201
|
};
|
|
205
202
|
}
|
|
206
203
|
|
|
207
|
-
const
|
|
208
|
-
resolveModelSpec(modelName, providers, config);
|
|
204
|
+
const resolution = resolveModelSpec(modelName, providers, config);
|
|
209
205
|
|
|
210
|
-
if (status
|
|
206
|
+
if (resolution.status !== 'ok') {
|
|
211
207
|
return {
|
|
212
208
|
model: modelName,
|
|
213
|
-
provider: providerName,
|
|
209
|
+
provider: resolution.providerName,
|
|
214
210
|
providerInstance: null,
|
|
215
|
-
resolvedModel,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (status === 'unavailable') {
|
|
221
|
-
return {
|
|
222
|
-
model: modelName,
|
|
223
|
-
provider: providerName,
|
|
224
|
-
providerInstance: null,
|
|
225
|
-
resolvedModel,
|
|
226
|
-
preFailReason: getProviderUnavailableMessage(providerName),
|
|
211
|
+
resolvedModel: null,
|
|
212
|
+
candidates: [],
|
|
213
|
+
preFailReason: resolution.error,
|
|
227
214
|
};
|
|
228
215
|
}
|
|
229
216
|
|
|
230
217
|
return {
|
|
231
218
|
model: modelName,
|
|
232
|
-
provider: providerName,
|
|
233
|
-
providerInstance: provider,
|
|
234
|
-
resolvedModel,
|
|
235
|
-
resolveOptions: options,
|
|
219
|
+
provider: resolution.providerName,
|
|
220
|
+
providerInstance: resolution.provider,
|
|
221
|
+
resolvedModel: resolution.resolvedModel,
|
|
222
|
+
resolveOptions: resolution.options,
|
|
223
|
+
candidates: resolution.candidates,
|
|
236
224
|
preFailReason: null,
|
|
237
225
|
};
|
|
238
226
|
});
|
|
@@ -254,7 +242,9 @@ function buildTurnUserContent(packetText, contextMessage) {
|
|
|
254
242
|
* Execute a single turn. Streams (updating job progress) when a job context is
|
|
255
243
|
* present; otherwise performs a plain invoke. Cancellation propagates by throwing
|
|
256
244
|
* so the lap aborts rather than demoting to a failed turn.
|
|
257
|
-
* @returns {Promise<object>} Turn result { model, provider, status, response|error }
|
|
245
|
+
* @returns {Promise<object>} Turn result { model, provider, status, response|error };
|
|
246
|
+
* failed turns also carry the thrown `cause` for the failover decision
|
|
247
|
+
* (stripped before the turn is recorded)
|
|
258
248
|
*/
|
|
259
249
|
async function executeTurn(
|
|
260
250
|
plan,
|
|
@@ -353,6 +343,7 @@ async function executeTurn(
|
|
|
353
343
|
provider: plan.provider,
|
|
354
344
|
status: 'failed',
|
|
355
345
|
error: error.message,
|
|
346
|
+
cause: error,
|
|
356
347
|
};
|
|
357
348
|
}
|
|
358
349
|
}
|
|
@@ -436,24 +427,44 @@ export async function runRoundtableLap({
|
|
|
436
427
|
{ role: 'user', content: finalUserContent },
|
|
437
428
|
];
|
|
438
429
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
430
|
+
let turnResult;
|
|
431
|
+
for (let ci = 0; ci < plan.candidates.length; ci++) {
|
|
432
|
+
const candidate = plan.candidates[ci];
|
|
433
|
+
turnResult = await executeTurn(
|
|
434
|
+
{
|
|
435
|
+
model: plan.model,
|
|
436
|
+
provider: candidate.providerName,
|
|
437
|
+
providerInstance: candidate.provider,
|
|
438
|
+
},
|
|
439
|
+
messages,
|
|
440
|
+
{
|
|
441
|
+
reasoning_effort,
|
|
442
|
+
signal: activeSignal,
|
|
443
|
+
config,
|
|
444
|
+
model: candidate.resolvedModel,
|
|
445
|
+
// Web search opt-in from an OpenRouter `:online` decoration; only ever
|
|
446
|
+
// set for OpenRouter turns.
|
|
447
|
+
...(candidate.options?.web_search && { web_search: true }),
|
|
448
|
+
},
|
|
449
|
+
context,
|
|
450
|
+
providerStreamNormalizer,
|
|
451
|
+
i,
|
|
452
|
+
);
|
|
453
|
+
const isLastCandidate = ci === plan.candidates.length - 1;
|
|
454
|
+
if (
|
|
455
|
+
turnResult.status === 'success' ||
|
|
456
|
+
isLastCandidate ||
|
|
457
|
+
!shouldFailoverToNextProvider(turnResult.cause)
|
|
458
|
+
) {
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
debugLog(
|
|
462
|
+
`[Roundtable] Turn ${i + 1} (${plan.model}) failed on ${candidate.providerName}; failing over`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
455
465
|
|
|
456
|
-
|
|
466
|
+
const { cause: _cause, ...turn } = turnResult;
|
|
467
|
+
lapTurns.push({ ...turn, position: i });
|
|
457
468
|
}
|
|
458
469
|
|
|
459
470
|
if (context) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Provider Availability Probes
|
|
3
|
+
*
|
|
4
|
+
* Cheap, synchronous checks for the CLI/SDK providers: the SDK package
|
|
5
|
+
* resolves and a credential is present. They never spawn a process or call a
|
|
6
|
+
* network endpoint, so they cannot prove a login is still valid — a stale
|
|
7
|
+
* credential surfaces as an auth error at invoke time, where bare-name and
|
|
8
|
+
* "auto" routing fail over to the next provider.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const resolvedPackages = new Map();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether an npm package resolves from this module. Cached per process:
|
|
19
|
+
* installing a package requires a restart to load it anyway.
|
|
20
|
+
* @param {string} packageName
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function isPackageResolvable(packageName) {
|
|
24
|
+
if (!resolvedPackages.has(packageName)) {
|
|
25
|
+
let resolvable;
|
|
26
|
+
try {
|
|
27
|
+
import.meta.resolve(packageName);
|
|
28
|
+
resolvable = true;
|
|
29
|
+
} catch {
|
|
30
|
+
resolvable = false;
|
|
31
|
+
}
|
|
32
|
+
resolvedPackages.set(packageName, resolvable);
|
|
33
|
+
}
|
|
34
|
+
return resolvedPackages.get(packageName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Codex credentials: CODEX_API_KEY, or the ChatGPT login file the Codex CLI
|
|
39
|
+
* writes to $CODEX_HOME/auth.json (default ~/.codex).
|
|
40
|
+
* @param {object} config
|
|
41
|
+
* @returns {boolean}
|
|
42
|
+
*/
|
|
43
|
+
export function hasCodexCredentials(config) {
|
|
44
|
+
if (config?.providers?.codexapikey) return true;
|
|
45
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
46
|
+
return existsSync(join(codexHome, 'auth.json'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Claude Code credentials: an OAuth token or API key in the environment the
|
|
51
|
+
* spawned CLI inherits, or the login file in $CLAUDE_CONFIG_DIR (default
|
|
52
|
+
* ~/.claude). macOS keeps the login in the Keychain, which cannot be read
|
|
53
|
+
* without spawning a process, so a login there is assumed.
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export function hasClaudeCredentials() {
|
|
57
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (process.platform === 'darwin') return true;
|
|
61
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
62
|
+
return existsSync(join(configDir, '.credentials.json'));
|
|
63
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Catalog Helpers
|
|
3
|
+
*
|
|
4
|
+
* Every provider keeps a catalog keyed by canonical model ID, each entry
|
|
5
|
+
* carrying an `aliases` array. These helpers are the single lookup rule for
|
|
6
|
+
* those catalogs: case-insensitive, canonical ID first, then aliases.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Canonical catalog ID for a name, or null.
|
|
11
|
+
* @param {Object<string, {aliases?: string[]}>} catalog
|
|
12
|
+
* @param {string} name - Canonical ID or alias
|
|
13
|
+
* @returns {string|null}
|
|
14
|
+
*/
|
|
15
|
+
export function findCatalogId(catalog, name) {
|
|
16
|
+
const lower = String(name ?? '').trim().toLowerCase();
|
|
17
|
+
if (!lower || !catalog) return null;
|
|
18
|
+
for (const id of Object.keys(catalog)) {
|
|
19
|
+
if (id.toLowerCase() === lower) return id;
|
|
20
|
+
}
|
|
21
|
+
for (const [id, entry] of Object.entries(catalog)) {
|
|
22
|
+
if (entry?.aliases?.some((alias) => String(alias).toLowerCase() === lower)) {
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Catalog entry for a name, or null.
|
|
31
|
+
* @param {Object<string, object>} catalog
|
|
32
|
+
* @param {string} name - Canonical ID or alias
|
|
33
|
+
* @returns {object|null}
|
|
34
|
+
*/
|
|
35
|
+
export function findCatalogEntry(catalog, name) {
|
|
36
|
+
const id = findCatalogId(catalog, name);
|
|
37
|
+
return id ? catalog[id] : null;
|
|
38
|
+
}
|