@1930dev/opencode-usage 0.3.4 → 0.3.5
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 +21 -0
- package/dist/cli.js +119 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,10 @@ opencode-usage usage --pct # add the % BUDGET column (--by provider
|
|
|
58
58
|
opencode-usage providers
|
|
59
59
|
opencode-usage providers --no-net # cached only
|
|
60
60
|
|
|
61
|
+
# What the providers themselves answer about their budget
|
|
62
|
+
opencode-usage probe # one free-model call per provider
|
|
63
|
+
opencode-usage probe --json
|
|
64
|
+
|
|
61
65
|
# Model ranking by intelligence per blended dollar
|
|
62
66
|
opencode-usage top
|
|
63
67
|
opencode-usage top --limit 10 --json
|
|
@@ -161,6 +165,23 @@ Normalized percentage of budget consumed per provider, from these sources (in pr
|
|
|
161
165
|
|
|
162
166
|
Providers without any source show `—`.
|
|
163
167
|
|
|
168
|
+
## Free-model probe (`probe`)
|
|
169
|
+
|
|
170
|
+
`probe` sends one minimal request against a free model per connected provider
|
|
171
|
+
and reports what the answer reveals about the budget: a rate-limit header, a
|
|
172
|
+
"no credits" error, or the meter itself (`usage.neurons` for Cloudflare). It
|
|
173
|
+
reads the key from opencode's auth store and never prints it. Providers covered:
|
|
174
|
+
|
|
175
|
+
- `cloudflare-workers-ai` — the response carries neurons per request (the free
|
|
176
|
+
tier's unit), so the probe answers whether the account can still run
|
|
177
|
+
- `nvidia` — an HTTP 200 means the free per-model allowance still works
|
|
178
|
+
- `orcarouter` — the `free` model answers `rate_limit_error` with a
|
|
179
|
+
`retry-after`, and credits raise the cap
|
|
180
|
+
- `zai` — an HTTP 429 with `code 1113` means the balance is empty
|
|
181
|
+
|
|
182
|
+
`opencode` exposes no chat API for keys and `snowflake-cortex` is read through
|
|
183
|
+
its SQL quota, so both are skipped.
|
|
184
|
+
|
|
164
185
|
## Ranking (`top`)
|
|
165
186
|
|
|
166
187
|
Models ranked by intelligence index per blended dollar:
|
package/dist/cli.js
CHANGED
|
@@ -526,6 +526,15 @@ function fmtAgo(ms, now = Date.now()) {
|
|
|
526
526
|
return `${h}h ago`;
|
|
527
527
|
return `${Math.floor(h / 24)}d ago`;
|
|
528
528
|
}
|
|
529
|
+
function fmtRetry(seconds) {
|
|
530
|
+
if (seconds < 60)
|
|
531
|
+
return `${seconds}s`;
|
|
532
|
+
if (seconds < 3600)
|
|
533
|
+
return `${Math.round(seconds / 60)}m`;
|
|
534
|
+
if (seconds < 86400)
|
|
535
|
+
return `${Math.ceil(seconds / 3600)}h`;
|
|
536
|
+
return `${Math.ceil(seconds / 86400)}d`;
|
|
537
|
+
}
|
|
529
538
|
function pad(s, n) {
|
|
530
539
|
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
531
540
|
}
|
|
@@ -578,6 +587,18 @@ function providersTable(statuses, local) {
|
|
|
578
587
|
});
|
|
579
588
|
return table(headers, rows);
|
|
580
589
|
}
|
|
590
|
+
function probeTable(results) {
|
|
591
|
+
const headers = ["PROVIDER", "TAUGHT", "SIGNAL", "RETRY"];
|
|
592
|
+
const rows = results.map((r) => {
|
|
593
|
+
if (r.status === 0)
|
|
594
|
+
return [r.provider, "error", r.message ?? "no signal", "\u2014"];
|
|
595
|
+
const taught = r.ok ? "yes" : "no";
|
|
596
|
+
const signal = r.ok ? r.usage && Object.keys(r.usage).length > 0 ? Object.entries(r.usage).map(([k, v]) => `${k}=${v}`).join(", ") : "http ok" : r.message || (r.code ? `code ${r.code}` : `HTTP ${r.status}`);
|
|
597
|
+
const retry = r.retryAfterSeconds !== undefined ? fmtRetry(r.retryAfterSeconds) : "\u2014";
|
|
598
|
+
return [r.provider, taught, signal, retry];
|
|
599
|
+
});
|
|
600
|
+
return table(headers, rows);
|
|
601
|
+
}
|
|
581
602
|
// packages/core/src/ranking/fetch.ts
|
|
582
603
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
583
604
|
import path3 from "path";
|
|
@@ -875,6 +896,89 @@ function resolvePct(provider, sources) {
|
|
|
875
896
|
}
|
|
876
897
|
return { pct: 0, source: "none" };
|
|
877
898
|
}
|
|
899
|
+
// packages/core/src/probe.ts
|
|
900
|
+
function numericUsage(u) {
|
|
901
|
+
if (typeof u !== "object" || u === null)
|
|
902
|
+
return;
|
|
903
|
+
const out = {};
|
|
904
|
+
for (const [k, v] of Object.entries(u)) {
|
|
905
|
+
if (typeof v === "number")
|
|
906
|
+
out[k] = v;
|
|
907
|
+
}
|
|
908
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
909
|
+
}
|
|
910
|
+
function redact(text, key) {
|
|
911
|
+
return key.length > 6 ? text.split(key).join("[redacted]") : text;
|
|
912
|
+
}
|
|
913
|
+
var chatBody = (model) => JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 4 });
|
|
914
|
+
async function probeChat(p, key) {
|
|
915
|
+
let res;
|
|
916
|
+
try {
|
|
917
|
+
res = await fetch(p.url, {
|
|
918
|
+
method: "POST",
|
|
919
|
+
headers: {
|
|
920
|
+
"Content-Type": "application/json",
|
|
921
|
+
Accept: "application/json",
|
|
922
|
+
Authorization: `Bearer ${key}`
|
|
923
|
+
},
|
|
924
|
+
body: p.body
|
|
925
|
+
});
|
|
926
|
+
} catch (err) {
|
|
927
|
+
return { provider: p.name, status: 0, ok: false, message: redact(err.message, key) };
|
|
928
|
+
}
|
|
929
|
+
const text = await res.text();
|
|
930
|
+
let parsed = null;
|
|
931
|
+
try {
|
|
932
|
+
parsed = JSON.parse(text);
|
|
933
|
+
} catch {
|
|
934
|
+
parsed = null;
|
|
935
|
+
}
|
|
936
|
+
const obj = parsed ?? {};
|
|
937
|
+
const err = obj.error;
|
|
938
|
+
const firstErr = Array.isArray(obj.errors) ? obj.errors[0] : undefined;
|
|
939
|
+
const message = redact(err?.message ?? err?.detail ?? firstErr?.message ?? "", key);
|
|
940
|
+
const rawRetry = res.headers.get("retry-after");
|
|
941
|
+
const headerRetry = rawRetry !== null && rawRetry !== "" ? Number(rawRetry) : Number.NaN;
|
|
942
|
+
const retry = Number.isFinite(headerRetry) ? headerRetry : err?.metadata?.retry_after_seconds;
|
|
943
|
+
const usage = p.extractUsage ? p.extractUsage(obj) : numericUsage(obj.usage);
|
|
944
|
+
return {
|
|
945
|
+
provider: p.name,
|
|
946
|
+
status: res.status,
|
|
947
|
+
ok: res.status >= 200 && res.status < 300,
|
|
948
|
+
...err?.code ?? firstErr?.code ? { code: err?.code ?? firstErr?.code } : {},
|
|
949
|
+
...message ? { message } : {},
|
|
950
|
+
...Number.isFinite(retry) ? { retryAfterSeconds: retry } : {},
|
|
951
|
+
...usage ? { usage } : {}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
var PROBERS = {
|
|
955
|
+
zai: (key) => probeChat({ name: "zai", url: "https://api.z.ai/api/paas/v4/chat/completions", body: chatBody("glm-4.5-air") }, key),
|
|
956
|
+
nvidia: (key) => probeChat({ name: "nvidia", url: "https://integrate.api.nvidia.com/v1/chat/completions", body: chatBody("meta/llama-3.2-11b-vision-instruct") }, key),
|
|
957
|
+
orcarouter: (key) => probeChat({ name: "orcarouter", url: "https://api.orcarouter.ai/v1/chat/completions", body: chatBody("orcarouter/free") }, key),
|
|
958
|
+
"cloudflare-workers-ai": (key, meta) => {
|
|
959
|
+
const account = String(meta?.accountId ?? "");
|
|
960
|
+
if (!account) {
|
|
961
|
+
return Promise.resolve({ provider: "cloudflare-workers-ai", status: 0, ok: false, message: "metadata.accountId is required" });
|
|
962
|
+
}
|
|
963
|
+
return probeChat({
|
|
964
|
+
name: "cloudflare-workers-ai",
|
|
965
|
+
url: `https://api.cloudflare.com/client/v4/accounts/${account}/ai/run/@cf/meta/llama-3.1-8b-fast-v2`,
|
|
966
|
+
body: JSON.stringify({ prompt: "ping", max_tokens: 4 }),
|
|
967
|
+
extractUsage: (parsed) => numericUsage(typeof parsed === "object" && parsed !== null && "result" in parsed ? parsed.result.usage : undefined)
|
|
968
|
+
}, key);
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
async function runProbes() {
|
|
972
|
+
const auth = await readAuth();
|
|
973
|
+
const results = await Promise.all(Object.entries(auth).sort().map(async ([name, entry]) => {
|
|
974
|
+
const prober = PROBERS[name];
|
|
975
|
+
const key = authSecret(entry);
|
|
976
|
+
if (!prober || !key)
|
|
977
|
+
return null;
|
|
978
|
+
return prober(key, entry.metadata);
|
|
979
|
+
}));
|
|
980
|
+
return results.filter((r) => r !== null);
|
|
981
|
+
}
|
|
878
982
|
// packages/core/src/snapshot.ts
|
|
879
983
|
async function withConnectedProviders(rows) {
|
|
880
984
|
let connected;
|
|
@@ -948,6 +1052,7 @@ var USAGE = `opencode-usage \u2014 usage tracking and model ranking for opencode
|
|
|
948
1052
|
USAGE
|
|
949
1053
|
opencode-usage usage [--since 7d] [--by provider|model|day|project|agent] [--today] [--pct] [--json]
|
|
950
1054
|
opencode-usage providers [--no-net] [--json]
|
|
1055
|
+
opencode-usage probe [--json]
|
|
951
1056
|
opencode-usage top [--limit 20] [--json]
|
|
952
1057
|
|
|
953
1058
|
OPTIONS
|
|
@@ -1037,6 +1142,18 @@ async function cmdProviders(flags, json) {
|
|
|
1037
1142
|
db.close();
|
|
1038
1143
|
}
|
|
1039
1144
|
}
|
|
1145
|
+
async function cmdProbe(flags, json) {
|
|
1146
|
+
const results = await runProbes();
|
|
1147
|
+
if (json) {
|
|
1148
|
+
console.log(JSON.stringify({ probes: results }, null, 2));
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const auth = await readAuth();
|
|
1152
|
+
const withoutProbe = Object.entries(auth).filter(([, e]) => e.key || e.access).map(([p]) => p).filter((p) => !["cloudflare-workers-ai", "nvidia", "orcarouter", "zai"].includes(p)).sort();
|
|
1153
|
+
console.log(probeTable(results));
|
|
1154
|
+
if (withoutProbe.length > 0)
|
|
1155
|
+
console.log(`no free-model probe: ${withoutProbe.join(", ")}`);
|
|
1156
|
+
}
|
|
1040
1157
|
var TOP_HEADERS = ["MODEL", "NAME", "IQ", "CODING", "$/M", "IQ/$"];
|
|
1041
1158
|
var TOP_WIDTHS = [28, 26, 5, 7, 8, 6];
|
|
1042
1159
|
function topLine(cells) {
|
|
@@ -1074,6 +1191,8 @@ async function run(argv) {
|
|
|
1074
1191
|
await cmdUsage(flags, json);
|
|
1075
1192
|
else if (cmd === "providers")
|
|
1076
1193
|
await cmdProviders(flags, json);
|
|
1194
|
+
else if (cmd === "probe")
|
|
1195
|
+
await cmdProbe(flags, json);
|
|
1077
1196
|
else if (cmd === "top")
|
|
1078
1197
|
await cmdTop(flags, json);
|
|
1079
1198
|
else
|