@inferall/cli 0.2.2 → 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
@@ -90,3 +90,9 @@ apply, and leaves your code otherwise untouched, with no reformatting and no
90
90
  renames.
91
91
 
92
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":"51fd6a7","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.
@@ -2173,83 +2262,27 @@ async function chatCmd() {
2173
2262
  const parsedMax = rawMax === undefined ? 512 : parseInt(rawMax, 10);
2174
2263
  const maxTokens = Number.isFinite(parsedMax) && parsedMax > 0 ? parsedMax : 512;
2175
2264
  const reply = await apiChat(key, [{ role: "user", content: prompt }], model, maxTokens);
2176
- const { status, body } = reply;
2177
- // 2026-07-27 tick 833: a body we could not parse is a FAILED call whatever
2178
- // the status line said. Measured: a 200 whose JSON was truncated mid-stream
2179
- // (a proxy cutting the response) printed `{}` and exited 0, so a script
2180
- // wrapping this command saw success and an empty answer. Reporting success
2181
- // for a call that produced nothing is the rung of the ladder this product
2182
- // keeps landing on, and it is worth its own branch before the status checks.
2183
- if (reply.parseFailed) {
2184
- console.log(`${c.red("✗")} ${nonJsonSummary(reply)}`);
2185
- console.log(` ${c.dim(`HTTP ${status}. Nothing usable came back, so this call did not succeed.`)}`);
2186
- if (status >= 500 || status === 502 || status === 503) {
2187
- console.log(` That is an infrastructure error, not a problem with your key. Retry shortly.`);
2188
- }
2189
- rl.close();
2190
- exit(1);
2191
- }
2192
- if (status === 200) {
2193
- const choice = body?.choices?.[0];
2194
- const text = choice?.message?.content;
2195
- if (text) {
2196
- console.log(text);
2197
- /*
2198
- * Printed to STDERR, deliberately. `chat` is piped: the usage line above
2199
- * documents `echo "hi" | npx @inferall/cli chat`, and people redirect the
2200
- * answer into files and other commands. A counter on stdout would corrupt
2201
- * every one of those, which is a worse bug than the blindness it fixes.
2202
- * stderr reaches a human at a terminal and stays out of the pipe.
2203
- */
2204
- const countdown = trialCountdownLine(reply.trialUsage);
2205
- if (countdown) process.stderr.write(`${countdown}\n`);
2206
- } else if (choice?.finish_reason === "length") {
2207
- // 2026-07-26 tick 817. Reasoning models (nemotron and friends) spend the
2208
- // token budget on reasoning_content and return content: null with
2209
- // finish_reason "length". Measured at tick 811: nemotron at max_tokens=64
2210
- // returns null, at 256 it answers. The old code fell through to
2211
- // JSON.stringify(body), so a user who asked a question got a JSON blob
2212
- // back with no hint that the fix is simply a bigger budget.
2213
- console.log(c.yellow("The model used its whole token budget before answering."));
2214
- 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(".")}`);
2215
- rl.close();
2216
- exit(1);
2217
- } else {
2218
- // 2026-07-27 tick 833: this printed a JSON blob and then fell out of the
2219
- // function, so the process exited 0. A 200 carrying no answer is still a
2220
- // call that produced nothing, and a caller piping this into anything had
2221
- // no way to tell. The blob is kept -- it is the only diagnostic there is
2222
- // -- but it is labelled and the exit code now says what happened.
2223
- console.log(`${c.yellow("~")} the gateway returned 200 with no answer in it:`);
2224
- console.log(` ${JSON.stringify(body).slice(0, 400)}`);
2225
- rl.close();
2226
- exit(1);
2227
- }
2228
- } else {
2229
- // 2026-07-26 tick 817: SHOW THE SERVER'S OWN MESSAGE.
2230
- //
2231
- // The gateway writes careful, model-aware 402 and 429 copy (ticks 812 and
2232
- // 815 were spent making it accurate) and this command threw it away. On 402
2233
- // it printed "your free allowance is used up or the account isn't activated
2234
- // yet", which conflates two different states and is simply WRONG for a user
2235
- // who was never activated: nothing of theirs is used up. The accurate
2236
- // sentence was in the response we discarded. On 429 it dumped raw JSON at
2237
- // the user, so the gateway's readable "retry in 42s" arrived wrapped in
2238
- // braces.
2239
- //
2240
- // Both are the same mistake: the client second-guessing a server that knows
2241
- // more than it does.
2242
- const serverMsg = body?.error?.message || body?.message;
2243
- console.log(`${status === 402 ? c.yellow("402") : c.red(`HTTP ${status}`)}: ${serverMsg || JSON.stringify(body).slice(0, 300)}`);
2244
- // Keep the pointer to `status`, which is genuinely useful and not something
2245
- // the server can know to suggest. Only for billing states, where "where do
2246
- // I stand" is the actual next question.
2247
- if (status === 402) {
2248
- console.log(` ${c.dim("check your account:")} ${c.bold("npx @inferall/cli status")}`);
2249
- }
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}`);
2250
2273
  rl.close();
2251
2274
  exit(1);
2252
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`);
2253
2286
  rl.close();
2254
2287
  }
2255
2288
 
@@ -2593,14 +2626,12 @@ async function doctor() {
2593
2626
  // 5. live account state
2594
2627
  process.stdout.write(` live call (${FREE_MODEL}) … `);
2595
2628
  const test = await apiChat(key, [{ role: "user", content: "Reply with the single word: ready" }]);
2596
- if (test.status === 200) {
2597
- console.log(c.green("account active ✓"));
2598
- } else if (test.status === 402) {
2599
- console.log(c.yellow("account not activated (402)"));
2600
- console.log(` One-time ${c.bold("$5 activation")} (becomes spendable balance): ${c.cyan(BILLING_URL)}`);
2601
- ok = false;
2629
+ const probe = classifyChatResult(test);
2630
+ if (probe.ok) {
2631
+ console.log(c.green("answered ✓"));
2602
2632
  } else {
2603
- 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}`);
2604
2635
  ok = false;
2605
2636
  }
2606
2637
 
@@ -3053,7 +3084,8 @@ async function main() {
3053
3084
  const testModel = flagValue("model") || FREE_MODEL;
3054
3085
  process.stdout.write(`\n test call (${testModel}${testModel === FREE_MODEL ? ", $0 model" : ""}) … `);
3055
3086
  const test = await apiChat(key, [{ role: "user", content: "Reply with the single word: ready" }], testModel);
3056
- if (test.status === 200) {
3087
+ const probe = classifyChatResult(test);
3088
+ if (probe.ok) {
3057
3089
  console.log(c.green("success ✓"));
3058
3090
  // 2026-07-26 tick 804: do not claim the project is wired when we have just
3059
3091
  // printed guidance saying specific files were NOT changed. Saying "Done.
@@ -3104,59 +3136,19 @@ async function main() {
3104
3136
  } else {
3105
3137
  console.log(`\n${c.green(c.bold("Done."))} Your project is wired to InferAll. Models list: ${API_BASE}/models\n`);
3106
3138
  }
3107
- } else if (test.status === 402) {
3108
- console.log(c.yellow("blocked (402)"));
3109
- console.log(`\n Everything is wired up, but your account isn't activated yet.`);
3110
- // Real account state beats a generic message: trial counter + whether a
3111
- // checkout was already started (settle vs start are different next steps).
3112
- const st = await fetch(`${ANTHROPIC_BASE}/ai/v1/key/status`, {
3113
- signal: timeoutSignal(),
3114
- headers: { Authorization: `Bearer ${key}` },
3115
- }).then((r) => (r.ok ? r.json() : null)).catch(() => null);
3116
- if (st?.trial) {
3117
- 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") : ""}.`);
3118
- }
3119
- // Same correction as printGeminiGuidance above; see the note there.
3120
- 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`);
3121
- console.log(` balance, not a fee) unlocks every model, including the 10+ free NIM models.`);
3122
- console.log(` Activate: ${c.cyan(BILLING_URL)}`);
3123
- console.log(` The code changes above are done. The first call will work the moment you activate.`);
3124
- console.log(` Check anytime: ${c.bold("npx @inferall/cli status")}\n`);
3125
- } else if (test.status === 401 || test.status === 403) {
3126
- // 2026-07-27 tick 826: this fell into the generic branch below, which
3127
- // printed a bare "HTTP 401", advised re-running init, and exited 0.
3128
- //
3129
- // Three separate things were wrong with that, in increasing order of harm.
3130
- // The wizard printed "verifying key ... ok" four lines earlier, so it
3131
- // contradicted its own output with no acknowledgement. Retrying an
3132
- // authentication failure with the same credential cannot succeed, so the
3133
- // remedy we printed was a dead end. And exiting 0 told every calling
3134
- // script that setup had worked, while the codemod had already repointed a
3135
- // working project at a gateway that rejects its key. A tool that breaks a
3136
- // project and reports success is the worst rung of the severity ladder,
3137
- // and this is the second time the exit status has been the tell (t824).
3138
- //
3139
- // The pre-flight hits /ai/v1/key/status and this hits /v1/chat/completions,
3140
- // so a key revoked between the two, or valid for a different account,
3141
- // produces exactly this split. Measured against a stub that answers 200 on
3142
- // status and 401 on chat.
3143
- console.log(c.red("rejected (401)"));
3144
- console.log(`\n The gateway accepted this key a moment ago and has now rejected it.`);
3145
- console.log(` That usually means the key was revoked or rotated between the two checks,`);
3146
- console.log(` or that it belongs to a different account than the one you are setting up.`);
3147
- console.log(`\n ${c.bold("Your files were already changed.")} The code and .env now point at InferAll,`);
3148
- console.log(` so calls will keep failing until the key works. Re-running with the same key`);
3149
- console.log(` cannot help.`);
3150
- console.log(`\n Mint a fresh key at ${c.cyan(KEYS_URL)}, then re-run:`);
3151
- console.log(` ${c.bold("npx @inferall/cli init --key ifu_your_new_key")}\n`);
3152
- rl.close();
3153
- exit(1);
3154
3139
  } else {
3155
- // Transient or unexpected. Retrying IS the right advice here, unlike the
3156
- // 401 case above, but the run still failed to verify so it must not exit 0.
3157
- console.log(c.red(`HTTP ${test.status}`));
3158
- console.log(` ${JSON.stringify(test.body).slice(0, 300)}`);
3159
- 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`);
3160
3152
  rl.close();
3161
3153
  exit(1);
3162
3154
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inferall/cli",
3
- "version": "0.2.2",
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"