crosscheck-mcp 0.2.14 → 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
  }
@@ -1254,7 +1261,7 @@ function httpFailureToProviderError(provider, status, bodyText, retryAfterS, mod
1254
1261
  return new ProviderError(
1255
1262
  "client",
1256
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}`,
1257
- { status }
1264
+ { status, modelAccessFailure: true }
1258
1265
  );
1259
1266
  }
1260
1267
  if (isCreditsFailure(status, bodyText)) {
@@ -1470,6 +1477,14 @@ function buildAnthropicRequest(opts) {
1470
1477
  if (system !== void 0) {
1471
1478
  body.system = system;
1472
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
+ }
1473
1488
  if (opts.jsonSchema) {
1474
1489
  body.tools = [{
1475
1490
  name: ANTHROPIC_STRUCTURED_TOOL_NAME,
@@ -1537,7 +1552,12 @@ function parseAnthropicResponse(opts) {
1537
1552
  purpose: opts.purpose
1538
1553
  };
1539
1554
  usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
1540
- 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";
1541
1561
  }
1542
1562
  function applyPricing(usage, pricing) {
1543
1563
  const { cost_usd, estimated } = calculateCost(
@@ -1555,49 +1575,64 @@ function applyPricing(usage, pricing) {
1555
1575
  estimated: usage.estimated || estimated
1556
1576
  };
1557
1577
  }
1578
+ var MAX_TOKENS_ESCALATIONS = 2;
1579
+ var ESCALATION_MULTIPLIER = 2;
1558
1580
  async function sendAnthropic(args) {
1559
- const { url, headers, body } = buildAnthropicRequest({
1560
- model: args.model,
1561
- apiKey: args.apiKey,
1562
- messages: args.messages,
1563
- maxTokens: args.maxTokens,
1564
- temperature: args.temperature,
1565
- ...args.jsonSchema ? { jsonSchema: args.jsonSchema } : {}
1566
- });
1567
1581
  const doFetch = args.fetchImpl ?? globalThis.fetch;
1568
- const init = {
1569
- method: "POST",
1570
- headers,
1571
- body: JSON.stringify(body)
1572
- };
1573
- if (args.signal) init.signal = args.signal;
1574
- const attemptOnce = async () => {
1575
- let respLike;
1576
- try {
1577
- respLike = await doFetch(url, init);
1578
- } catch (e) {
1579
- throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1580
- }
1581
- const status = respLike.status;
1582
- if (status >= 200 && status < 300) {
1583
- 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;
1584
1603
  try {
1585
- parsed = await respLike.json();
1604
+ respLike = await doFetch(url, init);
1586
1605
  } catch (e) {
1587
- throw new ProviderError("parse", `anthropic: response body not JSON: ${e.message}`);
1606
+ throw new ProviderError("network", `anthropic: fetch failed: ${e.message}`);
1588
1607
  }
1589
- const { text, usage } = parseAnthropicResponse({
1590
- resp: parsed,
1591
- model: args.model,
1592
- purpose: args.purpose ?? "worker"
1593
- });
1594
- 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;
1595
1632
  }
1596
- const bodyText = await respLike.text().catch(() => "");
1597
- throw httpFailureToProviderError("anthropic", status, bodyText, parseRetryAfter(respLike), args.model);
1598
- };
1599
- await acquireRateLimit("anthropic", args.signal ? { signal: args.signal } : void 0);
1600
- return sendWithRetry(attemptOnce, resolveRetryConfig(), args.signal);
1633
+ maxTokens *= ESCALATION_MULTIPLIER;
1634
+ }
1635
+ return { ...result, attempts: totalAttempts };
1601
1636
  }
1602
1637
 
1603
1638
  // src/providers/gemini.ts
@@ -1990,12 +2025,20 @@ function detectStalePins(env) {
1990
2025
  }
1991
2026
  return out;
1992
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
+ }
1993
2036
  function buildProviders(opts) {
1994
2037
  const out = {};
1995
2038
  const anthropicKey = opts.env["ANTHROPIC_API_KEY"];
1996
2039
  if (anthropicKey) {
1997
2040
  const model = opts.env["ANTHROPIC_MODEL"] ?? DEFAULT_MODELS.anthropic;
1998
- out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts);
2041
+ out["anthropic"] = makeAnthropicProvider(model, anthropicKey, opts, fallbackModelsFor(opts.env, "anthropic"));
1999
2042
  }
2000
2043
  const openAiCompatSpec = [
2001
2044
  { name: "openai", keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", defaultModel: DEFAULT_MODELS["openai"] },
@@ -2010,19 +2053,20 @@ function buildProviders(opts) {
2010
2053
  const apiKey = opts.env[s.keyEnv];
2011
2054
  if (!apiKey) continue;
2012
2055
  const model = opts.env[s.modelEnv] ?? s.defaultModel;
2013
- out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts);
2056
+ out[s.name] = makeOpenAICompatibleProvider(s.name, model, apiKey, opts, fallbackModelsFor(opts.env, s.name));
2014
2057
  }
2015
2058
  const geminiKey = opts.env["GEMINI_API_KEY"];
2016
2059
  if (geminiKey) {
2017
2060
  const model = opts.env["GEMINI_MODEL"] ?? DEFAULT_MODELS.gemini;
2018
- out["gemini"] = makeGeminiProvider(model, geminiKey, opts);
2061
+ out["gemini"] = makeGeminiProvider(model, geminiKey, opts, fallbackModelsFor(opts.env, "gemini"));
2019
2062
  }
2020
2063
  return out;
2021
2064
  }
2022
- function makeAnthropicProvider(model, apiKey, opts) {
2065
+ function makeAnthropicProvider(model, apiKey, opts, fallbackModels = []) {
2023
2066
  return {
2024
2067
  name: "anthropic",
2025
2068
  model,
2069
+ fallbackModels,
2026
2070
  send: async (args) => {
2027
2071
  const sendOpts = {
2028
2072
  ...args,
@@ -2035,11 +2079,12 @@ function makeAnthropicProvider(model, apiKey, opts) {
2035
2079
  }
2036
2080
  };
2037
2081
  }
2038
- function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
2082
+ function makeOpenAICompatibleProvider(name, model, apiKey, opts, fallbackModels = []) {
2039
2083
  const url = OPENAI_COMPAT_DEFAULT_URLS[name];
2040
2084
  return {
2041
2085
  name,
2042
2086
  model,
2087
+ fallbackModels,
2043
2088
  send: async (args) => {
2044
2089
  const sendOpts = {
2045
2090
  ...args,
@@ -2054,10 +2099,11 @@ function makeOpenAICompatibleProvider(name, model, apiKey, opts) {
2054
2099
  }
2055
2100
  };
2056
2101
  }
2057
- function makeGeminiProvider(model, apiKey, opts) {
2102
+ function makeGeminiProvider(model, apiKey, opts, fallbackModels = []) {
2058
2103
  return {
2059
2104
  name: "gemini",
2060
2105
  model,
2106
+ fallbackModels,
2061
2107
  send: async (args) => {
2062
2108
  const sendOpts = {
2063
2109
  ...args,
@@ -2208,7 +2254,7 @@ import { z } from "zod";
2208
2254
  // src/server-meta.ts
2209
2255
  init_esm_shims();
2210
2256
  var SERVER_NAME = "crosscheck-agent";
2211
- var SERVER_VERSION = true ? "0.2.14" : "0.0.0-dev";
2257
+ var SERVER_VERSION = true ? "0.2.15" : "0.0.0-dev";
2212
2258
 
2213
2259
  // src/tools/audit.ts
2214
2260
  init_esm_shims();
@@ -2870,6 +2916,55 @@ function operatorCeiling(purpose, provider) {
2870
2916
  return null;
2871
2917
  }
2872
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
+
2873
2968
  // src/core/structured.ts
2874
2969
  async function requestStructured(provider, baseMessages, schema, opts) {
2875
2970
  const maxRetries = opts.maxRetries ?? 1;
@@ -2948,7 +3043,7 @@ async function askOne(provider, messages, opts) {
2948
3043
  const ceiling = operatorCeiling(opts.purpose, provider.name);
2949
3044
  if (ceiling !== null && ceiling < maxTokens) maxTokens = ceiling;
2950
3045
  try {
2951
- const r = await provider.send({
3046
+ const { result: r, modelUsed } = await sendWithModelFallback(provider, {
2952
3047
  messages,
2953
3048
  maxTokens,
2954
3049
  temperature: opts.temperature,
@@ -2961,7 +3056,10 @@ async function askOne(provider, messages, opts) {
2961
3056
  const cpuMs = Math.trunc((cpu.user + cpu.system) / 1e3);
2962
3057
  return {
2963
3058
  provider: provider.name,
2964
- 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,
2965
3063
  response: r.text,
2966
3064
  attempts: r.attempts,
2967
3065
  usage: r.usage,
@@ -3682,32 +3780,6 @@ function numberOrNull(v) {
3682
3780
  // src/tools/audit.ts
3683
3781
  import { performance as performance2 } from "perf_hooks";
3684
3782
 
3685
- // src/core/retarget.ts
3686
- init_esm_shims();
3687
- function retargetProvider(p, newModel) {
3688
- if (p.model === newModel) return p;
3689
- return {
3690
- name: p.name,
3691
- model: newModel,
3692
- send: (args) => p.send({ ...args, modelOverride: newModel })
3693
- };
3694
- }
3695
- async function loadProviderWeights(storage, names) {
3696
- const out = {};
3697
- for (const raw of names) {
3698
- const name = raw.toLowerCase();
3699
- if (name in out) continue;
3700
- const row = await storage.getProviderStats(name);
3701
- if (!row) {
3702
- out[name] = 0.5;
3703
- continue;
3704
- }
3705
- const total = row.wins + row.losses + row.abstains;
3706
- out[name] = total <= 0 ? 0.5 : (row.wins + 0.5 * row.abstains) / total;
3707
- }
3708
- return out;
3709
- }
3710
-
3711
3783
  // src/core/co-reason.ts
3712
3784
  init_esm_shims();
3713
3785
  var CO_REASON_MODEL = "gpt-5.6";
@@ -12519,7 +12591,7 @@ var CHECK_INTERVAL_SECONDS = 3 * 24 * 60 * 60;
12519
12591
  var DEFAULT_PACKAGE = "crosscheck-cli";
12520
12592
  var FETCH_TIMEOUT_MS = 3e3;
12521
12593
  function engineVersion() {
12522
- return true ? "0.2.14" : "0.0.0-dev";
12594
+ return true ? "0.2.15" : "0.0.0-dev";
12523
12595
  }
12524
12596
  function defaultUpdateCachePath() {
12525
12597
  const base = process.env["CROSSCHECK_DATA_DIR"] || path10.join(os.homedir() || os.tmpdir(), ".crosscheck");