converse-mcp-server 2.27.2 → 2.28.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/src/tools/chat.js CHANGED
@@ -24,6 +24,10 @@ import { validateAllPaths } from '../utils/fileValidator.js';
24
24
  import { SummarizationService } from '../services/summarizationService.js';
25
25
  import { exportConversation } from '../utils/conversationExporter.js';
26
26
  import { isRecoverableError, retryWithBackoff } from '../utils/errorHandler.js';
27
+ import {
28
+ providerSupportsImages,
29
+ getProviderUnavailableMessage,
30
+ } from '../utils/modelRouting.js';
27
31
 
28
32
  const logger = createLogger('chat');
29
33
 
@@ -313,9 +317,16 @@ export async function chatTool(args, dependencies) {
313
317
  'openrouter',
314
318
  ];
315
319
 
320
+ const requestHasImages = Array.isArray(images) && images.length > 0;
321
+
316
322
  for (const name of providerOrder) {
317
323
  const provider = providers[name];
318
324
  if (provider && provider.isAvailable && provider.isAvailable(config)) {
325
+ // When the request has images, skip text-only providers (e.g.
326
+ // gemini-cli, copilot) so auto routing lands on an image-capable one.
327
+ if (requestHasImages && !providerSupportsImages(provider, name)) {
328
+ continue;
329
+ }
319
330
  providerCandidates.push({ name, provider });
320
331
  }
321
332
  }
@@ -335,9 +346,7 @@ export async function chatTool(args, dependencies) {
335
346
  }
336
347
 
337
348
  if (!selectedProvider.isAvailable(config)) {
338
- return createToolError(
339
- `Provider ${providerName} is not available. Check API key configuration.`,
340
- );
349
+ return createToolError(getProviderUnavailableMessage(providerName));
341
350
  }
342
351
 
343
352
  providerCandidates.push({
@@ -590,6 +599,12 @@ export function mapModelToProvider(model, providers) {
590
599
  return 'gemini-cli';
591
600
  }
592
601
 
602
+ // Check gemini: prefix (e.g., gemini:flash, gemini:pro) - routes to Antigravity
603
+ // CLI provider. Must be before the google flash/pro keyword rule so it wins.
604
+ if (modelLower.startsWith('gemini:')) {
605
+ return 'gemini-cli';
606
+ }
607
+
593
608
  // Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
594
609
  if (
595
610
  modelLower === 'claude' ||
@@ -882,9 +897,15 @@ async function executeChatWithStreaming(args, dependencies, context) {
882
897
  'openrouter',
883
898
  ];
884
899
 
900
+ const requestHasImages = Array.isArray(images) && images.length > 0;
901
+
885
902
  for (const name of providerOrder) {
886
903
  const provider = providers[name];
887
904
  if (provider && provider.isAvailable && provider.isAvailable(config)) {
905
+ // Skip text-only providers when the request includes images.
906
+ if (requestHasImages && !providerSupportsImages(provider, name)) {
907
+ continue;
908
+ }
888
909
  providerName = name;
889
910
  selectedProvider = provider;
890
911
  break;
@@ -906,9 +927,7 @@ async function executeChatWithStreaming(args, dependencies, context) {
906
927
  }
907
928
 
908
929
  if (!selectedProvider.isAvailable(config)) {
909
- throw new Error(
910
- `Provider ${providerName} is not available. Check API key configuration.`,
911
- );
930
+ throw new Error(getProviderUnavailableMessage(providerName));
912
931
  }
913
932
  }
914
933
 
@@ -1156,7 +1175,7 @@ chatTool.inputSchema = {
1156
1175
  model: {
1157
1176
  type: 'string',
1158
1177
  description:
1159
- 'AI model to use. Examples: "auto" (recommended), "codex", "gemini", "claude", "claude:fable", "claude:opus", "copilot", "copilot:codex". Defaults to auto-selection.',
1178
+ 'AI model to use. Examples: "auto" (recommended), "codex", "gemini", "gemini:flash", "claude", "claude:fable", "claude:opus", "copilot", "copilot:codex". Defaults to auto-selection.',
1160
1179
  },
1161
1180
  files: {
1162
1181
  type: 'array',
@@ -27,6 +27,10 @@ import { applyTokenLimit, getTokenLimit } from '../utils/tokenLimiter.js';
27
27
  import { validateAllPaths } from '../utils/fileValidator.js';
28
28
  import { SummarizationService } from '../services/summarizationService.js';
29
29
  import { exportConversation } from '../utils/conversationExporter.js';
30
+ import {
31
+ providerSupportsImages,
32
+ getProviderUnavailableMessage,
33
+ } from '../utils/modelRouting.js';
30
34
 
31
35
  const logger = createLogger('consensus');
32
36
 
@@ -315,10 +319,19 @@ export async function consensusTool(args, dependencies) {
315
319
  'openrouter',
316
320
  ];
317
321
 
322
+ const requestHasImages = Array.isArray(images) && images.length > 0;
323
+
318
324
  for (const providerName of providerOrder) {
319
325
  if (availableProviders.length >= 3) break;
320
326
  const provider = providers[providerName];
321
327
  if (provider && provider.isAvailable(config)) {
328
+ // Skip text-only providers when the request includes images.
329
+ if (
330
+ requestHasImages &&
331
+ !providerSupportsImages(provider, providerName)
332
+ ) {
333
+ continue;
334
+ }
322
335
  availableProviders.push(providerName);
323
336
  }
324
337
  }
@@ -369,7 +382,7 @@ export async function consensusTool(args, dependencies) {
369
382
  failedModels.push({
370
383
  model: modelName,
371
384
  provider: providerName,
372
- error: `Provider ${providerName} not available (check API key)`,
385
+ error: getProviderUnavailableMessage(providerName),
373
386
  status: 'failed',
374
387
  });
375
388
  continue;
@@ -794,6 +807,12 @@ function mapModelToProvider(model, providers) {
794
807
  return 'gemini-cli';
795
808
  }
796
809
 
810
+ // Check gemini: prefix (e.g., gemini:flash, gemini:pro) - routes to Antigravity
811
+ // CLI provider. Must be before the google flash/pro keyword rule so it wins.
812
+ if (modelLower.startsWith('gemini:')) {
813
+ return 'gemini-cli';
814
+ }
815
+
797
816
  // Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
798
817
  if (
799
818
  modelLower === 'claude' ||
@@ -1070,10 +1089,19 @@ async function executeConsensusWithStreaming(args, dependencies, context) {
1070
1089
  'openrouter',
1071
1090
  ];
1072
1091
 
1092
+ const requestHasImages = Array.isArray(images) && images.length > 0;
1093
+
1073
1094
  for (const providerName of providerOrder) {
1074
1095
  if (availableProviders.length >= 3) break;
1075
1096
  const provider = providers[providerName];
1076
1097
  if (provider && provider.isAvailable(config)) {
1098
+ // Skip text-only providers when the request includes images.
1099
+ if (
1100
+ requestHasImages &&
1101
+ !providerSupportsImages(provider, providerName)
1102
+ ) {
1103
+ continue;
1104
+ }
1077
1105
  availableProviders.push(providerName);
1078
1106
  }
1079
1107
  }
@@ -1125,7 +1153,7 @@ async function executeConsensusWithStreaming(args, dependencies, context) {
1125
1153
  failedModels.push({
1126
1154
  model: modelName,
1127
1155
  provider: providerName,
1128
- error: `Provider ${providerName} not available (check API key)`,
1156
+ error: getProviderUnavailableMessage(providerName),
1129
1157
  status: 'failed',
1130
1158
  });
1131
1159
  continue;
@@ -1712,7 +1740,7 @@ consensusTool.inputSchema = {
1712
1740
  items: { type: 'string' },
1713
1741
  minItems: 1,
1714
1742
  description:
1715
- 'List of models to consult. Examples: ["codex", "gemini", "claude", "claude:opus", "copilot", "copilot:codex"]',
1743
+ 'List of models to consult. Examples: ["codex", "gemini", "gemini:flash", "claude", "claude:opus", "copilot", "copilot:codex"]',
1716
1744
  },
1717
1745
  files: {
1718
1746
  type: 'array',
@@ -42,6 +42,8 @@ import {
42
42
  mapModelToProvider,
43
43
  resolveAutoModel,
44
44
  getDefaultModelForProvider,
45
+ providerSupportsImages,
46
+ getProviderUnavailableMessage,
45
47
  } from '../utils/modelRouting.js';
46
48
 
47
49
  const logger = createLogger('conversation');
@@ -202,7 +204,7 @@ function formatLapTranscript(lapTurns) {
202
204
  * @param {object} config - Configuration
203
205
  * @returns {Array<object>} Ordered turn plan entries
204
206
  */
205
- function resolveTurnPlan(models, providers, config) {
207
+ function resolveTurnPlan(models, providers, config, hasImages = false) {
206
208
  // Single "auto" expands to the first available provider's default model only
207
209
  // (a single-model round-table is valid). Multiple explicit models resolve per-entry.
208
210
  let modelsToProcess = models;
@@ -225,6 +227,10 @@ function resolveTurnPlan(models, providers, config) {
225
227
  for (const providerName of providerOrder) {
226
228
  const provider = providers[providerName];
227
229
  if (provider && provider.isAvailable(config)) {
230
+ // Skip text-only providers when the request includes images.
231
+ if (hasImages && !providerSupportsImages(provider, providerName)) {
232
+ continue;
233
+ }
228
234
  firstAvailable = providerName;
229
235
  break;
230
236
  }
@@ -268,7 +274,7 @@ function resolveTurnPlan(models, providers, config) {
268
274
  provider: providerName,
269
275
  providerInstance: null,
270
276
  resolvedModel,
271
- preFailReason: `Provider ${providerName} not available (check API key)`,
277
+ preFailReason: getProviderUnavailableMessage(providerName),
272
278
  };
273
279
  }
274
280
 
@@ -573,7 +579,12 @@ export async function conversationTool(args, dependencies) {
573
579
  );
574
580
 
575
581
  // Resolve ordered turn plan (unavailable models kept as pre-failed turns)
576
- const turnPlan = resolveTurnPlan(models, providers, config);
582
+ const turnPlan = resolveTurnPlan(
583
+ models,
584
+ providers,
585
+ config,
586
+ Array.isArray(images) && images.length > 0,
587
+ );
577
588
 
578
589
  const startedAt = Date.now();
579
590
  const lapTurns = [];
@@ -939,7 +950,12 @@ async function executeConversationWithStreaming(args, dependencies, context) {
939
950
  );
940
951
 
941
952
  const priorTranscriptText = renderStoredTranscriptToText(conversationHistory);
942
- const turnPlan = resolveTurnPlan(models, providers, config);
953
+ const turnPlan = resolveTurnPlan(
954
+ models,
955
+ providers,
956
+ config,
957
+ Array.isArray(images) && images.length > 0,
958
+ );
943
959
  const modelsList = models.join(', ');
944
960
 
945
961
  // Use passed title or generate if not provided
@@ -44,6 +44,49 @@ export function resolveAutoModel(model, providerName) {
44
44
  return getDefaultModelForProvider(providerName);
45
45
  }
46
46
 
47
+ /**
48
+ * Provider-specific setup hints appended to "Provider X is not available."
49
+ * errors so users know how to enable a provider. Keyed by registry name.
50
+ */
51
+ const PROVIDER_SETUP_HINTS = {
52
+ 'gemini-cli':
53
+ 'Install the Antigravity CLI and run `agy` once to log in (https://antigravity.google)',
54
+ };
55
+
56
+ /**
57
+ * Build the "provider not available" error message with an optional setup hint.
58
+ * @param {string} providerName - Provider registry name
59
+ * @returns {string}
60
+ */
61
+ export function getProviderUnavailableMessage(providerName) {
62
+ const base = `Provider ${providerName} is not available. Check API key configuration.`;
63
+ const hint = PROVIDER_SETUP_HINTS[providerName];
64
+ return hint ? `${base} ${hint}` : base;
65
+ }
66
+
67
+ /**
68
+ * Whether a provider's default model supports image inputs. Used by the "auto"
69
+ * selection paths to skip text-only providers (gemini-cli, copilot) when the
70
+ * request includes images. Providers without a resolvable config are treated as
71
+ * image-capable (fail open — they surface their own errors downstream).
72
+ * @param {object} providerInstance - Provider implementation
73
+ * @param {string} providerName - Provider registry name
74
+ * @returns {boolean}
75
+ */
76
+ export function providerSupportsImages(providerInstance, providerName) {
77
+ if (!providerInstance || typeof providerInstance.getModelConfig !== 'function') {
78
+ return true;
79
+ }
80
+ try {
81
+ const defaultModel = getDefaultModelForProvider(providerName);
82
+ const modelConfig = providerInstance.getModelConfig(defaultModel);
83
+ if (!modelConfig) return true;
84
+ return modelConfig.supportsImages !== false;
85
+ } catch {
86
+ return true;
87
+ }
88
+ }
89
+
47
90
  /**
48
91
  * Map model name to provider name
49
92
  * @param {string} model - Model name
@@ -80,6 +123,13 @@ export function mapModelToProvider(model, providers) {
80
123
  return 'gemini-cli';
81
124
  }
82
125
 
126
+ // Check gemini: prefix (e.g., gemini:flash, gemini:pro) - routes to Antigravity
127
+ // CLI provider. Must be before the google flash/pro keyword rule below so it
128
+ // wins over Google API routing. Bare gemini-pro/gemini-flash still hit google.
129
+ if (modelLower.startsWith('gemini:')) {
130
+ return 'gemini-cli';
131
+ }
132
+
83
133
  // Check Claude SDK (exact match only - routes to SDK provider instead of Anthropic API)
84
134
  if (
85
135
  modelLower === 'claude' ||