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.
@@ -439,11 +439,18 @@ var ProviderError = class extends Error {
439
439
  status;
440
440
  transient;
441
441
  retryAfterS;
442
+ /** True specifically for "the key is fine but this model isn't
443
+ * available to this account/project" (see http-errors.ts's
444
+ * isModelAccessFailure) — the one failure a model fallback chain can
445
+ * actually fix. Distinct from `transient`: this is never worth
446
+ * retrying the SAME model, but IS worth trying a different one. */
447
+ modelAccessFailure;
442
448
  constructor(kind, message, opts) {
443
449
  super(message);
444
450
  this.kind = kind;
445
451
  this.status = opts?.status;
446
452
  this.transient = opts?.transient ?? defaultTransient(kind);
453
+ this.modelAccessFailure = opts?.modelAccessFailure ?? false;
447
454
  if (opts?.retryAfterS !== void 0) {
448
455
  this.retryAfterS = opts.retryAfterS;
449
456
  }
@@ -462,8 +469,21 @@ function isCreditsFailure(status, bodyText) {
462
469
  }
463
470
  return false;
464
471
  }
465
- function httpFailureToProviderError(provider, status, bodyText, retryAfterS) {
472
+ 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;
473
+ function isModelAccessFailure(status, bodyText) {
474
+ if (status < 400 || status >= 500) return false;
475
+ return MODEL_ACCESS_RE.test(bodyText);
476
+ }
477
+ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, model) {
466
478
  const detail = bodyText.slice(0, 512);
479
+ if (isModelAccessFailure(status, bodyText)) {
480
+ const modelRef = model ? `"${model}"` : "the configured model";
481
+ return new ProviderError(
482
+ "client",
483
+ `${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}`,
484
+ { status, modelAccessFailure: true }
485
+ );
486
+ }
467
487
  if (isCreditsFailure(status, bodyText)) {
468
488
  return new ProviderError(
469
489
  "auth",
@@ -668,6 +688,14 @@ function buildAnthropicRequest(opts) {
668
688
  if (system !== void 0) {
669
689
  body.system = system;
670
690
  }
691
+ if (isReasoningModel("anthropic", opts.model)) {
692
+ if (opts.model.toLowerCase().startsWith("claude-fable-5")) {
693
+ body.thinking = { type: "adaptive" };
694
+ body.output_config = { effort: "low" };
695
+ } else {
696
+ body.thinking = { type: "disabled" };
697
+ }
698
+ }
671
699
  if (opts.jsonSchema) {
672
700
  body.tools = [{
673
701
  name: ANTHROPIC_STRUCTURED_TOOL_NAME,
@@ -735,7 +763,12 @@ function parseAnthropicResponse(opts) {
735
763
  purpose: opts.purpose
736
764
  };
737
765
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
738
- return { text, usage };
766
+ const stopReasonRaw = r["stop_reason"];
767
+ const stopReason = typeof stopReasonRaw === "string" ? stopReasonRaw : void 0;
768
+ return { text, usage, ...stopReason !== void 0 ? { stopReason } : {} };
769
+ }
770
+ function isEmptyDueToMaxTokens(text, stopReason) {
771
+ return text.trim() === "" && stopReason === "max_tokens";
739
772
  }
740
773
  function applyPricing(usage, pricing) {
741
774
  const { cost_usd, estimated } = calculateCost(
@@ -753,49 +786,64 @@ function applyPricing(usage, pricing) {
753
786
  estimated: usage.estimated || estimated
754
787
  };
755
788
  }
789
+ var MAX_TOKENS_ESCALATIONS = 2;
790
+ var ESCALATION_MULTIPLIER = 2;
756
791
  async function sendAnthropic(args) {
757
- const { url, headers, body } = buildAnthropicRequest({
758
- model: args.model,
759
- apiKey: args.apiKey,
760
- messages: args.messages,
761
- maxTokens: args.maxTokens,
762
- temperature: args.temperature,
763
- ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
764
- });
765
792
  const doFetch = args.fetchImpl ?? globalThis.fetch;
766
- const init = {
767
- method: "POST",
768
- headers,
769
- body: JSON.stringify(body)
770
- };
771
- if (args.signal) init.signal = args.signal;
772
- const attemptOnce = async () => {
773
- let respLike;
774
- try {
775
- respLike = await doFetch(url, init);
776
- } catch (e) {
777
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
778
- }
779
- const status = respLike.status;
780
- if (status >= 200 && status < 300) {
781
- let parsed;
793
+ let maxTokens = args.maxTokens;
794
+ let result = null;
795
+ let totalAttempts = 0;
796
+ for (let escalation = 0; escalation <= MAX_TOKENS_ESCALATIONS; escalation += 1) {
797
+ const { url, headers, body } = buildAnthropicRequest({
798
+ model: args.model,
799
+ apiKey: args.apiKey,
800
+ messages: args.messages,
801
+ maxTokens,
802
+ temperature: args.temperature,
803
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
804
+ });
805
+ const init = {
806
+ method: "POST",
807
+ headers,
808
+ body: JSON.stringify(body)
809
+ };
810
+ if (args.signal) init.signal = args.signal;
811
+ let stopReason;
812
+ const attemptOnce = async () => {
813
+ let respLike;
782
814
  try {
783
- parsed = await respLike.json();
815
+ respLike = await doFetch(url, init);
784
816
  } catch (e) {
785
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
817
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
786
818
  }
787
- const { text, usage } = parseAnthropicResponse({
788
- resp: parsed,
789
- model: args.model,
790
- purpose: args.purpose ?? "worker"
791
- });
792
- return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
819
+ const status = respLike.status;
820
+ if (status >= 200 && status < 300) {
821
+ let parsed;
822
+ try {
823
+ parsed = await respLike.json();
824
+ } catch (e) {
825
+ throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
826
+ }
827
+ const { text, usage, stopReason: sr } = parseAnthropicResponse({
828
+ resp: parsed,
829
+ model: args.model,
830
+ purpose: args.purpose ?? "worker"
831
+ });
832
+ stopReason = sr;
833
+ return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
834
+ }
835
+ const bodyText = await respLike.text().catch(() => "");
836
+ throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
837
+ };
838
+ await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
839
+ result = await sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
840
+ totalAttempts += result.attempts;
841
+ if (escalation >= MAX_TOKENS_ESCALATIONS || !isEmptyDueToMaxTokens(result.text, stopReason)) {
842
+ break;
793
843
  }
794
- const bodyText = await respLike.text().catch(() => "");
795
- throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike));
796
- };
797
- await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
798
- return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
844
+ maxTokens *= ESCALATION_MULTIPLIER;
845
+ }
846
+ return { ...result, attempts: totalAttempts };
799
847
  }
800
848
 
801
849
  // src/providers/gemini.ts
@@ -970,7 +1018,7 @@ async function sendGemini(args) {
970
1018
  return { text, attempts: 1, usage: applyPricing2(usage, args.pricing) };
971
1019
  }
972
1020
  const bodyText = await respLike.text().catch(() => "");
973
- throw httpFailureToProviderError("gemini", status, bodyText, parseRetryAfter(respLike));
1021
+ throw httpFailureToProviderError("gemini", status, bodyText, parseRetryAfter(respLike), args.model);
974
1022
  };
975
1023
  await acquireRateLimit("gemini", args.signal ? { signal: args.signal } : void 0);
976
1024
  return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
@@ -1143,7 +1191,7 @@ async function sendOpenAICompatible(args) {
1143
1191
  return { text, attempts: 1, usage: applyPricing3(usage, args.pricing) };
1144
1192
  }
1145
1193
  const bodyText = await respLike.text().catch(() => "");
1146
- throw httpFailureToProviderError(args.provider, status, bodyText, parseRetryAfter(respLike));
1194
+ throw httpFailureToProviderError(args.provider, status, bodyText, parseRetryAfter(respLike), args.model);
1147
1195
  };
1148
1196
  await acquireRateLimit(args.provider, args.signal ? { signal: args.signal } : void 0);
1149
1197
  return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
@@ -1152,7 +1200,10 @@ async function sendOpenAICompatible(args) {
1152
1200
  // src/providers/registry.ts
1153
1201
  var DEFAULT_MODELS = {
1154
1202
  anthropic: "claude-opus-5",
1155
- openai: "gpt-5.5",
1203
+ // gpt-5.5 is not enabled on every OpenAI account/project (some are still
1204
+ // capped at gpt-5) — gpt-5 is the safer, more broadly available default
1205
+ // until a user/org explicitly opts into 5.5 via `crosscheck models set`.
1206
+ openai: "gpt-5",
1156
1207
  xai: "grok-4-latest",
1157
1208
  mistral: "mistral-large-latest",
1158
1209
  groq: "llama-3.3-70b-versatile",
@@ -1161,12 +1212,31 @@ var DEFAULT_MODELS = {
1161
1212
  kimi: "kimi-k3",
1162
1213
  qwen: "qwen3.8-max"
1163
1214
  };
1215
+ var MODEL_ENV_VARS = {
1216
+ anthropic: "ANTHROPIC_MODEL",
1217
+ openai: "OPENAI_MODEL",
1218
+ xai: "XAI_MODEL",
1219
+ mistral: "MISTRAL_MODEL",
1220
+ groq: "GROQ_MODEL",
1221
+ deepseek: "DEEPSEEK_MODEL",
1222
+ gemini: "GEMINI_MODEL",
1223
+ kimi: "KIMI_MODEL",
1224
+ qwen: "QWEN_MODEL"
1225
+ };
1226
+ function parseFallbackModels(raw) {
1227
+ if (!raw) return [];
1228
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
1229
+ }
1230
+ function fallbackModelsFor(env, provider) {
1231
+ const envVar = MODEL_ENV_VARS[provider];
1232
+ return envVar ? parseFallbackModels(env[`${envVar}_FALLBACKS`]) : [];
1233
+ }
1164
1234
  function buildProviders(opts) {
1165
1235
  const out = {};
1166
1236
  const anthropicKey = opts.env["ANTHROPIC_API_KEY"];
1167
1237
  if (anthropicKey) {
1168
1238
  const model = opts.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODELS.anthropic;
1169
- out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts);
1239
+ out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts, fallbackModelsFor(opts.env, "anthropic"));
1170
1240
  }
1171
1241
  const openAiCompatSpec = [
1172
1242
  { name: "openai", keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", defaultModel: DEFAULT_MODELS["openai"] },
@@ -1181,19 +1251,20 @@ function buildProviders(opts) {
1181
1251
  const apiKey = opts.env[s.keyEnv];
1182
1252
  if (!apiKey) continue;
1183
1253
  const model = opts.env[s.modelEnv] ?? s.defaultModel;
1184
- out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts);
1254
+ out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts, fallbackModelsFor(opts.env, s.name));
1185
1255
  }
1186
1256
  const geminiKey = opts.env["GEMINI_API_KEY"];
1187
1257
  if (geminiKey) {
1188
1258
  const model = opts.env["GEMINI_MODEL"] ?? DEFAULT_MODELS.gemini;
1189
- out["gemini"] = makeGeminiProvider(model, geminiKey, opts);
1259
+ out["gemini"] = makeGeminiProvider(model, geminiKey, opts, fallbackModelsFor(opts.env, "gemini"));
1190
1260
  }
1191
1261
  return out;
1192
1262
  }
1193
- function makeAnthropicProvider(model, apiKey, opts) {
1263
+ function makeAnthropicProvider(model, apiKey, opts, fallbackModels = []) {
1194
1264
  return {
1195
1265
  name: "anthropic",
1196
1266
  model,
1267
+ fallbackModels,
1197
1268
  send: async (args) => {
1198
1269
  const sendOpts = {
1199
1270
  ...args,
@@ -1206,11 +1277,12 @@ function makeAnthropicProvider(model, apiKey, opts) {
1206
1277
  }
1207
1278
  };
1208
1279
  }
1209
- function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1280
+ function makeOpenAICompatibleProvider(name, model, apiKey, opts, fallbackModels = []) {
1210
1281
  const url = OPENAI_COMPAT_DEFAULT_URLS[name];
1211
1282
  return {
1212
1283
  name,
1213
1284
  model,
1285
+ fallbackModels,
1214
1286
  send: async (args) => {
1215
1287
  const sendOpts = {
1216
1288
  ...args,
@@ -1225,10 +1297,11 @@ function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1225
1297
  }
1226
1298
  };
1227
1299
  }
1228
- function makeGeminiProvider(model, apiKey, opts) {
1300
+ function makeGeminiProvider(model, apiKey, opts, fallbackModels = []) {
1229
1301
  return {
1230
1302
  name: "gemini",
1231
1303
  model,
1304
+ fallbackModels,
1232
1305
  send: async (args) => {
1233
1306
  const sendOpts = {
1234
1307
  ...args,
@@ -1247,7 +1320,7 @@ import { z } from "zod";
1247
1320
 
1248
1321
  // src/server-meta.ts
1249
1322
  var SERVER_NAME = "crosscheck-agent";
1250
- var SERVER_VERSION = true ? "0.2.13" : "0.0.0-dev";
1323
+ var SERVER_VERSION = true ? "0.2.15" : "0.0.0-dev";
1251
1324
 
1252
1325
  // src/tools/audit.ts
1253
1326
  import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
@@ -1898,6 +1971,51 @@ function operatorCeiling(purpose, provider) {
1898
1971
  return null;
1899
1972
  }
1900
1973
 
1974
+ // src/core/retarget.ts
1975
+ function retargetProvider(p, newModel) {
1976
+ if (p.model === newModel) return p;
1977
+ return {
1978
+ name: p.name,
1979
+ model: newModel,
1980
+ send: (args) => p.send({ ...args, modelOverride: newModel })
1981
+ };
1982
+ }
1983
+ async function loadProviderWeights(storage, names) {
1984
+ const out = {};
1985
+ for (const raw of names) {
1986
+ const name = raw.toLowerCase();
1987
+ if (name in out) continue;
1988
+ const row = await storage.getProviderStats(name);
1989
+ if (!row) {
1990
+ out[name] = 0.5;
1991
+ continue;
1992
+ }
1993
+ const total = row.wins + row.losses + row.abstains;
1994
+ out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
1995
+ }
1996
+ return out;
1997
+ }
1998
+
1999
+ // src/core/model-fallback.ts
2000
+ async function sendWithModelFallback(provider, args) {
2001
+ const chain = [provider.model, ...provider.fallbackModels ?? []];
2002
+ let lastErr;
2003
+ for (let i = 0; i < chain.length; i += 1) {
2004
+ const candidate = chain[i];
2005
+ const p = i === 0 ? provider : retargetProvider(provider, candidate);
2006
+ try {
2007
+ const result = await p.send(args);
2008
+ return { result, modelUsed: candidate };
2009
+ } catch (e) {
2010
+ lastErr = e;
2011
+ const isModelAccessFailure2 = e instanceof ProviderError && e.modelAccessFailure;
2012
+ const hasMoreCandidates = i < chain.length - 1;
2013
+ if (!isModelAccessFailure2 || !hasMoreCandidates) throw e;
2014
+ }
2015
+ }
2016
+ throw lastErr;
2017
+ }
2018
+
1901
2019
  // src/core/structured.ts
1902
2020
  async function requestStructured(provider, baseMessages, schema, opts) {
1903
2021
  const maxRetries = opts.maxRetries ?? 1;
@@ -1976,7 +2094,7 @@ async function askOne(provider, messages, opts) {
1976
2094
  const ceiling = operatorCeiling(opts.purpose, provider.name);
1977
2095
  if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
1978
2096
  try {
1979
- const r = await provider.send({
2097
+ const { result: r, modelUsed } = await sendWithModelFallback(provider, {
1980
2098
  messages,
1981
2099
  maxTokens,
1982
2100
  temperature: opts.temperature,
@@ -1989,7 +2107,10 @@ async function askOne(provider, messages, opts) {
1989
2107
  const cpuMs = Math.trunc((cpu.user + cpu.system) / 1e3);
1990
2108
  return {
1991
2109
  provider: provider.name,
1992
- model: provider.model,
2110
+ // modelUsed reflects whichever model actually answered — the
2111
+ // primary, or a fallback if one was needed. Equivalent to the old
2112
+ // `provider.model` whenever no fallback occurred.
2113
+ model: modelUsed,
1993
2114
  response: r.text,
1994
2115
  attempts: r.attempts,
1995
2116
  usage: r.usage,
@@ -2699,31 +2820,6 @@ function numberOrNull(v) {
2699
2820
  // src/tools/audit.ts
2700
2821
  import { performance as performance2 } from "perf_hooks";
2701
2822
 
2702
- // src/core/retarget.ts
2703
- function retargetProvider(p, newModel) {
2704
- if (p.model === newModel) return p;
2705
- return {
2706
- name: p.name,
2707
- model: newModel,
2708
- send: (args) => p.send({ ...args, modelOverride: newModel })
2709
- };
2710
- }
2711
- async function loadProviderWeights(storage, names) {
2712
- const out = {};
2713
- for (const raw of names) {
2714
- const name = raw.toLowerCase();
2715
- if (name in out) continue;
2716
- const row = await storage.getProviderStats(name);
2717
- if (!row) {
2718
- out[name] = 0.5;
2719
- continue;
2720
- }
2721
- const total = row.wins + row.losses + row.abstains;
2722
- out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
2723
- }
2724
- return out;
2725
- }
2726
-
2727
2823
  // src/core/co-reason.ts
2728
2824
  var CO_REASON_MODEL = "gpt-5.6";
2729
2825
  var CO_REASON_PROVIDER = "openai";
@@ -11474,7 +11570,7 @@ var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
11474
11570
  var DEFAULT_PACKAGE = "crosscheck-cli";
11475
11571
  var FETCH_TIMEOUT_MS = 3e3;
11476
11572
  function engineVersion() {
11477
- return true ? "0.2.13" : "0.0.0-dev";
11573
+ return true ? "0.2.15" : "0.0.0-dev";
11478
11574
  }
11479
11575
  function defaultUpdateCachePath() {
11480
11576
  const base = process.env["CROSSCHECK_DATA_DIR"] || path9.join(os.homedir() || os.tmpdir(), ".crosscheck");