@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 +6 -0
- package/bin/build-info.json +1 -1
- package/bin/inferall.js +127 -135
- package/package.json +1 -1
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.
|
package/bin/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
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
|
-
|
|
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
|
-
|
|
2177
|
-
//
|
|
2178
|
-
//
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
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
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
3156
|
-
//
|
|
3157
|
-
|
|
3158
|
-
console.log(
|
|
3159
|
-
|
|
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
|
}
|