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.
@@ -436,6 +436,15 @@ interface Provider {
436
436
  readonly name: string;
437
437
  /** Default model for this provider (env-resolved at factory time). */
438
438
  readonly model: string;
439
+ /** Ordered fallback models — "greenlight" candidates to try, in order,
440
+ * when `model` comes back with a model-access failure (the account/
441
+ * project doesn't have it enabled) rather than any other kind of
442
+ * error. Env-resolved at factory time from `<PROVIDER>_MODEL_
443
+ * FALLBACKS`, or set from a user's/org's configured chain. Empty for
444
+ * most providers — a bad key or rate limit isn't fixed by switching
445
+ * models, so this is only ever consulted for that one failure mode
446
+ * (see core/model-fallback.ts). */
447
+ readonly fallbackModels?: readonly string[];
439
448
  /** Make a request and return the parsed result. Throws `ProviderError`
440
449
  * on classified failures (auth, rate_limit, timeout, server, parse,
441
450
  * network, client). */
@@ -436,6 +436,15 @@ interface Provider {
436
436
  readonly name: string;
437
437
  /** Default model for this provider (env-resolved at factory time). */
438
438
  readonly model: string;
439
+ /** Ordered fallback models — "greenlight" candidates to try, in order,
440
+ * when `model` comes back with a model-access failure (the account/
441
+ * project doesn't have it enabled) rather than any other kind of
442
+ * error. Env-resolved at factory time from `<PROVIDER>_MODEL_
443
+ * FALLBACKS`, or set from a user's/org's configured chain. Empty for
444
+ * most providers — a bad key or rate limit isn't fixed by switching
445
+ * models, so this is only ever consulted for that one failure mode
446
+ * (see core/model-fallback.ts). */
447
+ readonly fallbackModels?: readonly string[];
439
448
  /** Make a request and return the parsed result. Throws `ProviderError`
440
449
  * on classified failures (auth, rate_limit, timeout, server, parse,
441
450
  * network, client). */
@@ -1218,11 +1218,18 @@ var ProviderError = class extends Error {
1218
1218
  status;
1219
1219
  transient;
1220
1220
  retryAfterS;
1221
+ /** True specifically for "the key is fine but this model isn't
1222
+ * available to this account/project" (see http-errors.ts's
1223
+ * isModelAccessFailure) — the one failure a model fallback chain can
1224
+ * actually fix. Distinct from `transient`: this is never worth
1225
+ * retrying the SAME model, but IS worth trying a different one. */
1226
+ modelAccessFailure;
1221
1227
  constructor(kind, message, opts) {
1222
1228
  super(message);
1223
1229
  this.kind = kind;
1224
1230
  this.status = opts?.status;
1225
1231
  this.transient = opts?.transient ?? defaultTransient(kind);
1232
+ this.modelAccessFailure = opts?.modelAccessFailure ?? false;
1226
1233
  if (opts?.retryAfterS !== void 0) {
1227
1234
  this.retryAfterS = opts.retryAfterS;
1228
1235
  }
@@ -1242,8 +1249,21 @@ function isCreditsFailure(status, bodyText) {
1242
1249
  }
1243
1250
  return false;
1244
1251
  }
1245
- function httpFailureToProviderError(provider, status, bodyText, retryAfterS) {
1252
+ 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;
1253
+ function isModelAccessFailure(status, bodyText) {
1254
+ if (status < 400 || status >= 500) return false;
1255
+ return MODEL_ACCESS_RE.test(bodyText);
1256
+ }
1257
+ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, model) {
1246
1258
  const detail = bodyText.slice(0, 512);
1259
+ if (isModelAccessFailure(status, bodyText)) {
1260
+ const modelRef = model ? `"${model}"` : "the configured model";
1261
+ return new ProviderError(
1262
+ "client",
1263
+ `${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}`,
1264
+ { status, modelAccessFailure: true }
1265
+ );
1266
+ }
1247
1267
  if (isCreditsFailure(status, bodyText)) {
1248
1268
  return new ProviderError(
1249
1269
  "auth",
@@ -1457,6 +1477,14 @@ function buildAnthropicRequest(opts) {
1457
1477
  if (system !== void 0) {
1458
1478
  body.system = system;
1459
1479
  }
1480
+ if (isReasoningModel("anthropic", opts.model)) {
1481
+ if (opts.model.toLowerCase().startsWith("claude-fable-5")) {
1482
+ body.thinking = { type: "adaptive" };
1483
+ body.output_config = { effort: "low" };
1484
+ } else {
1485
+ body.thinking = { type: "disabled" };
1486
+ }
1487
+ }
1460
1488
  if (opts.jsonSchema) {
1461
1489
  body.tools = [{
1462
1490
  name: ANTHROPIC_STRUCTURED_TOOL_NAME,
@@ -1524,7 +1552,12 @@ function parseAnthropicResponse(opts) {
1524
1552
  purpose: opts.purpose
1525
1553
  };
1526
1554
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
1527
- return { text, usage };
1555
+ const stopReasonRaw = r["stop_reason"];
1556
+ const stopReason = typeof stopReasonRaw === "string" ? stopReasonRaw : void 0;
1557
+ return { text, usage, ...stopReason !== void 0 ? { stopReason } : {} };
1558
+ }
1559
+ function isEmptyDueToMaxTokens(text, stopReason) {
1560
+ return text.trim() === "" && stopReason === "max_tokens";
1528
1561
  }
1529
1562
  function applyPricing(usage, pricing) {
1530
1563
  const { cost_usd, estimated } = calculateCost(
@@ -1542,49 +1575,64 @@ function applyPricing(usage, pricing) {
1542
1575
  estimated: usage.estimated || estimated
1543
1576
  };
1544
1577
  }
1578
+ var MAX_TOKENS_ESCALATIONS = 2;
1579
+ var ESCALATION_MULTIPLIER = 2;
1545
1580
  async function sendAnthropic(args) {
1546
- const { url, headers, body } = buildAnthropicRequest({
1547
- model: args.model,
1548
- apiKey: args.apiKey,
1549
- messages: args.messages,
1550
- maxTokens: args.maxTokens,
1551
- temperature: args.temperature,
1552
- ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1553
- });
1554
1581
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1555
- const init = {
1556
- method: "POST",
1557
- headers,
1558
- body: JSON.stringify(body)
1559
- };
1560
- if (args.signal) init.signal = args.signal;
1561
- const attemptOnce = async () => {
1562
- let respLike;
1563
- try {
1564
- respLike = await doFetch(url, init);
1565
- } catch (e) {
1566
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1567
- }
1568
- const status = respLike.status;
1569
- if (status >= 200 && status < 300) {
1570
- let parsed;
1582
+ let maxTokens = args.maxTokens;
1583
+ let result = null;
1584
+ let totalAttempts = 0;
1585
+ for (let escalation = 0; escalation <= MAX_TOKENS_ESCALATIONS; escalation += 1) {
1586
+ const { url, headers, body } = buildAnthropicRequest({
1587
+ model: args.model,
1588
+ apiKey: args.apiKey,
1589
+ messages: args.messages,
1590
+ maxTokens,
1591
+ temperature: args.temperature,
1592
+ ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1593
+ });
1594
+ const init = {
1595
+ method: "POST",
1596
+ headers,
1597
+ body: JSON.stringify(body)
1598
+ };
1599
+ if (args.signal) init.signal = args.signal;
1600
+ let stopReason;
1601
+ const attemptOnce = async () => {
1602
+ let respLike;
1571
1603
  try {
1572
- parsed = await respLike.json();
1604
+ respLike = await doFetch(url, init);
1573
1605
  } catch (e) {
1574
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
1606
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1575
1607
  }
1576
- const { text, usage } = parseAnthropicResponse({
1577
- resp: parsed,
1578
- model: args.model,
1579
- purpose: args.purpose ?? "worker"
1580
- });
1581
- return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
1608
+ const status = respLike.status;
1609
+ if (status >= 200 && status < 300) {
1610
+ let parsed;
1611
+ try {
1612
+ parsed = await respLike.json();
1613
+ } catch (e) {
1614
+ throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
1615
+ }
1616
+ const { text, usage, stopReason: sr } = parseAnthropicResponse({
1617
+ resp: parsed,
1618
+ model: args.model,
1619
+ purpose: args.purpose ?? "worker"
1620
+ });
1621
+ stopReason = sr;
1622
+ return { text, attempts: 1, usage: applyPricing(usage, args.pricing) };
1623
+ }
1624
+ const bodyText = await respLike.text().catch(() => "");
1625
+ throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
1626
+ };
1627
+ await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
1628
+ result = await sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1629
+ totalAttempts += result.attempts;
1630
+ if (escalation >= MAX_TOKENS_ESCALATIONS || !isEmptyDueToMaxTokens(result.text, stopReason)) {
1631
+ break;
1582
1632
  }
1583
- const bodyText = await respLike.text().catch(() => "");
1584
- throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike));
1585
- };
1586
- await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
1587
- return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1633
+ maxTokens *= ESCALATION_MULTIPLIER;
1634
+ }
1635
+ return { ...result, attempts: totalAttempts };
1588
1636
  }
1589
1637
 
1590
1638
  // src/providers/gemini.ts
@@ -1760,7 +1808,7 @@ async function sendGemini(args) {
1760
1808
  return { text, attempts: 1, usage: applyPricing2(usage, args.pricing) };
1761
1809
  }
1762
1810
  const bodyText = await respLike.text().catch(() => "");
1763
- throw httpFailureToProviderError("gemini", status, bodyText, parseRetryAfter(respLike));
1811
+ throw httpFailureToProviderError("gemini", status, bodyText, parseRetryAfter(respLike), args.model);
1764
1812
  };
1765
1813
  await acquireRateLimit("gemini", args.signal ? { signal: args.signal } : void 0);
1766
1814
  return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
@@ -1934,7 +1982,7 @@ async function sendOpenAICompatible(args) {
1934
1982
  return { text, attempts: 1, usage: applyPricing3(usage, args.pricing) };
1935
1983
  }
1936
1984
  const bodyText = await respLike.text().catch(() => "");
1937
- throw httpFailureToProviderError(args.provider, status, bodyText, parseRetryAfter(respLike));
1985
+ throw httpFailureToProviderError(args.provider, status, bodyText, parseRetryAfter(respLike), args.model);
1938
1986
  };
1939
1987
  await acquireRateLimit(args.provider, args.signal ? { signal: args.signal } : void 0);
1940
1988
  return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
@@ -1943,7 +1991,10 @@ async function sendOpenAICompatible(args) {
1943
1991
  // src/providers/registry.ts
1944
1992
  var DEFAULT_MODELS = {
1945
1993
  anthropic: "claude-opus-5",
1946
- openai: "gpt-5.5",
1994
+ // gpt-5.5 is not enabled on every OpenAI account/project (some are still
1995
+ // capped at gpt-5) — gpt-5 is the safer, more broadly available default
1996
+ // until a user/org explicitly opts into 5.5 via `crosscheck models set`.
1997
+ openai: "gpt-5",
1947
1998
  xai: "grok-4-latest",
1948
1999
  mistral: "mistral-large-latest",
1949
2000
  groq: "llama-3.3-70b-versatile",
@@ -1974,12 +2025,20 @@ function detectStalePins(env) {
1974
2025
  }
1975
2026
  return out;
1976
2027
  }
2028
+ function parseFallbackModels(raw) {
2029
+ if (!raw) return [];
2030
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
2031
+ }
2032
+ function fallbackModelsFor(env, provider) {
2033
+ const envVar = MODEL_ENV_VARS[provider];
2034
+ return envVar ? parseFallbackModels(env[`${envVar}_FALLBACKS`]) : [];
2035
+ }
1977
2036
  function buildProviders(opts) {
1978
2037
  const out = {};
1979
2038
  const anthropicKey = opts.env["ANTHROPIC_API_KEY"];
1980
2039
  if (anthropicKey) {
1981
2040
  const model = opts.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODELS.anthropic;
1982
- out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts);
2041
+ out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts, fallbackModelsFor(opts.env, "anthropic"));
1983
2042
  }
1984
2043
  const openAiCompatSpec = [
1985
2044
  { name: "openai", keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", defaultModel: DEFAULT_MODELS["openai"] },
@@ -1994,19 +2053,20 @@ function buildProviders(opts) {
1994
2053
  const apiKey = opts.env[s.keyEnv];
1995
2054
  if (!apiKey) continue;
1996
2055
  const model = opts.env[s.modelEnv] ?? s.defaultModel;
1997
- out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts);
2056
+ out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts, fallbackModelsFor(opts.env, s.name));
1998
2057
  }
1999
2058
  const geminiKey = opts.env["GEMINI_API_KEY"];
2000
2059
  if (geminiKey) {
2001
2060
  const model = opts.env["GEMINI_MODEL"] ?? DEFAULT_MODELS.gemini;
2002
- out["gemini"] = makeGeminiProvider(model, geminiKey, opts);
2061
+ out["gemini"] = makeGeminiProvider(model, geminiKey, opts, fallbackModelsFor(opts.env, "gemini"));
2003
2062
  }
2004
2063
  return out;
2005
2064
  }
2006
- function makeAnthropicProvider(model, apiKey, opts) {
2065
+ function makeAnthropicProvider(model, apiKey, opts, fallbackModels = []) {
2007
2066
  return {
2008
2067
  name: "anthropic",
2009
2068
  model,
2069
+ fallbackModels,
2010
2070
  send: async (args) => {
2011
2071
  const sendOpts = {
2012
2072
  ...args,
@@ -2019,11 +2079,12 @@ function makeAnthropicProvider(model, apiKey, opts) {
2019
2079
  }
2020
2080
  };
2021
2081
  }
2022
- function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
2082
+ function makeOpenAICompatibleProvider(name, model, apiKey, opts, fallbackModels = []) {
2023
2083
  const url = OPENAI_COMPAT_DEFAULT_URLS[name];
2024
2084
  return {
2025
2085
  name,
2026
2086
  model,
2087
+ fallbackModels,
2027
2088
  send: async (args) => {
2028
2089
  const sendOpts = {
2029
2090
  ...args,
@@ -2038,10 +2099,11 @@ function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
2038
2099
  }
2039
2100
  };
2040
2101
  }
2041
- function makeGeminiProvider(model, apiKey, opts) {
2102
+ function makeGeminiProvider(model, apiKey, opts, fallbackModels = []) {
2042
2103
  return {
2043
2104
  name: "gemini",
2044
2105
  model,
2106
+ fallbackModels,
2045
2107
  send: async (args) => {
2046
2108
  const sendOpts = {
2047
2109
  ...args,
@@ -2192,7 +2254,7 @@ import { z } from "zod";
2192
2254
  // src/server-meta.ts
2193
2255
  init_esm_shims();
2194
2256
  var SERVER_NAME = "crosscheck-agent";
2195
- var SERVER_VERSION = true ? "0.2.13" : "0.0.0-dev";
2257
+ var SERVER_VERSION = true ? "0.2.15" : "0.0.0-dev";
2196
2258
 
2197
2259
  // src/tools/audit.ts
2198
2260
  init_esm_shims();
@@ -2854,6 +2916,55 @@ function operatorCeiling(purpose, provider) {
2854
2916
  return null;
2855
2917
  }
2856
2918
 
2919
+ // src/core/model-fallback.ts
2920
+ init_esm_shims();
2921
+
2922
+ // src/core/retarget.ts
2923
+ init_esm_shims();
2924
+ function retargetProvider(p, newModel) {
2925
+ if (p.model === newModel) return p;
2926
+ return {
2927
+ name: p.name,
2928
+ model: newModel,
2929
+ send: (args) => p.send({ ...args, modelOverride: newModel })
2930
+ };
2931
+ }
2932
+ async function loadProviderWeights(storage, names) {
2933
+ const out = {};
2934
+ for (const raw of names) {
2935
+ const name = raw.toLowerCase();
2936
+ if (name in out) continue;
2937
+ const row = await storage.getProviderStats(name);
2938
+ if (!row) {
2939
+ out[name] = 0.5;
2940
+ continue;
2941
+ }
2942
+ const total = row.wins + row.losses + row.abstains;
2943
+ out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
2944
+ }
2945
+ return out;
2946
+ }
2947
+
2948
+ // src/core/model-fallback.ts
2949
+ async function sendWithModelFallback(provider, args) {
2950
+ const chain = [provider.model, ...provider.fallbackModels ?? []];
2951
+ let lastErr;
2952
+ for (let i = 0; i < chain.length; i += 1) {
2953
+ const candidate = chain[i];
2954
+ const p = i === 0 ? provider : retargetProvider(provider, candidate);
2955
+ try {
2956
+ const result = await p.send(args);
2957
+ return { result, modelUsed: candidate };
2958
+ } catch (e) {
2959
+ lastErr = e;
2960
+ const isModelAccessFailure2 = e instanceof ProviderError && e.modelAccessFailure;
2961
+ const hasMoreCandidates = i < chain.length - 1;
2962
+ if (!isModelAccessFailure2 || !hasMoreCandidates) throw e;
2963
+ }
2964
+ }
2965
+ throw lastErr;
2966
+ }
2967
+
2857
2968
  // src/core/structured.ts
2858
2969
  async function requestStructured(provider, baseMessages, schema, opts) {
2859
2970
  const maxRetries = opts.maxRetries ?? 1;
@@ -2932,7 +3043,7 @@ async function askOne(provider, messages, opts) {
2932
3043
  const ceiling = operatorCeiling(opts.purpose, provider.name);
2933
3044
  if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
2934
3045
  try {
2935
- const r = await provider.send({
3046
+ const { result: r, modelUsed } = await sendWithModelFallback(provider, {
2936
3047
  messages,
2937
3048
  maxTokens,
2938
3049
  temperature: opts.temperature,
@@ -2945,7 +3056,10 @@ async function askOne(provider, messages, opts) {
2945
3056
  const cpuMs = Math.trunc((cpu.user + cpu.system) / 1e3);
2946
3057
  return {
2947
3058
  provider: provider.name,
2948
- model: provider.model,
3059
+ // modelUsed reflects whichever model actually answered — the
3060
+ // primary, or a fallback if one was needed. Equivalent to the old
3061
+ // `provider.model` whenever no fallback occurred.
3062
+ model: modelUsed,
2949
3063
  response: r.text,
2950
3064
  attempts: r.attempts,
2951
3065
  usage: r.usage,
@@ -3666,32 +3780,6 @@ function numberOrNull(v) {
3666
3780
  // src/tools/audit.ts
3667
3781
  import { performance as performance2 } from "perf_hooks";
3668
3782
 
3669
- // src/core/retarget.ts
3670
- init_esm_shims();
3671
- function retargetProvider(p, newModel) {
3672
- if (p.model === newModel) return p;
3673
- return {
3674
- name: p.name,
3675
- model: newModel,
3676
- send: (args) => p.send({ ...args, modelOverride: newModel })
3677
- };
3678
- }
3679
- async function loadProviderWeights(storage, names) {
3680
- const out = {};
3681
- for (const raw of names) {
3682
- const name = raw.toLowerCase();
3683
- if (name in out) continue;
3684
- const row = await storage.getProviderStats(name);
3685
- if (!row) {
3686
- out[name] = 0.5;
3687
- continue;
3688
- }
3689
- const total = row.wins + row.losses + row.abstains;
3690
- out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
3691
- }
3692
- return out;
3693
- }
3694
-
3695
3783
  // src/core/co-reason.ts
3696
3784
  init_esm_shims();
3697
3785
  var CO_REASON_MODEL = "gpt-5.6";
@@ -12503,7 +12591,7 @@ var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
12503
12591
  var DEFAULT_PACKAGE = "crosscheck-cli";
12504
12592
  var FETCH_TIMEOUT_MS = 3e3;
12505
12593
  function engineVersion() {
12506
- return true ? "0.2.13" : "0.0.0-dev";
12594
+ return true ? "0.2.15" : "0.0.0-dev";
12507
12595
  }
12508
12596
  function defaultUpdateCachePath() {
12509
12597
  const base = process.env["CROSSCHECK_DATA_DIR"] || path10.join(os.homedir() || os.tmpdir(), ".crosscheck");