crosscheck-mcp 0.2.13 → 0.2.15

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.
@@ -472,11 +472,18 @@ var ProviderError = class extends Error {
472
472
  status;
473
473
  transient;
474
474
  retryAfterS;
475
+ /** True specifically for "the key is fine but this model isn't
476
+ * available to this account/project" (see http-errors.ts's
477
+ * isModelAccessFailure) — the one failure a model fallback chain can
478
+ * actually fix. Distinct from `transient`: this is never worth
479
+ * retrying the SAME model, but IS worth trying a different one. */
480
+ modelAccessFailure;
475
481
  constructor(kind, message, opts) {
476
482
  super(message);
477
483
  this.kind = kind;
478
484
  this.status = opts?.status;
479
485
  this.transient = opts?.transient ?? defaultTransient(kind);
486
+ this.modelAccessFailure = opts?.modelAccessFailure ?? false;
480
487
  if (opts?.retryAfterS !== void 0) {
481
488
  this.retryAfterS = opts.retryAfterS;
482
489
  }
@@ -495,8 +502,21 @@ function isCreditsFailure(status, bodyText) {
495
502
  }
496
503
  return false;
497
504
  }
498
- function httpFailureToProviderError(provider, status, bodyText, retryAfterS) {
505
+ var MODEL_ACCESS_RE = /model_not_found|does not have access to model|does not exist|model[\s_]not[\s_]found|invalid model|unknown model|unsupported model/i;
506
+ function isModelAccessFailure(status, bodyText) {
507
+ if (status < 400 || status >= 500) return false;
508
+ return MODEL_ACCESS_RE.test(bodyText);
509
+ }
510
+ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, model) {
499
511
  const detail = bodyText.slice(0, 512);
512
+ if (isModelAccessFailure(status, bodyText)) {
513
+ const modelRef = model ? `"${model}"` : "the configured model";
514
+ return new ProviderError(
515
+ "client",
516
+ `${provider}: your account/project doesn't have access to ${modelRef} (HTTP ${status}). This is a model-access problem, not a key problem \u2014 your ${provider} API key is fine, ${modelRef} just isn't enabled for this account/project. Fix: run \`crosscheck models set ${provider} <a-model-you-have-access-to>\` (or set ${provider.toUpperCase()}_MODEL in your .env until that's available). Detail: ${detail}`,
517
+ { status, modelAccessFailure: true }
518
+ );
519
+ }
500
520
  if (isCreditsFailure(status, bodyText)) {
501
521
  return new ProviderError(
502
522
  "auth",
@@ -701,6 +721,14 @@ function buildAnthropicRequest(opts) {
701
721
  if (system !== void 0) {
702
722
  body.system = system;
703
723
  }
724
+ if (isReasoningModel("anthropic", opts.model)) {
725
+ if (opts.model.toLowerCase().startsWith("claude-fable-5")) {
726
+ body.thinking = { type: "adaptive" };
727
+ body.output_config = { effort: "low" };
728
+ } else {
729
+ body.thinking = { type: "disabled" };
730
+ }
731
+ }
704
732
  if (opts.jsonSchema) {
705
733
  body.tools = [{
706
734
  name: ANTHROPIC_STRUCTURED_TOOL_NAME,
@@ -768,7 +796,12 @@ function parseAnthropicResponse(opts) {
768
796
  purpose: opts.purpose
769
797
  };
770
798
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
771
- return { text, usage };
799
+ const stopReasonRaw = r["stop_reason"];
800
+ const stopReason = typeof stopReasonRaw === "string" ? stopReasonRaw : void 0;
801
+ return { text, usage, ...stopReason !== void 0 ? { stopReason } : {} };
802
+ }
803
+ function isEmptyDueToMaxTokens(text, stopReason) {
804
+ return text.trim() === "" && stopReason === "max_tokens";
772
805
  }
773
806
  function applyPricing(usage, pricing) {
774
807
  const { cost_usd, estimated } = calculateCost(
@@ -786,49 +819,64 @@ function applyPricing(usage, pricing) {
786
819
  estimated: usage.estimated || estimated
787
820
  };
788
821
  }
822
+ var MAX_TOKENS_ESCALATIONS = 2;
823
+ var ESCALATION_MULTIPLIER = 2;
789
824
  async function sendAnthropic(args) {
790
- const { url, headers, body } = buildAnthropicRequest({
791
- model: args.model,
792
- apiKey: args.apiKey,
793
- messages: args.messages,
794
- maxTokens: args.maxTokens,
795
- temperature: args.temperature,
796
- ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
797
- });
798
825
  const doFetch = args.fetchImpl ?? globalThis.fetch;
799
- const init = {
800
- method: "POST",
801
- headers,
802
- body: JSON.stringify(body)
803
- };
804
- if (args.signal) init.signal = args.signal;
805
- const attemptOnce = async () => {
806
- let respLike;
807
- try {
808
- respLike = await doFetch(url, init);
809
- } catch (e) {
810
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
811
- }
812
- const status = respLike.status;
813
- if (status >= 200 && status < 300) {
814
- let parsed;
826
+ let maxTokens = args.maxTokens;
827
+ let result = null;
828
+ let totalAttempts = 0;
829
+ for (let escalation = 0; escalation <= MAX_TOKENS_ESCALATIONS; escalation += 1) {
830
+ const { url, headers, body } = buildAnthropicRequest({
831
+ model: args.model,
832
+ apiKey: args.apiKey,
833
+ messages: args.messages,
834
+ maxTokens,
835
+ temperature: args.temperature,
836
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
837
+ });
838
+ const init = {
839
+ method: "POST",
840
+ headers,
841
+ body: JSON.stringify(body)
842
+ };
843
+ if (args.signal) init.signal = args.signal;
844
+ let stopReason;
845
+ const attemptOnce = async () => {
846
+ let respLike;
815
847
  try {
816
- parsed = await respLike.json();
848
+ respLike = await doFetch(url, init);
817
849
  } catch (e) {
818
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
850
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
819
851
  }
820
- const { text, usage } = parseAnthropicResponse({
821
- resp: parsed,
822
- model: args.model,
823
- purpose: args.purpose ?? "worker"
824
- });
825
- return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
852
+ const status = respLike.status;
853
+ if (status >= 200 && status < 300) {
854
+ let parsed;
855
+ try {
856
+ parsed = await respLike.json();
857
+ } catch (e) {
858
+ throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
859
+ }
860
+ const { text, usage, stopReason: sr } = parseAnthropicResponse({
861
+ resp: parsed,
862
+ model: args.model,
863
+ purpose: args.purpose ?? "worker"
864
+ });
865
+ stopReason = sr;
866
+ return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
867
+ }
868
+ const bodyText = await respLike.text().catch(() => "");
869
+ throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
870
+ };
871
+ await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
872
+ result = await sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
873
+ totalAttempts += result.attempts;
874
+ if (escalation >= MAX_TOKENS_ESCALATIONS || !isEmptyDueToMaxTokens(result.text, stopReason)) {
875
+ break;
826
876
  }
827
- const bodyText = await respLike.text().catch(() => "");
828
- throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike));
829
- };
830
- await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
831
- return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
877
+ maxTokens *= ESCALATION_MULTIPLIER;
878
+ }
879
+ return { ...result, attempts: totalAttempts };
832
880
  }
833
881
 
834
882
  // src/providers/gemini.ts
@@ -1003,7 +1051,7 @@ async function sendGemini(args) {
1003
1051
  return { text, attempts: 1, usage: applyPricing2(usage, args.pricing) };
1004
1052
  }
1005
1053
  const bodyText = await respLike.text().catch(() => "");
1006
- throw httpFailureToProviderError("gemini", status, bodyText, parseRetryAfter(respLike));
1054
+ throw httpFailureToProviderError("gemini", status, bodyText, parseRetryAfter(respLike), args.model);
1007
1055
  };
1008
1056
  await acquireRateLimit("gemini", args.signal ? { signal: args.signal } : void 0);
1009
1057
  return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
@@ -1176,7 +1224,7 @@ async function sendOpenAICompatible(args) {
1176
1224
  return { text, attempts: 1, usage: applyPricing3(usage, args.pricing) };
1177
1225
  }
1178
1226
  const bodyText = await respLike.text().catch(() => "");
1179
- throw httpFailureToProviderError(args.provider, status, bodyText, parseRetryAfter(respLike));
1227
+ throw httpFailureToProviderError(args.provider, status, bodyText, parseRetryAfter(respLike), args.model);
1180
1228
  };
1181
1229
  await acquireRateLimit(args.provider, args.signal ? { signal: args.signal } : void 0);
1182
1230
  return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
@@ -1185,7 +1233,10 @@ async function sendOpenAICompatible(args) {
1185
1233
  // src/providers/registry.ts
1186
1234
  var DEFAULT_MODELS = {
1187
1235
  anthropic: "claude-opus-5",
1188
- openai: "gpt-5.5",
1236
+ // gpt-5.5 is not enabled on every OpenAI account/project (some are still
1237
+ // capped at gpt-5) — gpt-5 is the safer, more broadly available default
1238
+ // until a user/org explicitly opts into 5.5 via `crosscheck models set`.
1239
+ openai: "gpt-5",
1189
1240
  xai: "grok-4-latest",
1190
1241
  mistral: "mistral-large-latest",
1191
1242
  groq: "llama-3.3-70b-versatile",
@@ -1194,12 +1245,31 @@ var DEFAULT_MODELS = {
1194
1245
  kimi: "kimi-k3",
1195
1246
  qwen: "qwen3.8-max"
1196
1247
  };
1248
+ var MODEL_ENV_VARS = {
1249
+ anthropic: "ANTHROPIC_MODEL",
1250
+ openai: "OPENAI_MODEL",
1251
+ xai: "XAI_MODEL",
1252
+ mistral: "MISTRAL_MODEL",
1253
+ groq: "GROQ_MODEL",
1254
+ deepseek: "DEEPSEEK_MODEL",
1255
+ gemini: "GEMINI_MODEL",
1256
+ kimi: "KIMI_MODEL",
1257
+ qwen: "QWEN_MODEL"
1258
+ };
1259
+ function parseFallbackModels(raw) {
1260
+ if (!raw) return [];
1261
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
1262
+ }
1263
+ function fallbackModelsFor(env, provider) {
1264
+ const envVar = MODEL_ENV_VARS[provider];
1265
+ return envVar ? parseFallbackModels(env[`${envVar}_FALLBACKS`]) : [];
1266
+ }
1197
1267
  function buildProviders(opts) {
1198
1268
  const out = {};
1199
1269
  const anthropicKey = opts.env["ANTHROPIC_API_KEY"];
1200
1270
  if (anthropicKey) {
1201
1271
  const model = opts.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODELS.anthropic;
1202
- out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts);
1272
+ out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts, fallbackModelsFor(opts.env, "anthropic"));
1203
1273
  }
1204
1274
  const openAiCompatSpec = [
1205
1275
  { name: "openai", keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", defaultModel: DEFAULT_MODELS["openai"] },
@@ -1214,19 +1284,20 @@ function buildProviders(opts) {
1214
1284
  const apiKey = opts.env[s.keyEnv];
1215
1285
  if (!apiKey) continue;
1216
1286
  const model = opts.env[s.modelEnv] ?? s.defaultModel;
1217
- out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts);
1287
+ out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts, fallbackModelsFor(opts.env, s.name));
1218
1288
  }
1219
1289
  const geminiKey = opts.env["GEMINI_API_KEY"];
1220
1290
  if (geminiKey) {
1221
1291
  const model = opts.env["GEMINI_MODEL"] ?? DEFAULT_MODELS.gemini;
1222
- out["gemini"] = makeGeminiProvider(model, geminiKey, opts);
1292
+ out["gemini"] = makeGeminiProvider(model, geminiKey, opts, fallbackModelsFor(opts.env, "gemini"));
1223
1293
  }
1224
1294
  return out;
1225
1295
  }
1226
- function makeAnthropicProvider(model, apiKey, opts) {
1296
+ function makeAnthropicProvider(model, apiKey, opts, fallbackModels = []) {
1227
1297
  return {
1228
1298
  name: "anthropic",
1229
1299
  model,
1300
+ fallbackModels,
1230
1301
  send: async (args) => {
1231
1302
  const sendOpts = {
1232
1303
  ...args,
@@ -1239,11 +1310,12 @@ function makeAnthropicProvider(model, apiKey, opts) {
1239
1310
  }
1240
1311
  };
1241
1312
  }
1242
- function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1313
+ function makeOpenAICompatibleProvider(name, model, apiKey, opts, fallbackModels = []) {
1243
1314
  const url = OPENAI_COMPAT_DEFAULT_URLS[name];
1244
1315
  return {
1245
1316
  name,
1246
1317
  model,
1318
+ fallbackModels,
1247
1319
  send: async (args) => {
1248
1320
  const sendOpts = {
1249
1321
  ...args,
@@ -1258,10 +1330,11 @@ function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1258
1330
  }
1259
1331
  };
1260
1332
  }
1261
- function makeGeminiProvider(model, apiKey, opts) {
1333
+ function makeGeminiProvider(model, apiKey, opts, fallbackModels = []) {
1262
1334
  return {
1263
1335
  name: "gemini",
1264
1336
  model,
1337
+ fallbackModels,
1265
1338
  send: async (args) => {
1266
1339
  const sendOpts = {
1267
1340
  ...args,
@@ -1280,7 +1353,7 @@ var import_zod = require("zod");
1280
1353
 
1281
1354
  // src/server-meta.ts
1282
1355
  var SERVER_NAME = "crosscheck-agent";
1283
- var SERVER_VERSION = true ? "0.2.13" : "0.0.0-dev";
1356
+ var SERVER_VERSION = true ? "0.2.15" : "0.0.0-dev";
1284
1357
 
1285
1358
  // src/tools/audit.ts
1286
1359
  var import_node_fs4 = require("fs");
@@ -1931,6 +2004,51 @@ function operatorCeiling(purpose, provider) {
1931
2004
  return null;
1932
2005
  }
1933
2006
 
2007
+ // src/core/retarget.ts
2008
+ function retargetProvider(p, newModel) {
2009
+ if (p.model === newModel) return p;
2010
+ return {
2011
+ name: p.name,
2012
+ model: newModel,
2013
+ send: (args) => p.send({ ...args, modelOverride: newModel })
2014
+ };
2015
+ }
2016
+ async function loadProviderWeights(storage, names) {
2017
+ const out = {};
2018
+ for (const raw of names) {
2019
+ const name = raw.toLowerCase();
2020
+ if (name in out) continue;
2021
+ const row = await storage.getProviderStats(name);
2022
+ if (!row) {
2023
+ out[name] = 0.5;
2024
+ continue;
2025
+ }
2026
+ const total = row.wins + row.losses + row.abstains;
2027
+ out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
2028
+ }
2029
+ return out;
2030
+ }
2031
+
2032
+ // src/core/model-fallback.ts
2033
+ async function sendWithModelFallback(provider, args) {
2034
+ const chain = [provider.model, ...provider.fallbackModels ?? []];
2035
+ let lastErr;
2036
+ for (let i = 0; i < chain.length; i += 1) {
2037
+ const candidate = chain[i];
2038
+ const p = i === 0 ? provider : retargetProvider(provider, candidate);
2039
+ try {
2040
+ const result = await p.send(args);
2041
+ return { result, modelUsed: candidate };
2042
+ } catch (e) {
2043
+ lastErr = e;
2044
+ const isModelAccessFailure2 = e instanceof ProviderError && e.modelAccessFailure;
2045
+ const hasMoreCandidates = i < chain.length - 1;
2046
+ if (!isModelAccessFailure2 || !hasMoreCandidates) throw e;
2047
+ }
2048
+ }
2049
+ throw lastErr;
2050
+ }
2051
+
1934
2052
  // src/core/structured.ts
1935
2053
  async function requestStructured(provider, baseMessages, schema, opts) {
1936
2054
  const maxRetries = opts.maxRetries ?? 1;
@@ -2009,7 +2127,7 @@ async function askOne(provider, messages, opts) {
2009
2127
  const ceiling = operatorCeiling(opts.purpose, provider.name);
2010
2128
  if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
2011
2129
  try {
2012
- const r = await provider.send({
2130
+ const { result: r, modelUsed } = await sendWithModelFallback(provider, {
2013
2131
  messages,
2014
2132
  maxTokens,
2015
2133
  temperature: opts.temperature,
@@ -2022,7 +2140,10 @@ async function askOne(provider, messages, opts) {
2022
2140
  const cpuMs = Math.trunc((cpu.user + cpu.system) / 1e3);
2023
2141
  return {
2024
2142
  provider: provider.name,
2025
- model: provider.model,
2143
+ // modelUsed reflects whichever model actually answered — the
2144
+ // primary, or a fallback if one was needed. Equivalent to the old
2145
+ // `provider.model` whenever no fallback occurred.
2146
+ model: modelUsed,
2026
2147
  response: r.text,
2027
2148
  attempts: r.attempts,
2028
2149
  usage: r.usage,
@@ -2732,31 +2853,6 @@ function numberOrNull(v) {
2732
2853
  // src/tools/audit.ts
2733
2854
  var import_node_perf_hooks = require("perf_hooks");
2734
2855
 
2735
- // src/core/retarget.ts
2736
- function retargetProvider(p, newModel) {
2737
- if (p.model === newModel) return p;
2738
- return {
2739
- name: p.name,
2740
- model: newModel,
2741
- send: (args) => p.send({ ...args, modelOverride: newModel })
2742
- };
2743
- }
2744
- async function loadProviderWeights(storage, names) {
2745
- const out = {};
2746
- for (const raw of names) {
2747
- const name = raw.toLowerCase();
2748
- if (name in out) continue;
2749
- const row = await storage.getProviderStats(name);
2750
- if (!row) {
2751
- out[name] = 0.5;
2752
- continue;
2753
- }
2754
- const total = row.wins + row.losses + row.abstains;
2755
- out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
2756
- }
2757
- return out;
2758
- }
2759
-
2760
2856
  // src/core/co-reason.ts
2761
2857
  var CO_REASON_MODEL = "gpt-5.6";
2762
2858
  var CO_REASON_PROVIDER = "openai";
@@ -11490,7 +11586,7 @@ var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
11490
11586
  var DEFAULT_PACKAGE = "crosscheck-cli";
11491
11587
  var FETCH_TIMEOUT_MS = 3e3;
11492
11588
  function engineVersion() {
11493
- return true ? "0.2.13" : "0.0.0-dev";
11589
+ return true ? "0.2.15" : "0.0.0-dev";
11494
11590
  }
11495
11591
  function defaultUpdateCachePath() {
11496
11592
  const base = process.env["CROSSCHECK_DATA_DIR"] || import_node_path12.default.join(import_node_os2.default.homedir() || import_node_os2.default.tmpdir(), ".crosscheck");