crosscheck-mcp 0.2.14 → 0.2.16

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
  }
@@ -474,7 +481,7 @@ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, mod
474
481
  return new ProviderError(
475
482
  "client",
476
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}`,
477
- { status }
484
+ { status, modelAccessFailure: true }
478
485
  );
479
486
  }
480
487
  if (isCreditsFailure(status, bodyText)) {
@@ -681,6 +688,14 @@ function buildAnthropicRequest(opts) {
681
688
  if (system !== void 0) {
682
689
  body.system = system;
683
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
+ }
684
699
  if (opts.jsonSchema) {
685
700
  body.tools = [{
686
701
  name: ANTHROPIC_STRUCTURED_TOOL_NAME,
@@ -748,7 +763,12 @@ function parseAnthropicResponse(opts) {
748
763
  purpose: opts.purpose
749
764
  };
750
765
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
751
- 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";
752
772
  }
753
773
  function applyPricing(usage, pricing) {
754
774
  const { cost_usd, estimated } = calculateCost(
@@ -766,49 +786,64 @@ function applyPricing(usage, pricing) {
766
786
  estimated: usage.estimated || estimated
767
787
  };
768
788
  }
789
+ var MAX_TOKENS_ESCALATIONS = 2;
790
+ var ESCALATION_MULTIPLIER = 2;
769
791
  async function sendAnthropic(args) {
770
- const { url, headers, body } = buildAnthropicRequest({
771
- model: args.model,
772
- apiKey: args.apiKey,
773
- messages: args.messages,
774
- maxTokens: args.maxTokens,
775
- temperature: args.temperature,
776
- ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
777
- });
778
792
  const doFetch = args.fetchImpl ?? globalThis.fetch;
779
- const init = {
780
- method: "POST",
781
- headers,
782
- body: JSON.stringify(body)
783
- };
784
- if (args.signal) init.signal = args.signal;
785
- const attemptOnce = async () => {
786
- let respLike;
787
- try {
788
- respLike = await doFetch(url, init);
789
- } catch (e) {
790
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
791
- }
792
- const status = respLike.status;
793
- if (status >= 200 && status < 300) {
794
- 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;
795
814
  try {
796
- parsed = await respLike.json();
815
+ respLike = await doFetch(url, init);
797
816
  } catch (e) {
798
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
817
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
799
818
  }
800
- const { text, usage } = parseAnthropicResponse({
801
- resp: parsed,
802
- model: args.model,
803
- purpose: args.purpose ?? "worker"
804
- });
805
- 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;
806
843
  }
807
- const bodyText = await respLike.text().catch(() => "");
808
- throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
809
- };
810
- await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
811
- return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
844
+ maxTokens *= ESCALATION_MULTIPLIER;
845
+ }
846
+ return { ...result, attempts: totalAttempts };
812
847
  }
813
848
 
814
849
  // src/providers/gemini.ts
@@ -1177,12 +1212,31 @@ var DEFAULT_MODELS = {
1177
1212
  kimi: "kimi-k3",
1178
1213
  qwen: "qwen3.8-max"
1179
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
+ }
1180
1234
  function buildProviders(opts) {
1181
1235
  const out = {};
1182
1236
  const anthropicKey = opts.env["ANTHROPIC_API_KEY"];
1183
1237
  if (anthropicKey) {
1184
1238
  const model = opts.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODELS.anthropic;
1185
- out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts);
1239
+ out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts, fallbackModelsFor(opts.env, "anthropic"));
1186
1240
  }
1187
1241
  const openAiCompatSpec = [
1188
1242
  { name: "openai", keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", defaultModel: DEFAULT_MODELS["openai"] },
@@ -1197,19 +1251,20 @@ function buildProviders(opts) {
1197
1251
  const apiKey = opts.env[s.keyEnv];
1198
1252
  if (!apiKey) continue;
1199
1253
  const model = opts.env[s.modelEnv] ?? s.defaultModel;
1200
- out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts);
1254
+ out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts, fallbackModelsFor(opts.env, s.name));
1201
1255
  }
1202
1256
  const geminiKey = opts.env["GEMINI_API_KEY"];
1203
1257
  if (geminiKey) {
1204
1258
  const model = opts.env["GEMINI_MODEL"] ?? DEFAULT_MODELS.gemini;
1205
- out["gemini"] = makeGeminiProvider(model, geminiKey, opts);
1259
+ out["gemini"] = makeGeminiProvider(model, geminiKey, opts, fallbackModelsFor(opts.env, "gemini"));
1206
1260
  }
1207
1261
  return out;
1208
1262
  }
1209
- function makeAnthropicProvider(model, apiKey, opts) {
1263
+ function makeAnthropicProvider(model, apiKey, opts, fallbackModels = []) {
1210
1264
  return {
1211
1265
  name: "anthropic",
1212
1266
  model,
1267
+ fallbackModels,
1213
1268
  send: async (args) => {
1214
1269
  const sendOpts = {
1215
1270
  ...args,
@@ -1222,11 +1277,12 @@ function makeAnthropicProvider(model, apiKey, opts) {
1222
1277
  }
1223
1278
  };
1224
1279
  }
1225
- function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1280
+ function makeOpenAICompatibleProvider(name, model, apiKey, opts, fallbackModels = []) {
1226
1281
  const url = OPENAI_COMPAT_DEFAULT_URLS[name];
1227
1282
  return {
1228
1283
  name,
1229
1284
  model,
1285
+ fallbackModels,
1230
1286
  send: async (args) => {
1231
1287
  const sendOpts = {
1232
1288
  ...args,
@@ -1241,10 +1297,11 @@ function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1241
1297
  }
1242
1298
  };
1243
1299
  }
1244
- function makeGeminiProvider(model, apiKey, opts) {
1300
+ function makeGeminiProvider(model, apiKey, opts, fallbackModels = []) {
1245
1301
  return {
1246
1302
  name: "gemini",
1247
1303
  model,
1304
+ fallbackModels,
1248
1305
  send: async (args) => {
1249
1306
  const sendOpts = {
1250
1307
  ...args,
@@ -1263,7 +1320,7 @@ import { z } from "zod";
1263
1320
 
1264
1321
  // src/server-meta.ts
1265
1322
  var SERVER_NAME = "crosscheck-agent";
1266
- var SERVER_VERSION = true ? "0.2.14" : "0.0.0-dev";
1323
+ var SERVER_VERSION = true ? "0.2.16" : "0.0.0-dev";
1267
1324
 
1268
1325
  // src/tools/audit.ts
1269
1326
  import { readdirSync, readFileSync as readFileSync3, statSync } from "fs";
@@ -1914,6 +1971,51 @@ function operatorCeiling(purpose, provider) {
1914
1971
  return null;
1915
1972
  }
1916
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
+
1917
2019
  // src/core/structured.ts
1918
2020
  async function requestStructured(provider, baseMessages, schema, opts) {
1919
2021
  const maxRetries = opts.maxRetries ?? 1;
@@ -1992,7 +2094,7 @@ async function askOne(provider, messages, opts) {
1992
2094
  const ceiling = operatorCeiling(opts.purpose, provider.name);
1993
2095
  if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
1994
2096
  try {
1995
- const r = await provider.send({
2097
+ const { result: r, modelUsed } = await sendWithModelFallback(provider, {
1996
2098
  messages,
1997
2099
  maxTokens,
1998
2100
  temperature: opts.temperature,
@@ -2005,7 +2107,10 @@ async function askOne(provider, messages, opts) {
2005
2107
  const cpuMs = Math.trunc((cpu.user + cpu.system) / 1e3);
2006
2108
  return {
2007
2109
  provider: provider.name,
2008
- 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,
2009
2114
  response: r.text,
2010
2115
  attempts: r.attempts,
2011
2116
  usage: r.usage,
@@ -2715,31 +2820,6 @@ function numberOrNull(v) {
2715
2820
  // src/tools/audit.ts
2716
2821
  import { performance as performance2 } from "perf_hooks";
2717
2822
 
2718
- // src/core/retarget.ts
2719
- function retargetProvider(p, newModel) {
2720
- if (p.model === newModel) return p;
2721
- return {
2722
- name: p.name,
2723
- model: newModel,
2724
- send: (args) => p.send({ ...args, modelOverride: newModel })
2725
- };
2726
- }
2727
- async function loadProviderWeights(storage, names) {
2728
- const out = {};
2729
- for (const raw of names) {
2730
- const name = raw.toLowerCase();
2731
- if (name in out) continue;
2732
- const row = await storage.getProviderStats(name);
2733
- if (!row) {
2734
- out[name] = 0.5;
2735
- continue;
2736
- }
2737
- const total = row.wins + row.losses + row.abstains;
2738
- out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
2739
- }
2740
- return out;
2741
- }
2742
-
2743
2823
  // src/core/co-reason.ts
2744
2824
  var CO_REASON_MODEL = "gpt-5.6";
2745
2825
  var CO_REASON_PROVIDER = "openai";
@@ -11490,7 +11570,7 @@ var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
11490
11570
  var DEFAULT_PACKAGE = "crosscheck-cli";
11491
11571
  var FETCH_TIMEOUT_MS = 3e3;
11492
11572
  function engineVersion() {
11493
- return true ? "0.2.14" : "0.0.0-dev";
11573
+ return true ? "0.2.16" : "0.0.0-dev";
11494
11574
  }
11495
11575
  function defaultUpdateCachePath() {
11496
11576
  const base = process.env["CROSSCHECK_DATA_DIR"] || path9.join(os.homedir() || os.tmpdir(), ".crosscheck");