byollm 0.1.0-alpha.2 → 0.1.0-alpha.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 +8 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-DSQTDCOZ.js → chunk-2W2ZIXAS.js} +340 -25
- package/dist/chunk-2W2ZIXAS.js.map +1 -0
- package/dist/index.d.ts +97 -4
- package/dist/index.js +5 -1
- package/package.json +3 -3
- package/dist/chunk-DSQTDCOZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
> [!WARNING]
|
|
2
|
-
> **Alpha (`0.1.0-alpha.
|
|
2
|
+
> **Alpha (`0.1.0-alpha.3`) — under active development. Don't use this yet.**
|
|
3
3
|
>
|
|
4
4
|
> Install it deliberately: `npx byollm@alpha`, or `npm install byollm@alpha`.
|
|
5
5
|
>
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
> bare install resolves here too. This notice is the only guard — deliberately
|
|
13
13
|
> not an npm deprecation, which would read as *abandoned* rather than *early*.
|
|
14
14
|
> Ask for `@alpha` explicitly so your lockfile records that you meant to.
|
|
15
|
+
>
|
|
16
|
+
> **`alpha.3` is a breaking change.** A config naming `openai-http` with a
|
|
17
|
+
> remote base URL and an offer scope wider than `self` is narrowed to `self`
|
|
18
|
+
> until you acknowledge the spend and set a daily ceiling:
|
|
19
|
+
> `byollm offer <backend> public --cap <cents>`. Local base URLs are
|
|
20
|
+
> unaffected. `byollm backends` shows the cost class per route.
|
|
15
21
|
|
|
16
22
|
# `byollm`
|
|
17
23
|
|
|
@@ -103,6 +109,7 @@ Off by default. A fresh daemon runs your work and nobody else's.
|
|
|
103
109
|
```bash
|
|
104
110
|
byollm allow https://your-app.com alice # asks you to confirm, in plain words
|
|
105
111
|
byollm allow --list # everyone who can use this machine
|
|
112
|
+
byollm offer openai public --cap 250 # share a paid backend, with a ceiling
|
|
106
113
|
byollm disallow https://your-app.com alice
|
|
107
114
|
```
|
|
108
115
|
|
package/dist/bin.js
CHANGED
|
@@ -93,9 +93,11 @@ function normalizeOrigin(input) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// src/cli.ts
|
|
96
|
-
import { mkdir as
|
|
96
|
+
import { mkdir as mkdir6, readFile as readFile7, rm as rm2, stat, writeFile as writeFile6 } from "fs/promises";
|
|
97
97
|
import { hostname, userInfo } from "os";
|
|
98
|
+
import { dirname as dirname6 } from "path";
|
|
98
99
|
import { createInterface } from "readline/promises";
|
|
100
|
+
import { backendDescriptor as backendDescriptor3, resolveCost as resolveCost2 } from "@byollm/protocol";
|
|
99
101
|
|
|
100
102
|
// src/budgets.ts
|
|
101
103
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -368,7 +370,8 @@ import {
|
|
|
368
370
|
JobKind,
|
|
369
371
|
OfferScope,
|
|
370
372
|
backendDescriptor,
|
|
371
|
-
effectiveOfferScope
|
|
373
|
+
effectiveOfferScope,
|
|
374
|
+
resolveCost
|
|
372
375
|
} from "@byollm/protocol";
|
|
373
376
|
import { z as z3 } from "zod";
|
|
374
377
|
|
|
@@ -466,7 +469,31 @@ var BackendConfig = z3.object({
|
|
|
466
469
|
* remote OpenAI-compatible server that needs one. The *name*, never the
|
|
467
470
|
* value — a key does not belong in a config file the owner may share.
|
|
468
471
|
*/
|
|
469
|
-
apiKeyEnv: z3.string().optional()
|
|
472
|
+
apiKeyEnv: z3.string().optional(),
|
|
473
|
+
/**
|
|
474
|
+
* Required before a `metered` backend may be offered past `self`
|
|
475
|
+
* ({@link MUSTS.METERED_DEFAULTS_SELF}). There is deliberately no `cost`
|
|
476
|
+
* field: a provider's cost class comes from the protocol registry and is
|
|
477
|
+
* not the owner's to declare ({@link MUSTS.COST_NOT_CONFIGURABLE}).
|
|
478
|
+
*/
|
|
479
|
+
spend: z3.object({
|
|
480
|
+
/** Set by `byollm offer`, which states the consequence in words. */
|
|
481
|
+
acknowledged: z3.boolean().default(false),
|
|
482
|
+
/**
|
|
483
|
+
* Ceiling on community spend, in whole cents, per day. A widened
|
|
484
|
+
* metered backend without one is refused
|
|
485
|
+
* ({@link MUSTS.METERED_REQUIRES_CEILING}) — "unlimited" is not a
|
|
486
|
+
* thing an owner can mean by accident.
|
|
487
|
+
*/
|
|
488
|
+
dailyCapCents: z3.number().int().positive().optional(),
|
|
489
|
+
/**
|
|
490
|
+
* What this provider charges, for the ceiling estimate. Providers do
|
|
491
|
+
* not return a price, so the owner supplies one; the default is
|
|
492
|
+
* deliberately high so an unset rate trips the brake early rather
|
|
493
|
+
* than late.
|
|
494
|
+
*/
|
|
495
|
+
centsPerMillionTokens: z3.number().positive().default(1500)
|
|
496
|
+
}).strict().optional()
|
|
470
497
|
}).strict();
|
|
471
498
|
var RouteConfig = z3.object({
|
|
472
499
|
/** Key into {@link DaemonConfig.backends}. */
|
|
@@ -563,15 +590,16 @@ function resolveConfig(config) {
|
|
|
563
590
|
continue;
|
|
564
591
|
}
|
|
565
592
|
const descriptor = backendDescriptor(backend.backend);
|
|
593
|
+
const baseUrl = backend.baseUrl ?? descriptor.defaultBaseUrl;
|
|
566
594
|
if (descriptor.class === "http") {
|
|
567
|
-
if (
|
|
595
|
+
if (baseUrl === void 0) {
|
|
568
596
|
problems.push({
|
|
569
597
|
where: `backends.${route.backend}`,
|
|
570
598
|
message: "an HTTP-class backend needs a baseUrl"
|
|
571
599
|
});
|
|
572
600
|
continue;
|
|
573
601
|
}
|
|
574
|
-
const check = checkBaseUrl(
|
|
602
|
+
const check = checkBaseUrl(baseUrl);
|
|
575
603
|
if (!check.ok) {
|
|
576
604
|
problems.push({
|
|
577
605
|
where: `backends.${route.backend}.baseUrl`,
|
|
@@ -580,12 +608,25 @@ function resolveConfig(config) {
|
|
|
580
608
|
continue;
|
|
581
609
|
}
|
|
582
610
|
}
|
|
611
|
+
const cost = resolveCost(backend.backend, baseUrl);
|
|
612
|
+
const acknowledged = backend.spend?.acknowledged === true;
|
|
613
|
+
const capCents = backend.spend?.dailyCapCents;
|
|
614
|
+
const widened = backend.offer !== "self";
|
|
615
|
+
if (cost === "metered" && widened && acknowledged && capCents === void 0) {
|
|
616
|
+
problems.push({
|
|
617
|
+
where: `backends.${route.backend}.spend`,
|
|
618
|
+
message: "sharing a metered backend needs spend.dailyCapCents \u2014 an unlimited ceiling is not something anyone means on purpose"
|
|
619
|
+
});
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
583
622
|
const configured = backend.offer;
|
|
584
|
-
const offerScope = effectiveOfferScope(configured,
|
|
623
|
+
const offerScope = effectiveOfferScope(configured, cost, {
|
|
624
|
+
acknowledged: acknowledged && capCents !== void 0
|
|
625
|
+
});
|
|
585
626
|
if (offerScope !== configured) {
|
|
586
627
|
problems.push({
|
|
587
628
|
where: `backends.${route.backend}.offer`,
|
|
588
|
-
message: `"${configured}" was ignored: ${descriptor.label} runs on your own subscription, so it is locked to your work only`
|
|
629
|
+
message: cost === "subscription" ? `"${configured}" was ignored: ${descriptor.label} runs on your own subscription, so it is locked to your work only` : `"${configured}" was narrowed to "self": ${descriptor.label} bills you per token. \`byollm offer ${route.backend} ${configured}\` to share it deliberately, with a ceiling`
|
|
589
630
|
});
|
|
590
631
|
}
|
|
591
632
|
routes.push({
|
|
@@ -594,8 +635,12 @@ function resolveConfig(config) {
|
|
|
594
635
|
backendId: backend.backend,
|
|
595
636
|
backendClass: descriptor.class,
|
|
596
637
|
model: route.model,
|
|
638
|
+
cost,
|
|
597
639
|
offerScope,
|
|
598
|
-
|
|
640
|
+
spendAcknowledged: acknowledged,
|
|
641
|
+
spendDailyCapCents: capCents,
|
|
642
|
+
spendCentsPerMillionTokens: backend.spend?.centsPerMillionTokens ?? 1500,
|
|
643
|
+
baseUrl,
|
|
599
644
|
apiKeyEnv: backend.apiKeyEnv
|
|
600
645
|
});
|
|
601
646
|
}
|
|
@@ -907,6 +952,97 @@ var Pairings = class {
|
|
|
907
952
|
}
|
|
908
953
|
};
|
|
909
954
|
|
|
955
|
+
// src/spend.ts
|
|
956
|
+
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
957
|
+
import { dirname as dirname5 } from "path";
|
|
958
|
+
import { z as z6 } from "zod";
|
|
959
|
+
var SpendFile = z6.object({
|
|
960
|
+
version: z6.literal(1),
|
|
961
|
+
/** Per backend key: epoch-ms timestamps and the cents charged. */
|
|
962
|
+
entries: z6.record(
|
|
963
|
+
z6.string(),
|
|
964
|
+
z6.array(
|
|
965
|
+
z6.object({ at: z6.number().int().positive(), cents: z6.number().min(0) })
|
|
966
|
+
)
|
|
967
|
+
)
|
|
968
|
+
}).strict();
|
|
969
|
+
var SpendLedger = class {
|
|
970
|
+
#path;
|
|
971
|
+
#entries = {};
|
|
972
|
+
#loaded = false;
|
|
973
|
+
constructor(path) {
|
|
974
|
+
this.#path = path;
|
|
975
|
+
}
|
|
976
|
+
async load(now) {
|
|
977
|
+
try {
|
|
978
|
+
const parsed = SpendFile.safeParse(
|
|
979
|
+
JSON.parse(await readFile6(this.#path, "utf8"))
|
|
980
|
+
);
|
|
981
|
+
this.#entries = parsed.success ? parsed.data.entries : {};
|
|
982
|
+
} catch {
|
|
983
|
+
this.#entries = {};
|
|
984
|
+
}
|
|
985
|
+
this.#prune(now);
|
|
986
|
+
this.#loaded = true;
|
|
987
|
+
}
|
|
988
|
+
/** Cents spent on community work for this backend in the last 24 hours. */
|
|
989
|
+
spentTodayCents(backendKey, now) {
|
|
990
|
+
this.#assertLoaded();
|
|
991
|
+
const since = now - 864e5;
|
|
992
|
+
return (this.#entries[backendKey] ?? []).filter((e) => e.at >= since).reduce((sum, e) => sum + e.cents, 0);
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Has this backend spent its daily ceiling?
|
|
996
|
+
*
|
|
997
|
+
* A backend with no ceiling reads as reached — not as unlimited. That is
|
|
998
|
+
* the safe direction and it matches the config rule: sharing without a
|
|
999
|
+
* ceiling is refused at load, so reaching this state means something is
|
|
1000
|
+
* inconsistent and the brake should be on.
|
|
1001
|
+
*/
|
|
1002
|
+
hasReachedCeiling(backendKey, capCents, now) {
|
|
1003
|
+
if (capCents === void 0) return true;
|
|
1004
|
+
return this.spentTodayCents(backendKey, now) >= capCents;
|
|
1005
|
+
}
|
|
1006
|
+
/** Record an estimated charge for community work. */
|
|
1007
|
+
async record(backendKey, cents, now) {
|
|
1008
|
+
this.#assertLoaded();
|
|
1009
|
+
(this.#entries[backendKey] ??= []).push({ at: now, cents });
|
|
1010
|
+
this.#prune(now);
|
|
1011
|
+
await mkdir5(dirname5(this.#path), { recursive: true });
|
|
1012
|
+
await writeFile5(
|
|
1013
|
+
this.#path,
|
|
1014
|
+
JSON.stringify({ version: 1, entries: this.#entries }),
|
|
1015
|
+
{ mode: 384 }
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
/** Everything spent per backend today, for `byollm status`. */
|
|
1019
|
+
summary(now) {
|
|
1020
|
+
this.#assertLoaded();
|
|
1021
|
+
const out = {};
|
|
1022
|
+
for (const key of Object.keys(this.#entries)) {
|
|
1023
|
+
out[key] = this.spentTodayCents(key, now);
|
|
1024
|
+
}
|
|
1025
|
+
return out;
|
|
1026
|
+
}
|
|
1027
|
+
#assertLoaded() {
|
|
1028
|
+
if (!this.#loaded) throw new Error("spend ledger used before load()");
|
|
1029
|
+
}
|
|
1030
|
+
/** Anything older than a day can never affect the window again. */
|
|
1031
|
+
#prune(now) {
|
|
1032
|
+
const cutoff = now - 864e5;
|
|
1033
|
+
const pruned = {};
|
|
1034
|
+
for (const [key, entries] of Object.entries(this.#entries)) {
|
|
1035
|
+
const kept = entries.filter((e) => e.at >= cutoff);
|
|
1036
|
+
if (kept.length > 0) pruned[key] = kept;
|
|
1037
|
+
}
|
|
1038
|
+
this.#entries = pruned;
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
function estimateCents(promptChars, outputChars, centsPerMillionTokens) {
|
|
1042
|
+
const tokens = (promptChars + outputChars) / 4;
|
|
1043
|
+
return tokens / 1e6 * centsPerMillionTokens;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
910
1046
|
// src/paths.ts
|
|
911
1047
|
import { homedir } from "os";
|
|
912
1048
|
import { join } from "path";
|
|
@@ -918,6 +1054,7 @@ function daemonPaths(root = defaultRoot()) {
|
|
|
918
1054
|
allowlist: join(root, "allow.json"),
|
|
919
1055
|
ingressLog: join(root, "ingress.log"),
|
|
920
1056
|
budgets: join(root, "budgets.json"),
|
|
1057
|
+
spend: join(root, "spend.json"),
|
|
921
1058
|
pauseFlag: join(root, "paused"),
|
|
922
1059
|
scratch: join(root, "scratch")
|
|
923
1060
|
};
|
|
@@ -929,13 +1066,15 @@ function defaultRoot() {
|
|
|
929
1066
|
// src/runner.ts
|
|
930
1067
|
import {
|
|
931
1068
|
REFUSAL_MESSAGES,
|
|
932
|
-
backendDescriptor as backendDescriptor2,
|
|
933
1069
|
matchAudience,
|
|
934
1070
|
payloadTextLength
|
|
935
1071
|
} from "@byollm/protocol";
|
|
936
1072
|
|
|
937
1073
|
// src/backends/index.ts
|
|
938
|
-
import {
|
|
1074
|
+
import {
|
|
1075
|
+
BACKEND_IDS,
|
|
1076
|
+
backendDescriptor as backendDescriptor2
|
|
1077
|
+
} from "@byollm/protocol";
|
|
939
1078
|
|
|
940
1079
|
// src/backends/claude-cli.ts
|
|
941
1080
|
import { execFile, spawn } from "child_process";
|
|
@@ -1368,12 +1507,10 @@ function describeFetchError(error, origin) {
|
|
|
1368
1507
|
|
|
1369
1508
|
// src/backends/index.ts
|
|
1370
1509
|
function createBackend(id, init) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
return new OpenAiHttpBackend(init);
|
|
1374
|
-
case "claude-cli":
|
|
1375
|
-
return new ClaudeCliBackend();
|
|
1510
|
+
if (backendDescriptor2(id).class === "process") {
|
|
1511
|
+
return new ClaudeCliBackend();
|
|
1376
1512
|
}
|
|
1513
|
+
return new OpenAiHttpBackend(init);
|
|
1377
1514
|
}
|
|
1378
1515
|
var IMPLEMENTED_BACKEND_IDS = BACKEND_IDS;
|
|
1379
1516
|
|
|
@@ -1485,6 +1622,21 @@ var Runner = class {
|
|
|
1485
1622
|
}
|
|
1486
1623
|
return backend;
|
|
1487
1624
|
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Has this route spent the owner's daily ceiling on other people's work?
|
|
1627
|
+
*
|
|
1628
|
+
* Only meaningful for `metered` routes; `free` and `subscription` never
|
|
1629
|
+
* reach here in a way that matters, because the matcher refuses them for
|
|
1630
|
+
* other reasons first ({@link MUSTS.METERED_REQUIRES_CEILING}).
|
|
1631
|
+
*/
|
|
1632
|
+
#spendCeilingReached(route) {
|
|
1633
|
+
if (route.cost !== "metered") return false;
|
|
1634
|
+
return this.#options.spend.hasReachedCeiling(
|
|
1635
|
+
route.backendKey,
|
|
1636
|
+
route.spendDailyCapCents,
|
|
1637
|
+
this.#now()
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1488
1640
|
#routeFor(kind) {
|
|
1489
1641
|
return this.#options.loaded.routes.find((route) => route.kind === kind);
|
|
1490
1642
|
}
|
|
@@ -1512,7 +1664,11 @@ var Runner = class {
|
|
|
1512
1664
|
{
|
|
1513
1665
|
owner: this.#options.owner,
|
|
1514
1666
|
offerScope: route.offerScope,
|
|
1515
|
-
|
|
1667
|
+
cost: route.cost,
|
|
1668
|
+
spend: {
|
|
1669
|
+
acknowledged: route.spendAcknowledged,
|
|
1670
|
+
ceilingReached: this.#spendCeilingReached(route)
|
|
1671
|
+
},
|
|
1516
1672
|
// The daemon's own list — the whole point of Rev 1 §B.
|
|
1517
1673
|
locallyAllows: this.#options.allowlist.predicateFor(
|
|
1518
1674
|
this.#options.client.origin
|
|
@@ -1599,6 +1755,17 @@ var Runner = class {
|
|
|
1599
1755
|
outputChars: result.ok ? result.text.length : 0,
|
|
1600
1756
|
...result.ok ? {} : { detail: result.message }
|
|
1601
1757
|
});
|
|
1758
|
+
if (community && route.cost === "metered") {
|
|
1759
|
+
await this.#options.spend.record(
|
|
1760
|
+
route.backendKey,
|
|
1761
|
+
estimateCents(
|
|
1762
|
+
prompt.length,
|
|
1763
|
+
result.ok ? result.text.length : 0,
|
|
1764
|
+
route.spendCentsPerMillionTokens
|
|
1765
|
+
),
|
|
1766
|
+
this.#now()
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1602
1769
|
this.#completed += 1;
|
|
1603
1770
|
this.#options.onEvent?.({
|
|
1604
1771
|
type: "finished",
|
|
@@ -1791,6 +1958,7 @@ var USAGE = `byollm \u2014 run an app's LLM jobs on your own models.
|
|
|
1791
1958
|
byollm resume start claiming again
|
|
1792
1959
|
byollm allow <url> <user> let someone else's jobs run here (named audience)
|
|
1793
1960
|
byollm allow --list who can currently use this machine
|
|
1961
|
+
byollm offer <backend> <scope> who a backend is offered to (self|named|public)
|
|
1794
1962
|
byollm disallow <url> <user>
|
|
1795
1963
|
byollm forget <url> drop a pairing
|
|
1796
1964
|
byollm backends what is installed, healthy, and advertised
|
|
@@ -1836,6 +2004,8 @@ async function runCli(argv, options = {}) {
|
|
|
1836
2004
|
return commandAllow(paths, rest, io);
|
|
1837
2005
|
case "disallow":
|
|
1838
2006
|
return commandDisallow(paths, rest, io);
|
|
2007
|
+
case "offer":
|
|
2008
|
+
return commandOffer(paths, rest, io);
|
|
1839
2009
|
case "forget":
|
|
1840
2010
|
return commandForget(paths, rest, io);
|
|
1841
2011
|
case "backends":
|
|
@@ -1854,7 +2024,7 @@ async function commandConnect(paths, args, io, signal) {
|
|
|
1854
2024
|
return 2;
|
|
1855
2025
|
}
|
|
1856
2026
|
const origin = normalizeOrigin(target);
|
|
1857
|
-
const { loaded, ingress, allowlist, budgets } = await context(paths);
|
|
2027
|
+
const { loaded, ingress, allowlist, budgets, spend } = await context(paths);
|
|
1858
2028
|
for (const problem of loaded.problems) {
|
|
1859
2029
|
io.err(`config: ${problem.where}: ${problem.message}
|
|
1860
2030
|
`);
|
|
@@ -1868,6 +2038,7 @@ async function commandConnect(paths, args, io, signal) {
|
|
|
1868
2038
|
loaded,
|
|
1869
2039
|
allowlist,
|
|
1870
2040
|
budgets,
|
|
2041
|
+
spend,
|
|
1871
2042
|
ingress
|
|
1872
2043
|
});
|
|
1873
2044
|
const capabilities = await runner.detectCapabilities();
|
|
@@ -1934,7 +2105,7 @@ async function commandRun(paths, args, io, signal) {
|
|
|
1934
2105
|
return runLoop(paths, origins, io, signal);
|
|
1935
2106
|
}
|
|
1936
2107
|
async function runLoop(paths, origins, io, signal) {
|
|
1937
|
-
const { loaded, ingress, allowlist, budgets } = await context(paths);
|
|
2108
|
+
const { loaded, ingress, allowlist, budgets, spend } = await context(paths);
|
|
1938
2109
|
const pairings = new Pairings(paths.pairings);
|
|
1939
2110
|
await pairings.load();
|
|
1940
2111
|
const controller = new AbortController();
|
|
@@ -1961,6 +2132,7 @@ async function runLoop(paths, origins, io, signal) {
|
|
|
1961
2132
|
loaded,
|
|
1962
2133
|
allowlist,
|
|
1963
2134
|
budgets,
|
|
2135
|
+
spend,
|
|
1964
2136
|
ingress,
|
|
1965
2137
|
onEvent: (event) => {
|
|
1966
2138
|
report(origin, event, io);
|
|
@@ -2017,7 +2189,7 @@ function report(origin, event, io) {
|
|
|
2017
2189
|
}
|
|
2018
2190
|
}
|
|
2019
2191
|
async function commandStatus(paths, io) {
|
|
2020
|
-
const { loaded, ingress, allowlist, budgets } = await context(paths);
|
|
2192
|
+
const { loaded, ingress, allowlist, budgets, spend } = await context(paths);
|
|
2021
2193
|
const pairings = new Pairings(paths.pairings);
|
|
2022
2194
|
await pairings.load();
|
|
2023
2195
|
const paused = await isPaused(paths);
|
|
@@ -2063,6 +2235,19 @@ async function commandStatus(paths, io) {
|
|
|
2063
2235
|
`
|
|
2064
2236
|
);
|
|
2065
2237
|
}
|
|
2238
|
+
const spentToday = spend.summary(now);
|
|
2239
|
+
const metered = loaded.routes.filter((r) => r.cost === "metered");
|
|
2240
|
+
if (metered.length > 0) {
|
|
2241
|
+
io.out("\nmetered backends \u2014 your money\n");
|
|
2242
|
+
for (const route of metered) {
|
|
2243
|
+
const spent = spentToday[route.backendKey] ?? 0;
|
|
2244
|
+
io.out(
|
|
2245
|
+
route.spendAcknowledged ? ` ${route.backendKey}: ${spent.toFixed(1)}c spent today of ${String(route.spendDailyCapCents ?? 0)}c
|
|
2246
|
+
` : ` ${route.backendKey}: not shared \u2014 your work only
|
|
2247
|
+
`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2066
2251
|
const usage = budgets.usage(now);
|
|
2067
2252
|
io.out("\ncommunity work done for others\n");
|
|
2068
2253
|
io.out(
|
|
@@ -2134,9 +2319,9 @@ function indent(text) {
|
|
|
2134
2319
|
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
2135
2320
|
}
|
|
2136
2321
|
async function commandPause(paths, pause, io) {
|
|
2137
|
-
await
|
|
2322
|
+
await mkdir6(paths.root, { recursive: true });
|
|
2138
2323
|
if (pause) {
|
|
2139
|
-
await
|
|
2324
|
+
await writeFile6(paths.pauseFlag, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
2140
2325
|
`);
|
|
2141
2326
|
io.out(
|
|
2142
2327
|
"paused \u2014 no new work will be claimed. `byollm resume` to start again.\n"
|
|
@@ -2204,6 +2389,129 @@ Allow ${owner} to use this machine?`
|
|
|
2204
2389
|
`);
|
|
2205
2390
|
return 0;
|
|
2206
2391
|
}
|
|
2392
|
+
async function commandOffer(paths, args, io) {
|
|
2393
|
+
const [backendKey, scope, ...rest] = args;
|
|
2394
|
+
if (backendKey === void 0 || scope === void 0) {
|
|
2395
|
+
io.err(
|
|
2396
|
+
"usage: byollm offer <backend> <self|named|public> [--cap <cents>]\n"
|
|
2397
|
+
);
|
|
2398
|
+
return 2;
|
|
2399
|
+
}
|
|
2400
|
+
if (scope !== "self" && scope !== "named" && scope !== "public") {
|
|
2401
|
+
io.err(`"${scope}" is not an offer scope \u2014 use self, named, or public
|
|
2402
|
+
`);
|
|
2403
|
+
return 2;
|
|
2404
|
+
}
|
|
2405
|
+
const capIndex = rest.indexOf("--cap");
|
|
2406
|
+
let capCents;
|
|
2407
|
+
if (capIndex !== -1) {
|
|
2408
|
+
const raw2 = rest[capIndex + 1];
|
|
2409
|
+
const parsed2 = Number(raw2);
|
|
2410
|
+
if (raw2 === void 0 || !Number.isFinite(parsed2) || parsed2 <= 0) {
|
|
2411
|
+
io.err("--cap takes a number of cents per day, greater than zero\n");
|
|
2412
|
+
return 2;
|
|
2413
|
+
}
|
|
2414
|
+
capCents = Math.round(parsed2);
|
|
2415
|
+
}
|
|
2416
|
+
let raw;
|
|
2417
|
+
try {
|
|
2418
|
+
raw = await readFile7(paths.config, "utf8");
|
|
2419
|
+
} catch {
|
|
2420
|
+
io.err(
|
|
2421
|
+
`no config at ${paths.config} \u2014 nothing to offer yet.
|
|
2422
|
+
Write one, or run \`byollm connect <url>\` first.
|
|
2423
|
+
`
|
|
2424
|
+
);
|
|
2425
|
+
return 1;
|
|
2426
|
+
}
|
|
2427
|
+
let parsed;
|
|
2428
|
+
try {
|
|
2429
|
+
parsed = JSON.parse(raw);
|
|
2430
|
+
} catch {
|
|
2431
|
+
io.err(`${paths.config} is not valid JSON \u2014 fix it by hand first
|
|
2432
|
+
`);
|
|
2433
|
+
return 1;
|
|
2434
|
+
}
|
|
2435
|
+
const result = DaemonConfig.safeParse(parsed);
|
|
2436
|
+
if (!result.success) {
|
|
2437
|
+
io.err(`${paths.config} is not a valid byollm config \u2014 fix it first
|
|
2438
|
+
`);
|
|
2439
|
+
return 1;
|
|
2440
|
+
}
|
|
2441
|
+
const backend = result.data.backends[backendKey];
|
|
2442
|
+
if (!backend) {
|
|
2443
|
+
const known = Object.keys(result.data.backends);
|
|
2444
|
+
io.err(
|
|
2445
|
+
`no backend named "${backendKey}" in ${paths.config}
|
|
2446
|
+
` + (known.length > 0 ? `configured: ${known.join(", ")}
|
|
2447
|
+
` : "")
|
|
2448
|
+
);
|
|
2449
|
+
return 2;
|
|
2450
|
+
}
|
|
2451
|
+
const descriptor = backendDescriptor3(backend.backend);
|
|
2452
|
+
const baseUrl = backend.baseUrl ?? descriptor.defaultBaseUrl;
|
|
2453
|
+
const cost = resolveCost2(backend.backend, baseUrl);
|
|
2454
|
+
const widening = scope !== "self";
|
|
2455
|
+
if (cost === "subscription" && widening) {
|
|
2456
|
+
io.err(
|
|
2457
|
+
`${descriptor.label} runs on your own subscription, whose terms cover
|
|
2458
|
+
your work and nobody else's. It cannot be offered to other people.
|
|
2459
|
+
`
|
|
2460
|
+
);
|
|
2461
|
+
return 1;
|
|
2462
|
+
}
|
|
2463
|
+
if (cost === "metered" && widening) {
|
|
2464
|
+
const cap = capCents ?? backend.spend?.dailyCapCents;
|
|
2465
|
+
if (cap === void 0) {
|
|
2466
|
+
io.err(
|
|
2467
|
+
`${descriptor.label} bills you per token, so sharing it needs a daily
|
|
2468
|
+
ceiling. Add one: \`byollm offer ${backendKey} ${scope} --cap <cents>\`
|
|
2469
|
+
`
|
|
2470
|
+
);
|
|
2471
|
+
return 2;
|
|
2472
|
+
}
|
|
2473
|
+
const dollars = (cap / 100).toFixed(2);
|
|
2474
|
+
const confirmed = await io.confirm(
|
|
2475
|
+
`
|
|
2476
|
+
This lets other people's jobs run on ${descriptor.label}, which bills
|
|
2477
|
+
your account per token. You would be paying for their work, up to
|
|
2478
|
+
$${dollars} a day, every day, until you change it.
|
|
2479
|
+
Spending stops at that ceiling and resumes the next day.
|
|
2480
|
+
|
|
2481
|
+
Offer ${backendKey} to ${scope === "public" ? "anyone" : "people you have allowed"}?`
|
|
2482
|
+
);
|
|
2483
|
+
if (!confirmed) {
|
|
2484
|
+
io.out("nothing changed\n");
|
|
2485
|
+
return 0;
|
|
2486
|
+
}
|
|
2487
|
+
result.data.backends[backendKey] = {
|
|
2488
|
+
...backend,
|
|
2489
|
+
offer: scope,
|
|
2490
|
+
spend: {
|
|
2491
|
+
centsPerMillionTokens: backend.spend?.centsPerMillionTokens ?? 1500,
|
|
2492
|
+
...backend.spend,
|
|
2493
|
+
acknowledged: true,
|
|
2494
|
+
dailyCapCents: cap
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
} else {
|
|
2498
|
+
result.data.backends[backendKey] = {
|
|
2499
|
+
...backend,
|
|
2500
|
+
offer: scope,
|
|
2501
|
+
// Narrowing back to `self` withdraws the consent too, so a later
|
|
2502
|
+
// widening has to be agreed to again rather than inherited.
|
|
2503
|
+
...cost === "metered" && !widening && backend.spend !== void 0 ? { spend: { ...backend.spend, acknowledged: false } } : {}
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
await mkdir6(dirname6(paths.config), { recursive: true });
|
|
2507
|
+
await writeFile6(paths.config, `${JSON.stringify(result.data, null, 2)}
|
|
2508
|
+
`);
|
|
2509
|
+
const written = result.data.backends[backendKey].spend?.dailyCapCents;
|
|
2510
|
+
io.out(
|
|
2511
|
+
`${backendKey} is now offered to ${scope}` + (written === void 0 || !widening ? "" : `, capped at ${String(written)}c a day`) + "\n"
|
|
2512
|
+
);
|
|
2513
|
+
return 0;
|
|
2514
|
+
}
|
|
2207
2515
|
async function commandDisallow(paths, args, io) {
|
|
2208
2516
|
const [rawOrigin, owner] = args;
|
|
2209
2517
|
if (rawOrigin === void 0 || owner === void 0) {
|
|
@@ -2237,7 +2545,7 @@ async function commandForget(paths, args, io) {
|
|
|
2237
2545
|
return 0;
|
|
2238
2546
|
}
|
|
2239
2547
|
async function commandBackends(paths, io) {
|
|
2240
|
-
const { loaded, ingress, allowlist, budgets } = await context(paths);
|
|
2548
|
+
const { loaded, ingress, allowlist, budgets, spend } = await context(paths);
|
|
2241
2549
|
const runner = new Runner({
|
|
2242
2550
|
client: new ProtocolClient({ origin: "https://unused.invalid" }),
|
|
2243
2551
|
runnerId: "local",
|
|
@@ -2246,6 +2554,7 @@ async function commandBackends(paths, io) {
|
|
|
2246
2554
|
loaded,
|
|
2247
2555
|
allowlist,
|
|
2248
2556
|
budgets,
|
|
2557
|
+
spend,
|
|
2249
2558
|
ingress
|
|
2250
2559
|
});
|
|
2251
2560
|
const advertised = await runner.detectCapabilities();
|
|
@@ -2253,8 +2562,10 @@ async function commandBackends(paths, io) {
|
|
|
2253
2562
|
io.out("configured routes\n");
|
|
2254
2563
|
for (const route of loaded.routes) {
|
|
2255
2564
|
const ok = advertisedKinds.has(route.kind);
|
|
2565
|
+
const pays = route.cost === "free" ? "free (your electricity)" : route.cost === "subscription" ? "your subscription \u2014 locked to your work" : route.spendAcknowledged ? `metered \u2014 shared, cap ${String(route.spendDailyCapCents ?? 0)}c/day` : "metered \u2014 your money, not shared";
|
|
2256
2566
|
io.out(
|
|
2257
2567
|
` ${ok ? "\u2713" : "\u2717"} ${route.kind.padEnd(14)} ${route.backendId}:${route.model}${route.baseUrl === void 0 ? "" : ` @ ${route.baseUrl}`}
|
|
2568
|
+
${pays}
|
|
2258
2569
|
`
|
|
2259
2570
|
);
|
|
2260
2571
|
}
|
|
@@ -2282,7 +2593,9 @@ async function context(paths) {
|
|
|
2282
2593
|
await allowlist.load();
|
|
2283
2594
|
const budgets = new Budgets(paths.budgets, loaded.config.community);
|
|
2284
2595
|
await budgets.load(Date.now());
|
|
2285
|
-
|
|
2596
|
+
const spend = new SpendLedger(paths.spend);
|
|
2597
|
+
await spend.load(Date.now());
|
|
2598
|
+
return { loaded, ingress, allowlist, budgets, spend };
|
|
2286
2599
|
}
|
|
2287
2600
|
async function confirmInteractively(question) {
|
|
2288
2601
|
if (!process.stdin.isTTY) {
|
|
@@ -2321,7 +2634,7 @@ async function main(argv) {
|
|
|
2321
2634
|
}
|
|
2322
2635
|
|
|
2323
2636
|
// src/index.ts
|
|
2324
|
-
var DAEMON_VERSION = "0.1.0-alpha.
|
|
2637
|
+
var DAEMON_VERSION = "0.1.0-alpha.3";
|
|
2325
2638
|
|
|
2326
2639
|
export {
|
|
2327
2640
|
Allowlist,
|
|
@@ -2341,6 +2654,8 @@ export {
|
|
|
2341
2654
|
hashText,
|
|
2342
2655
|
stripControlChars,
|
|
2343
2656
|
Pairings,
|
|
2657
|
+
SpendLedger,
|
|
2658
|
+
estimateCents,
|
|
2344
2659
|
daemonPaths,
|
|
2345
2660
|
defaultRoot,
|
|
2346
2661
|
childEnv,
|
|
@@ -2355,4 +2670,4 @@ export {
|
|
|
2355
2670
|
runCli,
|
|
2356
2671
|
main
|
|
2357
2672
|
};
|
|
2358
|
-
//# sourceMappingURL=chunk-
|
|
2673
|
+
//# sourceMappingURL=chunk-2W2ZIXAS.js.map
|