@inferall/cli 0.2.1 → 0.2.3

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.
package/README.md CHANGED
@@ -35,6 +35,12 @@ The wizard:
35
35
 
36
36
  ## Commands
37
37
 
38
+ For a project with no existing AI client, `init` writes a first-request example:
39
+ `node inferall-hello.mjs` or `python3 inferall_hello.py`. Both use built-in
40
+ libraries, read your exported key or the `.env` beside the example, and select
41
+ the free model explicitly. A refusal or empty response exits nonzero and prints
42
+ an explanation to stderr. Existing example files are left intact.
43
+
38
44
  - `init`: the wizard above (default).
39
45
  - `doctor`: health-check an existing setup. Key present and valid, `.env`
40
46
  gitignore hygiene, whether every client call site points at the gateway, and
@@ -84,3 +90,9 @@ apply, and leaves your code otherwise untouched, with no reformatting and no
84
90
  renames.
85
91
 
86
92
  Zero runtime dependencies. Node 18+.
93
+
94
+ A live test call succeeds only when it returns usable answer text. `init` and
95
+ `doctor` exit nonzero for refused, malformed, empty or interrupted test calls.
96
+ This includes HTTP 402: any file changes already reported by `init` remain,
97
+ but the call has not been verified. Follow the gateway explanation before
98
+ retrying. `chat` sends gateway failure diagnostics to stderr and answer text to stdout.
@@ -1 +1 @@
1
- {"sha":"2a3e95e","ref":"main","offMain":null}
1
+ {"sha":"8abc047","ref":"main","offMain":null}
package/bin/inferall.js CHANGED
@@ -981,7 +981,8 @@ async function apiChat(key, messages, model = FREE_MODEL, maxTokens = 512) {
981
981
  //
982
982
  // Reading the body as text first keeps the distinction, and keeps a snippet
983
983
  // so we can quote what actually arrived instead of an empty object.
984
- const raw = await res.text().catch(() => "");
984
+ let readFailed = false;
985
+ const raw = await res.text().catch(() => { readFailed = true; return ""; });
985
986
  let body = {};
986
987
  let parseFailed = false;
987
988
  if (raw) {
@@ -995,12 +996,100 @@ async function apiChat(key, messages, model = FREE_MODEL, maxTokens = 512) {
995
996
  status: res.status,
996
997
  body,
997
998
  parseFailed,
999
+ readFailed,
998
1000
  raw,
999
1001
  contentType: res.headers.get("content-type") || "",
1000
1002
  retryAfter: res.headers.get("retry-after"),
1003
+ trialUsage: res.headers.get("x-inferall-trial-usage") || "",
1004
+ model,
1005
+ maxTokens,
1001
1006
  };
1002
1007
  }
1003
1008
 
1009
+ /**
1010
+ * Did this chat call produce an ANSWER? One verdict shared by `chat`, `doctor`
1011
+ * and `init`, so no command can call a call successful from the status line.
1012
+ *
1013
+ * Success is HTTP 200, a parseable body, and a non-blank string at
1014
+ * choices[0].message.content. Everything else is a failure with a kind:
1015
+ * unparseable body is not JSON (HTML page, truncated JSON), any status
1016
+ * http parsed body, status other than 200
1017
+ * length finish_reason "length" and no usable answer text
1018
+ * no_answer 200 and JSON, but no usable answer (empty, blank, wrong shape)
1019
+ */
1020
+ export function classifyChatResult(reply) {
1021
+ const status = reply?.status;
1022
+ if (reply?.readFailed) return { ok: false, kind: "incomplete", status };
1023
+ if (reply?.parseFailed) return { ok: false, kind: "unparseable", status };
1024
+ if (status !== 200) return { ok: false, kind: "http", status };
1025
+ const choice = reply.body?.choices?.[0];
1026
+ const text = choice?.message?.content;
1027
+ if (typeof text === "string" && text.trim()) return { ok: true, text, status };
1028
+ if (choice?.finish_reason === "length") return { ok: false, kind: "length", status };
1029
+ let reason;
1030
+ if (!(reply.raw || "").trim()) reason = "the response body was empty";
1031
+ else if (!Array.isArray(reply.body?.choices) || !choice) reason = "the response had no choices";
1032
+ else if (text === undefined || text === null) reason = "the message had no content";
1033
+ else if (typeof text !== "string") reason = "the message content was not text";
1034
+ else reason = "the message content was blank";
1035
+ return { ok: false, kind: "no_answer", status, reason };
1036
+ }
1037
+
1038
+ // One printable line from a provider-supplied string: no newlines, bounded, and
1039
+ // never the caller's own key if a gateway echoes it back.
1040
+ function safeProviderText(s, key) {
1041
+ let t = String(s).replace(/\s+/g, " ").trim();
1042
+ if (key) t = t.split(key).join("[key]");
1043
+ return t.slice(0, 300);
1044
+ }
1045
+
1046
+ /**
1047
+ * Plain lines explaining a failed classifyChatResult. Never includes the raw
1048
+ * body, reasoning text or the key. `surface` is "chat" or "probe"; only the
1049
+ * length guidance differs, because init and doctor cannot take --max-tokens.
1050
+ */
1051
+ export function chatFailureLines(result, reply, key, surface = "chat") {
1052
+ const status = reply.status;
1053
+ if (result.kind === "incomplete") {
1054
+ return [`HTTP ${status}: the response could not be read completely. This call did not succeed.`];
1055
+ }
1056
+ if (result.kind === "unparseable") {
1057
+ const isHtml = /html/i.test(reply.contentType || "") || /^\s*(?:<!doctype html|<html)/i.test(reply.raw || "");
1058
+ const summary = isHtml ? nonJsonSummary(reply) : "The gateway returned a response that was not valid JSON.";
1059
+ const lines = [safeProviderText(summary, key), `HTTP ${status}. Nothing usable came back, so this call did not succeed.`];
1060
+ if (status >= 500) lines.push("That is an infrastructure error, not a problem with your key. Retry shortly.");
1061
+ return lines;
1062
+ }
1063
+ if (result.kind === "length") {
1064
+ const limit = reply.maxTokens ? ` (max_tokens=${reply.maxTokens})` : "";
1065
+ return [
1066
+ `The model used its whole token budget${limit} before returning any answer text.`,
1067
+ surface === "chat"
1068
+ ? "Retry with a larger --max-tokens, or pick another model with --model."
1069
+ : 'Use npx @inferall/cli chat with --max-tokens to test a larger limit, or --model to choose another model.',
1070
+ ];
1071
+ }
1072
+ if (result.kind === "no_answer") {
1073
+ return [`HTTP 200, but the gateway returned no answer in it (${result.reason}).`];
1074
+ }
1075
+ const err = reply.body?.error;
1076
+ const msg = (typeof err === "string" ? err : typeof err?.message === "string" ? err.message : typeof reply.body?.message === "string" ? reply.body.message : "");
1077
+ const code = typeof err?.code === "string" ? err.code : "";
1078
+ let detail = safeProviderText(msg || code || "the response carried no error message", key);
1079
+ const safeCode = safeProviderText(code, key);
1080
+ if (msg && safeCode && !detail.includes(safeCode)) detail += ` (${safeCode})`;
1081
+ const lines = [`HTTP ${status}: ${detail}`];
1082
+ if (status === 401) lines.push(
1083
+ "The gateway rejected the credential sent with this call.",
1084
+ `Re-running with the same key will not resolve an unchanged credential rejection. Check it at ${KEYS_URL}.`,
1085
+ );
1086
+ else if (status === 403) lines.push("The gateway refused this request. The reason it gave is above; that does not by itself mean the key is wrong.");
1087
+ else if (status === 402) lines.push(`Check your account state: npx @inferall/cli status`);
1088
+ else if (status === 429) lines.push("Rate limited. Retry shortly.");
1089
+ else if (status >= 500) lines.push("That is an infrastructure error, not a problem with your key. Retry shortly.");
1090
+ return lines;
1091
+ }
1092
+
1004
1093
 
1005
1094
  /**
1006
1095
  * Read a response as JSON, or say plainly that it was not JSON.
@@ -1877,9 +1966,175 @@ export function isPlausiblePatch(text, site, base) {
1877
1966
  export function writeExample(root, kind) {
1878
1967
  const p = join(root, kind === "python" ? "inferall_hello.py" : "inferall-hello.mjs");
1879
1968
  if (existsSync(p)) return { path: null, problem: null }; // never clobber theirs
1969
+ // The examples are dependency-free (Node fetch, Python urllib) and read the
1970
+ // key from the environment, else from the .env next to the file, so a plain
1971
+ // `node`/`python3` run works right after init. String.raw keeps the regex
1972
+ // backslashes; the generated code avoids backticks and "${" for that reason.
1880
1973
  const body = kind === "python"
1881
- ? `"""InferAll hello. Run: python inferall_hello.py (pip install inferall-ai)"""\nfrom inferall import Inferall\n\nai = Inferall() # reads INFERALL_API_KEY from the environment\nprint(ai.text("Explain InferAll in one sentence."))\n`
1882
- : `// InferAll hello. Run: node --env-file=.env inferall-hello.mjs\nconst res = await fetch("${API_BASE}/chat/completions", {\n method: "POST",\n headers: {\n Authorization: \`Bearer \${process.env.INFERALL_API_KEY}\`,\n "Content-Type": "application/json",\n },\n body: JSON.stringify({\n model: "${FREE_MODEL}",\n messages: [{ role: "user", content: "Explain InferAll in one sentence." }],\n }),\n});\nconsole.log((await res.json()).choices?.[0]?.message?.content ?? (await res.text()));\n`;
1974
+ ? String.raw`"""InferAll hello. Run: python3 inferall_hello.py (no packages needed)"""
1975
+ import json
1976
+ import http.client
1977
+ import os
1978
+ import re
1979
+ import sys
1980
+ import urllib.error
1981
+ import urllib.request
1982
+ from pathlib import Path
1983
+
1984
+ API_BASE = ${JSON.stringify(API_BASE)}
1985
+ MODEL = ${JSON.stringify(FREE_MODEL)}
1986
+
1987
+
1988
+ def fail(message):
1989
+ print("inferall: " + message, file=sys.stderr)
1990
+ sys.exit(1)
1991
+
1992
+
1993
+ def read_key():
1994
+ # INFERALL_API_KEY from the environment first, else the .env beside this file.
1995
+ key = os.environ.get("INFERALL_API_KEY", "").strip()
1996
+ if key:
1997
+ return key
1998
+ try:
1999
+ text = (Path(__file__).resolve().parent / ".env").read_text(encoding="utf-8-sig")
2000
+ except FileNotFoundError:
2001
+ return ""
2002
+ except OSError as e:
2003
+ fail("could not read .env: %s" % e)
2004
+ pattern = re.compile(r"^\s*(?:export\s+)?INFERALL_API_KEY\s*=(.*)$")
2005
+ for line in text.splitlines():
2006
+ m = pattern.match(line)
2007
+ if m:
2008
+ value = m.group(1).strip()
2009
+ if value[:1] in ('"', "'"):
2010
+ close = value.find(value[0], 1)
2011
+ return value[1:close] if close != -1 else value
2012
+ return re.sub(r"\s+#.*$", "", value).strip()
2013
+ return ""
2014
+
2015
+
2016
+ key = read_key()
2017
+ if not key:
2018
+ fail("INFERALL_API_KEY is not set. Export it or put it in the .env next to this file.")
2019
+
2020
+ request = urllib.request.Request(
2021
+ API_BASE + "/chat/completions",
2022
+ data=json.dumps({
2023
+ "model": MODEL,
2024
+ "messages": [{"role": "user", "content": "Explain InferAll in one sentence."}],
2025
+ }).encode(),
2026
+ headers={
2027
+ "Authorization": "Bearer " + key,
2028
+ "Content-Type": "application/json",
2029
+ "User-Agent": "inferall-hello-example",
2030
+ },
2031
+ method="POST",
2032
+ )
2033
+ try:
2034
+ try:
2035
+ res = urllib.request.urlopen(request, timeout=120)
2036
+ except urllib.error.HTTPError as e:
2037
+ res = e
2038
+ with res:
2039
+ status, raw = res.status, res.read()
2040
+ except (OSError, http.client.HTTPException) as e:
2041
+ fail("request failed: %s" % e)
2042
+
2043
+ text = raw.decode("utf-8", "replace")
2044
+ try:
2045
+ data = json.loads(text)
2046
+ except ValueError:
2047
+ data = None
2048
+ if not isinstance(data, dict):
2049
+ data = {}
2050
+
2051
+ if not 200 <= status < 300:
2052
+ err = data.get("error")
2053
+ detail = err.get("message") if isinstance(err, dict) else err
2054
+ detail = detail or data.get("message") or text.strip()[:300] or "no response body"
2055
+ fail("HTTP %d: %s" % (status, detail))
2056
+
2057
+ try:
2058
+ answer = data["choices"][0]["message"]["content"]
2059
+ except (KeyError, IndexError, TypeError):
2060
+ answer = None
2061
+ if not isinstance(answer, str) or not answer.strip():
2062
+ fail("the gateway answered HTTP %d but the response had no answer text" % status)
2063
+ print(answer)
2064
+ `
2065
+ : String.raw`// InferAll hello. Run: node inferall-hello.mjs (Node 18+, no packages needed)
2066
+ import { readFileSync } from "node:fs";
2067
+
2068
+ const API_BASE = ${JSON.stringify(API_BASE)};
2069
+ const MODEL = ${JSON.stringify(FREE_MODEL)};
2070
+
2071
+ function fail(message) {
2072
+ console.error("inferall: " + message);
2073
+ process.exit(1);
2074
+ }
2075
+
2076
+ // INFERALL_API_KEY from the environment first, else the .env beside this file.
2077
+ function readKey() {
2078
+ const fromEnv = (process.env.INFERALL_API_KEY || "").trim();
2079
+ if (fromEnv) return fromEnv;
2080
+ let text;
2081
+ try {
2082
+ text = readFileSync(new URL(".env", import.meta.url), "utf8");
2083
+ } catch (err) {
2084
+ if (err.code === "ENOENT") return "";
2085
+ fail("could not read .env: " + err.message);
2086
+ }
2087
+ for (const line of text.split(/\r?\n/)) {
2088
+ const m = line.match(/^\s*(?:export\s+)?INFERALL_API_KEY\s*=(.*)$/);
2089
+ if (m) {
2090
+ const value = m[1].trim();
2091
+ if (value[0] === '"' || value[0] === "'") {
2092
+ const close = value.indexOf(value[0], 1);
2093
+ return close === -1 ? value : value.slice(1, close);
2094
+ }
2095
+ return value.replace(/\s+#.*$/, "").trim();
2096
+ }
2097
+ }
2098
+ return "";
2099
+ }
2100
+
2101
+ const key = readKey();
2102
+ if (!key) fail("INFERALL_API_KEY is not set. Export it or put it in the .env next to this file.");
2103
+
2104
+ let res;
2105
+ let raw;
2106
+ try {
2107
+ res = await fetch(API_BASE + "/chat/completions", {
2108
+ method: "POST",
2109
+ headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" },
2110
+ body: JSON.stringify({
2111
+ model: MODEL,
2112
+ messages: [{ role: "user", content: "Explain InferAll in one sentence." }],
2113
+ }),
2114
+ signal: AbortSignal.timeout(120_000),
2115
+ });
2116
+ raw = await res.text();
2117
+ } catch (err) {
2118
+ fail("request failed: " + (err.name === "TimeoutError" ? "timed out after 120s" : err.message));
2119
+ }
2120
+
2121
+ let data;
2122
+ try {
2123
+ data = JSON.parse(raw);
2124
+ } catch {}
2125
+
2126
+ if (!res.ok) {
2127
+ const e = data?.error;
2128
+ const detail = (typeof e === "string" ? e : e?.message) || data?.message || raw.trim().slice(0, 300) || res.statusText;
2129
+ fail("HTTP " + res.status + ": " + detail);
2130
+ }
2131
+
2132
+ const answer = data?.choices?.[0]?.message?.content;
2133
+ if (typeof answer !== "string" || !answer.trim()) {
2134
+ fail("the gateway answered HTTP " + res.status + " but the response had no answer text");
2135
+ }
2136
+ console.log(answer);
2137
+ `;
1883
2138
  try {
1884
2139
  writeFileSync(p, body);
1885
2140
  return { path: p, problem: null };
@@ -2007,83 +2262,27 @@ async function chatCmd() {
2007
2262
  const parsedMax = rawMax === undefined ? 512 : parseInt(rawMax, 10);
2008
2263
  const maxTokens = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : 512;
2009
2264
  const reply = await apiChat(key, [{ role: "user", content: prompt }], model, maxTokens);
2010
- const { status, body } = reply;
2011
- // 2026-07-27 tick 833: a body we could not parse is a FAILED call whatever
2012
- // the status line said. Measured: a 200 whose JSON was truncated mid-stream
2013
- // (a proxy cutting the response) printed `{}` and exited 0, so a script
2014
- // wrapping this command saw success and an empty answer. Reporting success
2015
- // for a call that produced nothing is the rung of the ladder this product
2016
- // keeps landing on, and it is worth its own branch before the status checks.
2017
- if (reply.parseFailed) {
2018
- console.log(`${c.red("✗")} ${nonJsonSummary(reply)}`);
2019
- console.log(` ${c.dim(`HTTP ${status}. Nothing usable came back, so this call did not succeed.`)}`);
2020
- if (status >= 500 || status === 502 || status === 503) {
2021
- console.log(` That is an infrastructure error, not a problem with your key. Retry shortly.`);
2022
- }
2023
- rl.close();
2024
- exit(1);
2025
- }
2026
- if (status === 200) {
2027
- const choice = body?.choices?.[0];
2028
- const text = choice?.message?.content;
2029
- if (text) {
2030
- console.log(text);
2031
- /*
2032
- * Printed to STDERR, deliberately. `chat` is piped: the usage line above
2033
- * documents `echo "hi" | npx @inferall/cli chat`, and people redirect the
2034
- * answer into files and other commands. A counter on stdout would corrupt
2035
- * every one of those, which is a worse bug than the blindness it fixes.
2036
- * stderr reaches a human at a terminal and stays out of the pipe.
2037
- */
2038
- const countdown = trialCountdownLine(reply.trialUsage);
2039
- if (countdown) process.stderr.write(`${countdown}\n`);
2040
- } else if (choice?.finish_reason === "length") {
2041
- // 2026-07-26 tick 817. Reasoning models (nemotron and friends) spend the
2042
- // token budget on reasoning_content and return content: null with
2043
- // finish_reason "length". Measured at tick 811: nemotron at max_tokens=64
2044
- // returns null, at 256 it answers. The old code fell through to
2045
- // JSON.stringify(body), so a user who asked a question got a JSON blob
2046
- // back with no hint that the fix is simply a bigger budget.
2047
- console.log(c.yellow("The model used its whole token budget before answering."));
2048
- console.log(` ${c.dim("Reasoning models spend the budget thinking first. Retry with a larger")} ${c.bold("--max-tokens")}${c.dim(", or use a non-reasoning model such as")} ${c.bold(FREE_MODEL)}${c.dim(".")}`);
2049
- rl.close();
2050
- exit(1);
2051
- } else {
2052
- // 2026-07-27 tick 833: this printed a JSON blob and then fell out of the
2053
- // function, so the process exited 0. A 200 carrying no answer is still a
2054
- // call that produced nothing, and a caller piping this into anything had
2055
- // no way to tell. The blob is kept -- it is the only diagnostic there is
2056
- // -- but it is labelled and the exit code now says what happened.
2057
- console.log(`${c.yellow("~")} the gateway returned 200 with no answer in it:`);
2058
- console.log(` ${JSON.stringify(body).slice(0, 400)}`);
2059
- rl.close();
2060
- exit(1);
2061
- }
2062
- } else {
2063
- // 2026-07-26 tick 817: SHOW THE SERVER'S OWN MESSAGE.
2064
- //
2065
- // The gateway writes careful, model-aware 402 and 429 copy (ticks 812 and
2066
- // 815 were spent making it accurate) and this command threw it away. On 402
2067
- // it printed "your free allowance is used up or the account isn't activated
2068
- // yet", which conflates two different states and is simply WRONG for a user
2069
- // who was never activated: nothing of theirs is used up. The accurate
2070
- // sentence was in the response we discarded. On 429 it dumped raw JSON at
2071
- // the user, so the gateway's readable "retry in 42s" arrived wrapped in
2072
- // braces.
2073
- //
2074
- // Both are the same mistake: the client second-guessing a server that knows
2075
- // more than it does.
2076
- const serverMsg = body?.error?.message || body?.message;
2077
- console.log(`${status === 402 ? c.yellow("402") : c.red(`HTTP ${status}`)}: ${serverMsg || JSON.stringify(body).slice(0, 300)}`);
2078
- // Keep the pointer to `status`, which is genuinely useful and not something
2079
- // the server can know to suggest. Only for billing states, where "where do
2080
- // I stand" is the actual next question.
2081
- if (status === 402) {
2082
- console.log(` ${c.dim("check your account:")} ${c.bold("npx @inferall/cli status")}`);
2083
- }
2265
+ // The verdict is shared with doctor and init (classifyChatResult). Only a 200
2266
+ // with a non-blank string answer is success; every failure is reported on
2267
+ // STDERR so gateway diagnostics stay out of answer pipes.
2268
+ const verdict = classifyChatResult(reply);
2269
+ if (!verdict.ok) {
2270
+ const [first, ...rest] = chatFailureLines(verdict, reply, key, "chat");
2271
+ console.error(`${c.red("✗")} ${first}`);
2272
+ for (const line of rest) console.error(` ${line}`);
2084
2273
  rl.close();
2085
2274
  exit(1);
2086
2275
  }
2276
+ console.log(verdict.text);
2277
+ /*
2278
+ * Printed to STDERR, deliberately. `chat` is piped: the usage line above
2279
+ * documents `echo "hi" | npx @inferall/cli chat`, and people redirect the
2280
+ * answer into files and other commands. A counter on stdout would corrupt
2281
+ * every one of those, which is a worse bug than the blindness it fixes.
2282
+ * stderr reaches a human at a terminal and stays out of the pipe.
2283
+ */
2284
+ const countdown = trialCountdownLine(reply.trialUsage);
2285
+ if (countdown) process.stderr.write(`${countdown}\n`);
2087
2286
  rl.close();
2088
2287
  }
2089
2288
 
@@ -2427,14 +2626,12 @@ async function doctor() {
2427
2626
  // 5. live account state
2428
2627
  process.stdout.write(` live call (${FREE_MODEL}) … `);
2429
2628
  const test = await apiChat(key, [{ role: "user", content: "Reply with the single word: ready" }]);
2430
- if (test.status === 200) {
2431
- console.log(c.green("account active ✓"));
2432
- } else if (test.status === 402) {
2433
- console.log(c.yellow("account not activated (402)"));
2434
- console.log(` One-time ${c.bold("$5 activation")} (becomes spendable balance): ${c.cyan(BILLING_URL)}`);
2435
- ok = false;
2629
+ const probe = classifyChatResult(test);
2630
+ if (probe.ok) {
2631
+ console.log(c.green("answered ✓"));
2436
2632
  } else {
2437
- console.log(c.red(`HTTP ${test.status}`));
2633
+ console.log(c.red("failed"));
2634
+ for (const line of chatFailureLines(probe, test, key, "probe")) console.log(` ${line}`);
2438
2635
  ok = false;
2439
2636
  }
2440
2637
 
@@ -2858,6 +3055,10 @@ async function main() {
2858
3055
  console.log(` ${c.red("✗")} ${example.problem}`);
2859
3056
  envResult.problems.push(example.problem);
2860
3057
  }
3058
+ if (example.path) {
3059
+ const run = project.kind === "node" ? "node inferall-hello.mjs" : "python3 inferall_hello.py";
3060
+ console.log(` ${c.dim("run:")} ${c.bold(run)} ${c.dim("(reads INFERALL_API_KEY from the environment or .env)")}`);
3061
+ }
2861
3062
  if (project.kind === "node") console.log(` ${c.dim("optional:")} npm install @inferall/sdk`);
2862
3063
  else console.log(` ${c.dim("optional:")} pip install inferall-ai`);
2863
3064
  }
@@ -2883,7 +3084,8 @@ async function main() {
2883
3084
  const testModel = flagValue("model") || FREE_MODEL;
2884
3085
  process.stdout.write(`\n test call (${testModel}${testModel === FREE_MODEL ? ", $0 model" : ""}) … `);
2885
3086
  const test = await apiChat(key, [{ role: "user", content: "Reply with the single word: ready" }], testModel);
2886
- if (test.status === 200) {
3087
+ const probe = classifyChatResult(test);
3088
+ if (probe.ok) {
2887
3089
  console.log(c.green("success ✓"));
2888
3090
  // 2026-07-26 tick 804: do not claim the project is wired when we have just
2889
3091
  // printed guidance saying specific files were NOT changed. Saying "Done.
@@ -2934,59 +3136,19 @@ async function main() {
2934
3136
  } else {
2935
3137
  console.log(`\n${c.green(c.bold("Done."))} Your project is wired to InferAll. Models list: ${API_BASE}/models\n`);
2936
3138
  }
2937
- } else if (test.status === 402) {
2938
- console.log(c.yellow("blocked (402)"));
2939
- console.log(`\n Everything is wired up, but your account isn't activated yet.`);
2940
- // Real account state beats a generic message: trial counter + whether a
2941
- // checkout was already started (settle vs start are different next steps).
2942
- const st = await fetch(`${ANTHROPIC_BASE}/ai/v1/key/status`, {
2943
- signal: timeoutSignal(),
2944
- headers: { Authorization: `Bearer ${key}` },
2945
- }).then((r) => (r.ok ? r.json() : null)).catch(() => null);
2946
- if (st?.trial) {
2947
- console.log(` Trial: ${st.trial.used}/${st.trial.cap} free calls used. ${st.trial.remaining === 0 ? "none left" : `${st.trial.remaining} still available`}${st.has_card ? c.dim(" · checkout started but not settled, and finishing it activates you") : ""}.`);
2948
- }
2949
- // Same correction as printGeminiGuidance above; see the note there.
2950
- console.log(` The 10+ free NIM models are ${c.bold("$0 in/out")}; premium tokens bill at InferAll's published per-token rate. A one-time ${c.bold("$5 activation")} (it becomes spendable`);
2951
- console.log(` balance, not a fee) unlocks every model, including the 10+ free NIM models.`);
2952
- console.log(` Activate: ${c.cyan(BILLING_URL)}`);
2953
- console.log(` The code changes above are done. The first call will work the moment you activate.`);
2954
- console.log(` Check anytime: ${c.bold("npx @inferall/cli status")}\n`);
2955
- } else if (test.status === 401 || test.status === 403) {
2956
- // 2026-07-27 tick 826: this fell into the generic branch below, which
2957
- // printed a bare "HTTP 401", advised re-running init, and exited 0.
2958
- //
2959
- // Three separate things were wrong with that, in increasing order of harm.
2960
- // The wizard printed "verifying key ... ok" four lines earlier, so it
2961
- // contradicted its own output with no acknowledgement. Retrying an
2962
- // authentication failure with the same credential cannot succeed, so the
2963
- // remedy we printed was a dead end. And exiting 0 told every calling
2964
- // script that setup had worked, while the codemod had already repointed a
2965
- // working project at a gateway that rejects its key. A tool that breaks a
2966
- // project and reports success is the worst rung of the severity ladder,
2967
- // and this is the second time the exit status has been the tell (t824).
2968
- //
2969
- // The pre-flight hits /ai/v1/key/status and this hits /v1/chat/completions,
2970
- // so a key revoked between the two, or valid for a different account,
2971
- // produces exactly this split. Measured against a stub that answers 200 on
2972
- // status and 401 on chat.
2973
- console.log(c.red("rejected (401)"));
2974
- console.log(`\n The gateway accepted this key a moment ago and has now rejected it.`);
2975
- console.log(` That usually means the key was revoked or rotated between the two checks,`);
2976
- console.log(` or that it belongs to a different account than the one you are setting up.`);
2977
- console.log(`\n ${c.bold("Your files were already changed.")} The code and .env now point at InferAll,`);
2978
- console.log(` so calls will keep failing until the key works. Re-running with the same key`);
2979
- console.log(` cannot help.`);
2980
- console.log(`\n Mint a fresh key at ${c.cyan(KEYS_URL)}, then re-run:`);
2981
- console.log(` ${c.bold("npx @inferall/cli init --key ifu_your_new_key")}\n`);
2982
- rl.close();
2983
- exit(1);
2984
3139
  } else {
2985
- // Transient or unexpected. Retrying IS the right advice here, unlike the
2986
- // 401 case above, but the run still failed to verify so it must not exit 0.
2987
- console.log(c.red(`HTTP ${test.status}`));
2988
- console.log(` ${JSON.stringify(test.body).slice(0, 300)}`);
2989
- console.log(` Setup files were written; re-run ${c.bold("npx @inferall/cli init")} to retry the verify step.\n`);
3140
+ // Any failed probe (401, 402, 403, 429, 5xx, non-JSON, blank or empty
3141
+ // answer, token limit). Report what the gateway actually said, claim no
3142
+ // cause we have no evidence for, and never exit 0: the call did not work.
3143
+ console.log(c.red("failed"));
3144
+ const [first, ...rest] = chatFailureLines(probe, test, key, "probe");
3145
+ console.log(`\n ${c.red("✗")} ${first}`);
3146
+ for (const line of rest) console.log(` ${line}`);
3147
+ if (envResult.problems.length) {
3148
+ console.log(` ${envResult.keyWritten ? `Your key is in .env, but ${c.bold(".gitignore")} was not updated.` : c.bold("Your key was not saved.")}`);
3149
+ }
3150
+ console.log(`\n The setup changes reported above were applied, but the live call did not return an answer, so this setup is ${c.bold("not verified")}.`);
3151
+ console.log(` Once the cause above is resolved, re-run ${c.bold("npx @inferall/cli init")} to retry the verify step.\n`);
2990
3152
  rl.close();
2991
3153
  exit(1);
2992
3154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferall/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Install InferAll into your project in one command: npx @inferall/cli init",
5
5
  "bin": {
6
6
  "inferall": "bin/inferall.js"