@index9/mcp 6.1.0 → 6.2.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/dist/cli.js CHANGED
@@ -48,6 +48,11 @@ var MissingModelDiagnosticSchema = z.object({
48
48
  provider: z.string().optional(),
49
49
  message: z.string()
50
50
  });
51
+ var SuggestionEntrySchema = z.object({
52
+ id: z.string(),
53
+ name: z.string(),
54
+ created: z.number().nullable()
55
+ });
51
56
  var UserContentTextPartSchema = z.strictObject({
52
57
  type: z.literal("text"),
53
58
  text: z.string().trim().min(1)
@@ -108,8 +113,9 @@ Key rules:
108
113
  - find_models price-asc tends to be dominated by free preview models \u2014 pass \`excludeFree=true\` when you want a paid SLA.
109
114
  - find_models always emits \`meta.confidence\` ("high" | "low") on semantic queries. Low means no candidate matched on keyword (BM25); \`meta.lowConfidenceReason\` is "no_keyword_matches" or "no_results" and \`meta.suggestion\` carries an actionable hint. Weak hits are capped at score=30 so they don't masquerade as strong matches. Pass \`requireKeywordMatch: true\` to get an empty page instead of weak vector-only neighbors.
110
115
  - find_models with sortBy=price exposes \`pricing.effectivePromptPerMillion\` and \`pageInfo.priceSortBasis\` \u2014 sort order may diverge from displayed promptPerMillion for models with per-request fees.
111
- - get_models accepts aliases (display names, short names) \u2014 not just full IDs. Unknown ids return in missingIds with \`suggestions\` (token-fuzzy or recency-anchored newest-from-provider) and \`missingDiagnostics\` keyed by id with \`reason\` ("unknown_provider" | "no_match" | "suggestions_available" | "ambiguous_alias") so retry strategy is explicit. Retry with one of the suggested ids.
116
+ - Your training-data model IDs are routinely stale. get_models / compare_models / test_model all accept aliases (display names, short names) and return unknown ids in \`missingIds\` with \`suggestions[id]\` ordered newest-first (each entry: \`{id, name, created}\`, where \`created\` is unix seconds), plus \`missingDiagnostics[id].reason\` \u2208 {"unknown_provider", "no_match", "suggestions_available", "ambiguous_alias"}. **Default recovery: retry with \`suggestions[id][0].id\` \u2014 it's the newest viable replacement.** If suggestions is empty or reason="no_match"/"unknown_provider", fall back to \`find_models sortBy=created\` instead.
112
117
  - compare_models accepts the same alias formats as get_models. Use it instead of N parallel get_models calls when the user is comparing finalists.
118
+ - test_model pre-flight resolves and filters unresolvable ids out of the OpenRouter call, so stale ids never cost you credits \u2014 they come back in missingIds with the same suggestions/diagnostics surface as get_models. If every id is unresolvable, the call returns 400 with diagnostics and no inference fires.
113
119
  - Use test_model with \`dryRun=true\` to estimate cost before live testing. Pass \`expectedPromptTokens\` for capacity planning at sizes you don't want to paste in full.
114
120
  - test_model with \`dryRun=false\` (default) requires OPENROUTER_API_KEY and incurs real usage costs.
115
121
  - Reasoning-capable models (capabilities includes "reasoning") burn hidden reasoning tokens against \`maxTokens\` before emitting visible text. Leave \`maxTokens\` unset, or set it to at least 2000, when testing reasoning models \u2014 otherwise results may fail with finish_reason=length.
@@ -152,7 +158,7 @@ Pass result.id to get_models for full specs or to test_model for live testing.`,
152
158
 
153
159
  Call after find_models to inspect candidates, or directly when the user names a model (format: 'provider/model-name').
154
160
 
155
- Response: { results: (Model | null)[], missingIds: string[], resolvedAliases?: Record<alias, canonicalId>, ambiguousAliases?: Record<alias, candidateIds[]>, suggestions?: Record<unknownId, candidateIds[]> }. Each non-null result has:
161
+ Response: { results: (Model | null)[], missingIds: string[], resolvedAliases?: Record<alias, canonicalId>, ambiguousAliases?: Record<alias, candidateIds[]>, suggestions?: Record<unknownId, Array<{id, name, created}>> }. Each non-null result has:
156
162
  - id, canonicalSlug, name, description
157
163
  - created (unix seconds), createdAt (ISO 8601), knowledgeCutoff (ISO date or null)
158
164
  - contextLength (tokens), maxOutputTokens, isModerated
@@ -161,7 +167,7 @@ Response: { results: (Model | null)[], missingIds: string[], resolvedAliases?: R
161
167
  - capabilities[]: normalized capability flags (same values as find_models and capabilitiesAll/Any)
162
168
  - supportedParameters[]: OpenRouter parameters the model accepts (e.g., "temperature", "tools", "response_format")
163
169
 
164
- Entries in results are null when the id is unknown; those ids appear in missingIds. Ambiguous aliases appear in ambiguousAliases with candidate canonical ids \u2014 pass a canonical id to disambiguate. Unknown ids that partially match (e.g. "sonnet" \u2192 all Claude Sonnet variants) appear in suggestions with up to 5 candidate ids. When token-overlap finds nothing but the id is shaped like \`provider/<unknown>\` and the provider exists, suggestions falls back to the 5 newest models from that provider (real created timestamps, no hardcoded "popular" list). Retry with one of the suggested ids.
170
+ Entries in results are null when the id is unknown; those ids appear in missingIds. Ambiguous aliases appear in ambiguousAliases with candidate canonical ids \u2014 pass a canonical id to disambiguate. Unknown ids that partially match (e.g. "sonnet" \u2192 all Claude Sonnet variants) appear in \`suggestions\` as up to 5 \`{id, name, created}\` entries **sorted newest-first** \u2014 pick \`suggestions[id][0].id\` for the most current replacement without a second lookup. When token-overlap finds nothing but the id is shaped like \`provider/<unknown>\` and the provider exists, suggestions falls back to the 5 newest models from that provider (real created timestamps, no hardcoded "popular" list).
165
171
 
166
172
  \`missingDiagnostics\` (when present) gives a machine-readable reason per missing id: \`unknown_provider\` (the prefix before / isn't in the catalog \u2014 fix the provider, not the model name), \`ambiguous_alias\`, \`suggestions_available\` (mirrors suggestions[id]), or \`no_match\`.`,
167
173
  requiresKey: false
@@ -174,13 +180,13 @@ Entries in results are null when the id is unknown; those ids appear in missingI
174
180
 
175
181
  Use this when the user asks "which is cheaper / has more context / supports X" across multiple specific models. Faster than calling get_models and diffing yourself.
176
182
 
177
- Response: { models: ModelResponse[], diff: { contextLength, maxOutputTokens, promptPricePerMillion, completionPricePerMillion, tokenizer, inputModalities, outputModalities, capabilities, supportedParameters }, cheapestForPromptPerMillion, largestContext, missingIds, resolvedAliases?, ambiguousAliases?, suggestions? }.
183
+ Response: { models: ModelResponse[], diff: { contextLength, maxOutputTokens, promptPricePerMillion, completionPricePerMillion, tokenizer, inputModalities, outputModalities, capabilities, supportedParameters }, cheapestForPromptPerMillion, largestContext, missingIds, resolvedAliases?, ambiguousAliases?, suggestions?: Record<unknownId, Array<{id, name, created}>> (newest-first), missingDiagnostics? }.
178
184
 
179
185
  Each numeric/string diff field has { allEqual: boolean, values: Record<id, value|null> }. Capability/parameter diffs have { commonAll: string[], uniquePerModel: Record<id, string[]> }. cheapestForPromptPerMillion / largestContext are convenience picks across the supplied models \u2014 null when the field is missing on every model.
180
186
 
181
187
  Optional: pass \`expectedPromptTokens\` AND \`expectedCompletionTokens\` to also receive \`workloadCosts\` and \`cheapestForRealisticWorkload\` \u2014 the actual cheapest given the user's expected token mix. Each \`workloadCosts[i]\` carries \`tokenCostUsd\` (token-only), \`requestCostUsd\` (per-request fee), \`totalCostUsd\` (sum, includes request fees), and \`pricingBasis\` ("exact_per_token" | "rounded_per_million" | "unavailable"). This matters when prompt:completion price ratios diverge across models, or when a model has a per-request fee.
182
188
 
183
- Accepts the same alias formats as get_models. Unknown ids are returned in missingIds (with suggestions when partial matches exist, plus \`missingDiagnostics\` carrying a machine-readable reason per id).`,
189
+ Accepts the same alias formats as get_models. Unknown ids are returned in missingIds (with \`suggestions[id]\` as newest-first \`{id, name, created}\` entries when partial matches exist, plus \`missingDiagnostics\` carrying a machine-readable reason per id). When fewer than 2 ids resolve, this returns 400 with the diagnostics so you can retry with \`suggestions[id][0].id\` for each missing id.`,
184
190
  requiresKey: false
185
191
  },
186
192
  list_facets: {
@@ -216,7 +222,9 @@ Parameters:
216
222
 
217
223
  Results (live): each result carries modelId (the id you passed), resolvedModelId (canonical id, present when the input was an alias), ok, response, latencyMs, tokens { prompt, completion }, cost (USD; live from OpenRouter when available, else estimated from cached pricing), and truncated=true when finish_reason is "length". On failure, results include \`error\` (free-form) plus \`failureReason\` ("insufficient_credits" | "model_unavailable" | "rate_limited" | "timeout" | "invalid_request" | "unknown") so callers can pick a retry strategy without parsing the error string.
218
224
 
219
- Results (dryRun): each entry carries \`tokenCostUsd\`, \`requestCostUsd\`, \`totalCostUsd\` (matches \`estimatedCost\`, includes per-request fees), and \`estimatedCostBasis\` (same enum as compare_models.workloadCosts). Use find_models or get_models first to identify model ids.`,
225
+ Results (dryRun): each entry carries \`tokenCostUsd\`, \`requestCostUsd\`, \`totalCostUsd\` (matches \`estimatedCost\`, includes per-request fees), and \`estimatedCostBasis\` (same enum as compare_models.workloadCosts). Use find_models or get_models first to identify model ids.
226
+
227
+ Stale-id recovery: unresolvable model ids are filtered out **before** any OpenRouter call (so they cost nothing) and returned in \`missingIds\` alongside \`suggestions\` (newest-first \`{id, name, created}\` entries), \`resolvedAliases\`, \`ambiguousAliases\`, and \`missingDiagnostics\` \u2014 same shape as get_models / compare_models. If every id is unresolvable, the call returns 400 with diagnostics and no inference fires. Default recovery: retry with \`suggestions[id][0].id\`.`,
220
228
  requiresKey: true
221
229
  }
222
230
  };
@@ -654,7 +662,7 @@ var BatchModelLookupResponseSchema = z3.object({
654
662
  missingIds: z3.array(z3.string()),
655
663
  resolvedAliases: z3.record(z3.string(), z3.string()).optional(),
656
664
  ambiguousAliases: z3.record(z3.string(), z3.array(z3.string())).optional(),
657
- suggestions: z3.record(z3.string(), z3.array(z3.string())).optional(),
665
+ suggestions: z3.record(z3.string(), z3.array(SuggestionEntrySchema)).optional(),
658
666
  missingDiagnostics: z3.record(z3.string(), MissingModelDiagnosticSchema).optional()
659
667
  }).strict();
660
668
  var GetModelsToolResultSchema = z3.object({
@@ -662,7 +670,7 @@ var GetModelsToolResultSchema = z3.object({
662
670
  missingIds: z3.array(z3.string()),
663
671
  resolvedAliases: z3.record(z3.string(), z3.string()).optional(),
664
672
  ambiguousAliases: z3.record(z3.string(), z3.array(z3.string())).optional(),
665
- suggestions: z3.record(z3.string(), z3.array(z3.string())).optional(),
673
+ suggestions: z3.record(z3.string(), z3.array(SuggestionEntrySchema)).optional(),
666
674
  missingDiagnostics: z3.record(z3.string(), MissingModelDiagnosticSchema).optional(),
667
675
  _index9: Index9MetaSchema
668
676
  });
@@ -720,7 +728,7 @@ var CompareResponseSchema = z4.object({
720
728
  workloadCosts: z4.array(CompareWorkloadCostSchema).optional(),
721
729
  resolvedAliases: z4.record(z4.string(), z4.string()).optional(),
722
730
  missingIds: z4.array(z4.string()),
723
- suggestions: z4.record(z4.string(), z4.array(z4.string())).optional(),
731
+ suggestions: z4.record(z4.string(), z4.array(SuggestionEntrySchema)).optional(),
724
732
  ambiguousAliases: z4.record(z4.string(), z4.array(z4.string())).optional(),
725
733
  missingDiagnostics: z4.record(z4.string(), MissingModelDiagnosticSchema).optional()
726
734
  }).strict();
@@ -850,13 +858,22 @@ var TestEstimateResultSchema = z6.object({
850
858
  totalCostUsd: z6.number().nullable().optional(),
851
859
  estimatedCostBasis: PricingBasisSchema.optional()
852
860
  });
861
+ var TestResolutionFieldsSchema = {
862
+ missingIds: z6.array(z6.string()).optional(),
863
+ resolvedAliases: z6.record(z6.string(), z6.string()).optional(),
864
+ ambiguousAliases: z6.record(z6.string(), z6.array(z6.string())).optional(),
865
+ suggestions: z6.record(z6.string(), z6.array(SuggestionEntrySchema)).optional(),
866
+ missingDiagnostics: z6.record(z6.string(), MissingModelDiagnosticSchema).optional()
867
+ };
853
868
  var TestDryRunResponseSchema = z6.object({
854
869
  dryRun: z6.literal(true),
855
870
  results: z6.array(TestEstimateResultSchema),
856
- disclaimer: z6.string()
871
+ disclaimer: z6.string(),
872
+ ...TestResolutionFieldsSchema
857
873
  });
858
874
  var TestLiveResponseSchema = z6.object({
859
- results: z6.array(TestResultSchema)
875
+ results: z6.array(TestResultSchema),
876
+ ...TestResolutionFieldsSchema
860
877
  });
861
878
  var TestResponseSchema = z6.union([TestDryRunResponseSchema, TestLiveResponseSchema]);
862
879
 
@@ -1004,6 +1021,22 @@ function extractError(body) {
1004
1021
  }
1005
1022
  return "Request failed";
1006
1023
  }
1024
+ var RECOVERY_FIELDS = [
1025
+ "missingIds",
1026
+ "resolvedAliases",
1027
+ "ambiguousAliases",
1028
+ "suggestions",
1029
+ "missingDiagnostics"
1030
+ ];
1031
+ function extractRecoveryFields(body) {
1032
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return {};
1033
+ const out = {};
1034
+ const b = body;
1035
+ for (const key of RECOVERY_FIELDS) {
1036
+ if (key in b) out[key] = b[key];
1037
+ }
1038
+ return out;
1039
+ }
1007
1040
  async function callApi(ctx, url, options, responseSchema) {
1008
1041
  const res = await fetchWithRetry(url, options);
1009
1042
  let body;
@@ -1014,7 +1047,12 @@ async function callApi(ctx, url, options, responseSchema) {
1014
1047
  }
1015
1048
  if (!res.ok) {
1016
1049
  return toResponse(
1017
- { error: extractError(body), status: res.status, _index9: buildMeta(ctx, res.headers) },
1050
+ {
1051
+ error: extractError(body),
1052
+ status: res.status,
1053
+ ...extractRecoveryFields(body),
1054
+ _index9: buildMeta(ctx, res.headers)
1055
+ },
1018
1056
  true
1019
1057
  );
1020
1058
  }
@@ -1062,7 +1100,11 @@ async function handleGetModels(ctx, args) {
1062
1100
  return callApi(
1063
1101
  ctx,
1064
1102
  `${ctx.baseUrl}${API_PATHS.model}`,
1065
- { method: "POST", headers: baseHeaders(ctx), body: JSON.stringify(parsed.data) },
1103
+ {
1104
+ method: "POST",
1105
+ headers: baseHeaders(ctx),
1106
+ body: JSON.stringify(parsed.data)
1107
+ },
1066
1108
  BatchModelLookupResponseSchema
1067
1109
  );
1068
1110
  }
@@ -1074,7 +1116,11 @@ async function handleCompareModels(ctx, args) {
1074
1116
  return callApi(
1075
1117
  ctx,
1076
1118
  `${ctx.baseUrl}${API_PATHS.compare}`,
1077
- { method: "POST", headers: baseHeaders(ctx), body: JSON.stringify(parsed.data) },
1119
+ {
1120
+ method: "POST",
1121
+ headers: baseHeaders(ctx),
1122
+ body: JSON.stringify(parsed.data)
1123
+ },
1078
1124
  CompareResponseSchema
1079
1125
  );
1080
1126
  }
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.3",
3
3
  "name": "index9",
4
- "version": "6.0.0",
4
+ "version": "6.1.0",
5
5
  "description": "Discover, shortlist, compare, cost-model, and live-test 300+ AI models from your editor",
6
6
  "author": {
7
7
  "name": "Index9"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@index9/mcp",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,11 +24,11 @@
24
24
  "zod": "^4.4.3"
25
25
  },
26
26
  "devDependencies": {
27
- "@types/node": "^25.6.1",
27
+ "@types/node": "^25.6.2",
28
28
  "tsup": "^8.5.1",
29
29
  "typescript": "6.0.3",
30
- "vitest": "^4.1.5",
31
- "@index9/core": "2.4.0"
30
+ "vitest": "^4.1.6",
31
+ "@index9/core": "2.5.0"
32
32
  },
33
33
  "engines": {
34
34
  "node": ">=20"