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.
@@ -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
  }
@@ -507,7 +514,7 @@ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, mod
507
514
  return new ProviderError(
508
515
  "client",
509
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}`,
510
- { status }
517
+ { status, modelAccessFailure: true }
511
518
  );
512
519
  }
513
520
  if (isCreditsFailure(status, bodyText)) {
@@ -714,6 +721,14 @@ function buildAnthropicRequest(opts) {
714
721
  if (system !== void 0) {
715
722
  body.system = system;
716
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
+ }
717
732
  if (opts.jsonSchema) {
718
733
  body.tools = [{
719
734
  name: ANTHROPIC_STRUCTURED_TOOL_NAME,
@@ -781,7 +796,12 @@ function parseAnthropicResponse(opts) {
781
796
  purpose: opts.purpose
782
797
  };
783
798
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
784
- 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";
785
805
  }
786
806
  function applyPricing(usage, pricing) {
787
807
  const { cost_usd, estimated } = calculateCost(
@@ -799,49 +819,64 @@ function applyPricing(usage, pricing) {
799
819
  estimated: usage.estimated || estimated
800
820
  };
801
821
  }
822
+ var MAX_TOKENS_ESCALATIONS = 2;
823
+ var ESCALATION_MULTIPLIER = 2;
802
824
  async function sendAnthropic(args) {
803
- const { url, headers, body } = buildAnthropicRequest({
804
- model: args.model,
805
- apiKey: args.apiKey,
806
- messages: args.messages,
807
- maxTokens: args.maxTokens,
808
- temperature: args.temperature,
809
- ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
810
- });
811
825
  const doFetch = args.fetchImpl ?? globalThis.fetch;
812
- const init = {
813
- method: "POST",
814
- headers,
815
- body: JSON.stringify(body)
816
- };
817
- if (args.signal) init.signal = args.signal;
818
- const attemptOnce = async () => {
819
- let respLike;
820
- try {
821
- respLike = await doFetch(url, init);
822
- } catch (e) {
823
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
824
- }
825
- const status = respLike.status;
826
- if (status >= 200 && status < 300) {
827
- 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;
828
847
  try {
829
- parsed = await respLike.json();
848
+ respLike = await doFetch(url, init);
830
849
  } catch (e) {
831
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
850
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
832
851
  }
833
- const { text, usage } = parseAnthropicResponse({
834
- resp: parsed,
835
- model: args.model,
836
- purpose: args.purpose ?? "worker"
837
- });
838
- 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;
839
876
  }
840
- const bodyText = await respLike.text().catch(() => "");
841
- throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
842
- };
843
- await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
844
- return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
877
+ maxTokens *= ESCALATION_MULTIPLIER;
878
+ }
879
+ return { ...result, attempts: totalAttempts };
845
880
  }
846
881
 
847
882
  // src/providers/gemini.ts
@@ -1210,12 +1245,31 @@ var DEFAULT_MODELS = {
1210
1245
  kimi: "kimi-k3",
1211
1246
  qwen: "qwen3.8-max"
1212
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
+ }
1213
1267
  function buildProviders(opts) {
1214
1268
  const out = {};
1215
1269
  const anthropicKey = opts.env["ANTHROPIC_API_KEY"];
1216
1270
  if (anthropicKey) {
1217
1271
  const model = opts.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODELS.anthropic;
1218
- out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts);
1272
+ out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts, fallbackModelsFor(opts.env, "anthropic"));
1219
1273
  }
1220
1274
  const openAiCompatSpec = [
1221
1275
  { name: "openai", keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", defaultModel: DEFAULT_MODELS["openai"] },
@@ -1230,19 +1284,20 @@ function buildProviders(opts) {
1230
1284
  const apiKey = opts.env[s.keyEnv];
1231
1285
  if (!apiKey) continue;
1232
1286
  const model = opts.env[s.modelEnv] ?? s.defaultModel;
1233
- out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts);
1287
+ out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts, fallbackModelsFor(opts.env, s.name));
1234
1288
  }
1235
1289
  const geminiKey = opts.env["GEMINI_API_KEY"];
1236
1290
  if (geminiKey) {
1237
1291
  const model = opts.env["GEMINI_MODEL"] ?? DEFAULT_MODELS.gemini;
1238
- out["gemini"] = makeGeminiProvider(model, geminiKey, opts);
1292
+ out["gemini"] = makeGeminiProvider(model, geminiKey, opts, fallbackModelsFor(opts.env, "gemini"));
1239
1293
  }
1240
1294
  return out;
1241
1295
  }
1242
- function makeAnthropicProvider(model, apiKey, opts) {
1296
+ function makeAnthropicProvider(model, apiKey, opts, fallbackModels = []) {
1243
1297
  return {
1244
1298
  name: "anthropic",
1245
1299
  model,
1300
+ fallbackModels,
1246
1301
  send: async (args) => {
1247
1302
  const sendOpts = {
1248
1303
  ...args,
@@ -1255,11 +1310,12 @@ function makeAnthropicProvider(model, apiKey, opts) {
1255
1310
  }
1256
1311
  };
1257
1312
  }
1258
- function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1313
+ function makeOpenAICompatibleProvider(name, model, apiKey, opts, fallbackModels = []) {
1259
1314
  const url = OPENAI_COMPAT_DEFAULT_URLS[name];
1260
1315
  return {
1261
1316
  name,
1262
1317
  model,
1318
+ fallbackModels,
1263
1319
  send: async (args) => {
1264
1320
  const sendOpts = {
1265
1321
  ...args,
@@ -1274,10 +1330,11 @@ function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
1274
1330
  }
1275
1331
  };
1276
1332
  }
1277
- function makeGeminiProvider(model, apiKey, opts) {
1333
+ function makeGeminiProvider(model, apiKey, opts, fallbackModels = []) {
1278
1334
  return {
1279
1335
  name: "gemini",
1280
1336
  model,
1337
+ fallbackModels,
1281
1338
  send: async (args) => {
1282
1339
  const sendOpts = {
1283
1340
  ...args,
@@ -1296,7 +1353,7 @@ var import_zod = require("zod");
1296
1353
 
1297
1354
  // src/server-meta.ts
1298
1355
  var SERVER_NAME = "crosscheck-agent";
1299
- var SERVER_VERSION = true ? "0.2.14" : "0.0.0-dev";
1356
+ var SERVER_VERSION = true ? "0.2.16" : "0.0.0-dev";
1300
1357
 
1301
1358
  // src/tools/audit.ts
1302
1359
  var import_node_fs4 = require("fs");
@@ -1947,6 +2004,51 @@ function operatorCeiling(purpose, provider) {
1947
2004
  return null;
1948
2005
  }
1949
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
+
1950
2052
  // src/core/structured.ts
1951
2053
  async function requestStructured(provider, baseMessages, schema, opts) {
1952
2054
  const maxRetries = opts.maxRetries ?? 1;
@@ -2025,7 +2127,7 @@ async function askOne(provider, messages, opts) {
2025
2127
  const ceiling = operatorCeiling(opts.purpose, provider.name);
2026
2128
  if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
2027
2129
  try {
2028
- const r = await provider.send({
2130
+ const { result: r, modelUsed } = await sendWithModelFallback(provider, {
2029
2131
  messages,
2030
2132
  maxTokens,
2031
2133
  temperature: opts.temperature,
@@ -2038,7 +2140,10 @@ async function askOne(provider, messages, opts) {
2038
2140
  const cpuMs = Math.trunc((cpu.user + cpu.system) / 1e3);
2039
2141
  return {
2040
2142
  provider: provider.name,
2041
- 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,
2042
2147
  response: r.text,
2043
2148
  attempts: r.attempts,
2044
2149
  usage: r.usage,
@@ -2748,31 +2853,6 @@ function numberOrNull(v) {
2748
2853
  // src/tools/audit.ts
2749
2854
  var import_node_perf_hooks = require("perf_hooks");
2750
2855
 
2751
- // src/core/retarget.ts
2752
- function retargetProvider(p, newModel) {
2753
- if (p.model === newModel) return p;
2754
- return {
2755
- name: p.name,
2756
- model: newModel,
2757
- send: (args) => p.send({ ...args, modelOverride: newModel })
2758
- };
2759
- }
2760
- async function loadProviderWeights(storage, names) {
2761
- const out = {};
2762
- for (const raw of names) {
2763
- const name = raw.toLowerCase();
2764
- if (name in out) continue;
2765
- const row = await storage.getProviderStats(name);
2766
- if (!row) {
2767
- out[name] = 0.5;
2768
- continue;
2769
- }
2770
- const total = row.wins + row.losses + row.abstains;
2771
- out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
2772
- }
2773
- return out;
2774
- }
2775
-
2776
2856
  // src/core/co-reason.ts
2777
2857
  var CO_REASON_MODEL = "gpt-5.6";
2778
2858
  var CO_REASON_PROVIDER = "openai";
@@ -11506,7 +11586,7 @@ var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
11506
11586
  var DEFAULT_PACKAGE = "crosscheck-cli";
11507
11587
  var FETCH_TIMEOUT_MS = 3e3;
11508
11588
  function engineVersion() {
11509
- return true ? "0.2.14" : "0.0.0-dev";
11589
+ return true ? "0.2.16" : "0.0.0-dev";
11510
11590
  }
11511
11591
  function defaultUpdateCachePath() {
11512
11592
  const base = process.env["CROSSCHECK_DATA_DIR"] || import_node_path12.default.join(import_node_os2.default.homedir() || import_node_os2.default.tmpdir(), ".crosscheck");