@bytesbrains/cruise 0.0.0-stage → 0.12.1
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/LICENSE +202 -0
- package/README.md +59 -2
- package/dist/budget-FIRNSN67.mjs +132 -0
- package/dist/chunk-2FB2T3UI.mjs +122 -0
- package/dist/chunk-2NVDAK5J.mjs +76 -0
- package/dist/chunk-42ERHAJD.mjs +79 -0
- package/dist/chunk-4447IUTF.mjs +7 -0
- package/dist/chunk-5TVD3ZAI.mjs +69 -0
- package/dist/chunk-7NQHBXVZ.mjs +104 -0
- package/dist/chunk-BNP63KB2.mjs +42 -0
- package/dist/chunk-FGLMW5IY.mjs +575 -0
- package/dist/chunk-GUZNI3NS.mjs +267 -0
- package/dist/chunk-HQDQTXRX.mjs +57 -0
- package/dist/chunk-ICJVJSTC.mjs +42 -0
- package/dist/chunk-JZPUTXM7.mjs +236 -0
- package/dist/chunk-LALZUKEC.mjs +137 -0
- package/dist/chunk-N3YKWKOA.mjs +9 -0
- package/dist/chunk-O7TNRKSA.mjs +2 -0
- package/dist/chunk-RT4R2W6H.mjs +21 -0
- package/dist/chunk-SYTYOTO4.mjs +18 -0
- package/dist/chunk-T3ICD43U.mjs +15 -0
- package/dist/chunk-THEXUHQC.mjs +98 -0
- package/dist/chunk-UNXM6FSM.mjs +83 -0
- package/dist/chunk-WS6NFRIU.mjs +66 -0
- package/dist/chunk-XOH3XKM3.mjs +126 -0
- package/dist/compress-3DWG35BY.mjs +49 -0
- package/dist/conformance-NPFDYCQL.mjs +922 -0
- package/dist/cruise.mjs +693 -0
- package/dist/fallback-P5KK55M4.mjs +139 -0
- package/dist/keys-FWPQPMTW.mjs +127 -0
- package/dist/ledger-XBGWXY6C.mjs +97 -0
- package/dist/member-ZBV4YG2M.mjs +74 -0
- package/dist/report-VRYJ3W5L.mjs +414 -0
- package/dist/service-key-N2RZ7FMZ.mjs +59 -0
- package/dist/smoke-ZNYX6RO5.mjs +105 -0
- package/dist/sweep-SZX7OIDK.mjs +33 -0
- package/dist/tenant-4YDGI23Q.mjs +122 -0
- package/dist/tier-4L3SPLFP.mjs +116 -0
- package/package.json +38 -4
- package/templates/docker-compose.yml +66 -0
- package/templates/env.example +44 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
import {
|
|
4
|
+
MAX_CHAIN
|
|
5
|
+
} from "./chunk-FGLMW5IY.mjs";
|
|
6
|
+
import "./chunk-4447IUTF.mjs";
|
|
7
|
+
import "./chunk-XOH3XKM3.mjs";
|
|
8
|
+
import "./chunk-42ERHAJD.mjs";
|
|
9
|
+
import "./chunk-BNP63KB2.mjs";
|
|
10
|
+
import "./chunk-ICJVJSTC.mjs";
|
|
11
|
+
import "./chunk-UNXM6FSM.mjs";
|
|
12
|
+
import "./chunk-SYTYOTO4.mjs";
|
|
13
|
+
import "./chunk-O7TNRKSA.mjs";
|
|
14
|
+
import "./chunk-T3ICD43U.mjs";
|
|
15
|
+
import "./chunk-RT4R2W6H.mjs";
|
|
16
|
+
import "./chunk-THEXUHQC.mjs";
|
|
17
|
+
import {
|
|
18
|
+
lookupModel
|
|
19
|
+
} from "./chunk-LALZUKEC.mjs";
|
|
20
|
+
import "./chunk-5TVD3ZAI.mjs";
|
|
21
|
+
import {
|
|
22
|
+
connect,
|
|
23
|
+
environmentOf,
|
|
24
|
+
parseArgs,
|
|
25
|
+
requireString,
|
|
26
|
+
run
|
|
27
|
+
} from "./chunk-7NQHBXVZ.mjs";
|
|
28
|
+
import "./chunk-JZPUTXM7.mjs";
|
|
29
|
+
|
|
30
|
+
// src/fallbacks-write.ts
|
|
31
|
+
async function setFallbacks(db, modelId, fallbacks, actor) {
|
|
32
|
+
const primary = await lookupModel(db, modelId);
|
|
33
|
+
if (primary === null || !primary.enabled) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
refusal: { code: "model_not_found", message: `The model '${modelId}' does not exist or is not enabled.` }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const invalid = await validate(db, primary, fallbacks);
|
|
40
|
+
if (invalid !== null) return { ok: false, refusal: { code: "invalid_chain", message: invalid } };
|
|
41
|
+
const statements = [
|
|
42
|
+
db.prepare(`DELETE FROM model_fallbacks WHERE model = ?`).bind(modelId),
|
|
43
|
+
...fallbacks.map(
|
|
44
|
+
(fallback, index) => db.prepare(`INSERT INTO model_fallbacks (model, position, fallback_model) VALUES (?, ?, ?)`).bind(modelId, index + 1, fallback)
|
|
45
|
+
),
|
|
46
|
+
db.prepare(
|
|
47
|
+
`INSERT INTO audit_events (id, actor, action, subject, detail) VALUES (?, ?, 'model.fallbacks', ?, ?)`
|
|
48
|
+
).bind(crypto.randomUUID(), actor, modelId, JSON.stringify({ fallbacks }))
|
|
49
|
+
];
|
|
50
|
+
await db.batch(statements);
|
|
51
|
+
return { ok: true, model: modelId, fallbacks: [...fallbacks], warnings: await warn(db, primary, fallbacks) };
|
|
52
|
+
}
|
|
53
|
+
async function validate(db, primary, fallbacks) {
|
|
54
|
+
if (fallbacks.length + 1 > MAX_CHAIN) {
|
|
55
|
+
return `A chain holds at most ${MAX_CHAIN} models including the primary, so at most ${MAX_CHAIN - 1} fallback(s); ${fallbacks.length} were sent. Candidates are tried in turn against the full upstream deadline, so a long chain is a long wait rather than more redundancy.`;
|
|
56
|
+
}
|
|
57
|
+
if (new Set(fallbacks).size !== fallbacks.length) {
|
|
58
|
+
return `A model appears twice in the chain. Trying it again after it has already failed spends a deadline to learn nothing.`;
|
|
59
|
+
}
|
|
60
|
+
if (fallbacks.includes(primary.id)) {
|
|
61
|
+
return `'${primary.id}' cannot fall back to itself.`;
|
|
62
|
+
}
|
|
63
|
+
for (const id of fallbacks) {
|
|
64
|
+
const candidate = await lookupModel(db, id);
|
|
65
|
+
if (candidate === null || !candidate.enabled) {
|
|
66
|
+
return `The fallback '${id}' does not exist or is not enabled.`;
|
|
67
|
+
}
|
|
68
|
+
if (candidate.modality !== primary.modality) {
|
|
69
|
+
return `'${id}' is a ${candidate.modality} model and '${primary.id}' is a ${primary.modality} model. A chain crosses models, never modalities: the request path would reject it on every request and the chain would be redundancy in name only.`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
async function warn(db, primary, fallbacks) {
|
|
75
|
+
const warnings = [];
|
|
76
|
+
if (fallbacks.length === 0) {
|
|
77
|
+
warnings.push(
|
|
78
|
+
`'${primary.id}' now has no fallback. A provider's 429, 5xx or expired credential on it will reach the caller.`
|
|
79
|
+
);
|
|
80
|
+
return warnings;
|
|
81
|
+
}
|
|
82
|
+
const sameUpstream = [];
|
|
83
|
+
for (const id of fallbacks) {
|
|
84
|
+
const candidate = await lookupModel(db, id);
|
|
85
|
+
if (candidate !== null && candidate.upstreamId === primary.upstreamId) sameUpstream.push(id);
|
|
86
|
+
}
|
|
87
|
+
if (sameUpstream.length === fallbacks.length) {
|
|
88
|
+
warnings.push(
|
|
89
|
+
`Every fallback is on '${primary.upstreamId}', the primary's own upstream. Where that host rate-limits per organisation rather than per model, the chain answers the same 429 a deadline later.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return warnings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// scripts/fallback.ts
|
|
96
|
+
run(async () => {
|
|
97
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
98
|
+
const args = parseArgs(rest);
|
|
99
|
+
const db = connect(environmentOf(args));
|
|
100
|
+
switch (command) {
|
|
101
|
+
case "set": {
|
|
102
|
+
const model = requireString(args, "model");
|
|
103
|
+
const actor = requireString(args, "actor");
|
|
104
|
+
const to = requireString(args, "to");
|
|
105
|
+
const fallbacks = to.split(",").map((id) => id.trim()).filter((id) => id !== "");
|
|
106
|
+
const result = await setFallbacks(db, model, fallbacks, actor);
|
|
107
|
+
if (!result.ok) throw new Error(result.refusal.message);
|
|
108
|
+
console.log(
|
|
109
|
+
result.fallbacks.length === 0 ? `${model}: chain cleared.` : `${model} \u2192 ${result.fallbacks.join(" \u2192 ")}`
|
|
110
|
+
);
|
|
111
|
+
for (const warning of result.warnings) console.log(` \u26A0 ${warning}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
case "show":
|
|
115
|
+
default: {
|
|
116
|
+
const { results } = await db.prepare(
|
|
117
|
+
`SELECT m.id,
|
|
118
|
+
COALESCE(group_concat(f.fallback_model, ' \u2192 '), '') AS chain
|
|
119
|
+
FROM models m
|
|
120
|
+
JOIN upstreams u ON u.id = m.upstream_id
|
|
121
|
+
LEFT JOIN (SELECT model, fallback_model FROM model_fallbacks ORDER BY position) f
|
|
122
|
+
ON f.model = m.id
|
|
123
|
+
WHERE m.enabled = 1 AND u.enabled = 1
|
|
124
|
+
GROUP BY m.id
|
|
125
|
+
ORDER BY m.id`
|
|
126
|
+
).all();
|
|
127
|
+
const without = results.filter((row) => row.chain === "");
|
|
128
|
+
for (const row of results) {
|
|
129
|
+
console.log(`${row.id.padEnd(55)} ${row.chain || "\u2014"}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(`
|
|
132
|
+
${results.length - without.length} of ${results.length} model(s) have a fallback chain.`);
|
|
133
|
+
if (without.length > 0) {
|
|
134
|
+
console.log(`A model with none answers its provider's 429, 5xx or expired credential straight to the caller (#237).`);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
import {
|
|
4
|
+
listFlag
|
|
5
|
+
} from "./chunk-WS6NFRIU.mjs";
|
|
6
|
+
import {
|
|
7
|
+
issueKey,
|
|
8
|
+
rateLimitProblem,
|
|
9
|
+
revokeKey
|
|
10
|
+
} from "./chunk-2FB2T3UI.mjs";
|
|
11
|
+
import {
|
|
12
|
+
CACHE_POOLS,
|
|
13
|
+
DEFAULT_CACHE_POOL,
|
|
14
|
+
isCachePool
|
|
15
|
+
} from "./chunk-T3ICD43U.mjs";
|
|
16
|
+
import "./chunk-LALZUKEC.mjs";
|
|
17
|
+
import {
|
|
18
|
+
keyEnvironmentFor
|
|
19
|
+
} from "./chunk-5TVD3ZAI.mjs";
|
|
20
|
+
import {
|
|
21
|
+
connect,
|
|
22
|
+
environmentOf,
|
|
23
|
+
optionalNumber,
|
|
24
|
+
parseArgs,
|
|
25
|
+
requireString,
|
|
26
|
+
run
|
|
27
|
+
} from "./chunk-7NQHBXVZ.mjs";
|
|
28
|
+
import "./chunk-JZPUTXM7.mjs";
|
|
29
|
+
|
|
30
|
+
// scripts/keys.ts
|
|
31
|
+
run(async () => {
|
|
32
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
33
|
+
const args = parseArgs(rest);
|
|
34
|
+
const environment = environmentOf(args);
|
|
35
|
+
const db = connect(environment);
|
|
36
|
+
switch (command) {
|
|
37
|
+
case "issue": {
|
|
38
|
+
const project = requireString(args, "project");
|
|
39
|
+
const allowedModels = listFlag(args.models, "models") ?? [];
|
|
40
|
+
const allowedClasses = listFlag(args.classes, "classes") ?? [];
|
|
41
|
+
const cachePool = args["cache-pool"] === void 0 ? DEFAULT_CACHE_POOL : args["cache-pool"];
|
|
42
|
+
if (!isCachePool(cachePool)) {
|
|
43
|
+
throw new Error(`--cache-pool must be one of: ${CACHE_POOLS.join(", ")}. Leave it out for '${DEFAULT_CACHE_POOL}'.`);
|
|
44
|
+
}
|
|
45
|
+
const rateLimitPerMin = optionalNumber(args, "rate-limit");
|
|
46
|
+
const accountRateLimitPerMin = optionalNumber(args, "account-rate-limit");
|
|
47
|
+
for (const [what, value] of [
|
|
48
|
+
["--rate-limit", rateLimitPerMin],
|
|
49
|
+
["--account-rate-limit", accountRateLimitPerMin]
|
|
50
|
+
]) {
|
|
51
|
+
if (args[what.slice(2)] === void 0) continue;
|
|
52
|
+
const problem = rateLimitProblem(value, what);
|
|
53
|
+
if (problem !== null) throw new Error(problem);
|
|
54
|
+
}
|
|
55
|
+
const owner = await db.prepare(`SELECT tenant_id FROM projects WHERE id = ?`).bind(project).first();
|
|
56
|
+
if (owner === null) throw new Error(`No such project: ${project}`);
|
|
57
|
+
const issued = await issueKey(db, {
|
|
58
|
+
projectId: project,
|
|
59
|
+
tenantId: owner.tenant_id,
|
|
60
|
+
// The same answer the route gives (#326): production issues `live`,
|
|
61
|
+
// the demo `demo`, and anything else `test`. This line was the binary
|
|
62
|
+
// `keyEnvironmentFor` replaced in `admin-keys.ts`, still here — so
|
|
63
|
+
// `key:issue --env demo` minted a `cru_test_` key into the demo's
|
|
64
|
+
// database, contradicting the rule `openapi.ts` publishes (#328).
|
|
65
|
+
environment: keyEnvironmentFor(environment),
|
|
66
|
+
actor: requireString(args, "actor"),
|
|
67
|
+
...typeof args.name === "string" ? { name: args.name } : {},
|
|
68
|
+
allowedModels,
|
|
69
|
+
allowedClasses,
|
|
70
|
+
rateLimitPerMin,
|
|
71
|
+
accountRateLimitPerMin,
|
|
72
|
+
conformance: args.conformance === true,
|
|
73
|
+
cachePool
|
|
74
|
+
});
|
|
75
|
+
const scope = (entries) => entries.length === 0 ? "unrestricted" : entries.join(", ");
|
|
76
|
+
console.log(`
|
|
77
|
+
environment ${environment}`);
|
|
78
|
+
console.log(` project ${project}`);
|
|
79
|
+
console.log(` models ${scope(allowedModels)}`);
|
|
80
|
+
console.log(` classes ${scope(allowedClasses)}`);
|
|
81
|
+
console.log(` cache pool ${cachePool}`);
|
|
82
|
+
const perMin = (n) => n === null ? "none" : `${n}/min`;
|
|
83
|
+
console.log(` rate limit ${perMin(issued.rateLimitPerMin)}`);
|
|
84
|
+
console.log(` acct limit ${perMin(issued.accountRateLimitPerMin)}`);
|
|
85
|
+
console.log(` prefix ${issued.prefix}`);
|
|
86
|
+
console.log(`
|
|
87
|
+
${issued.key}
|
|
88
|
+
`);
|
|
89
|
+
console.log(" Shown once. It is not stored and cannot be recovered \u2014 reissue instead.\n");
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "revoke": {
|
|
93
|
+
const prefix = requireString(args, "prefix");
|
|
94
|
+
const owner = await db.prepare(`SELECT p.tenant_id FROM virtual_keys k JOIN projects p ON p.id = k.project_id WHERE k.prefix = ?`).bind(prefix).first();
|
|
95
|
+
const changed = owner !== null && await revokeKey(db, {
|
|
96
|
+
tenantId: owner.tenant_id,
|
|
97
|
+
prefix,
|
|
98
|
+
actor: requireString(args, "actor"),
|
|
99
|
+
reason: requireString(args, "reason")
|
|
100
|
+
});
|
|
101
|
+
console.log(changed ? `revoked ${prefix}` : `no change \u2014 ${prefix} is unknown or already revoked`);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case "list": {
|
|
105
|
+
const { results } = await db.prepare(
|
|
106
|
+
// The limit beside the pool, for the same reason the HTTP listing
|
|
107
|
+
// carries it: on the demo every key holds one nobody typed (#328),
|
|
108
|
+
// and a listing that omits it is an audit that cannot see the one
|
|
109
|
+
// field the deployment set. Raised in review of PR #338.
|
|
110
|
+
`SELECT k.prefix, p.slug, k.name, k.created_at, k.revoked_at, k.cache_pool, k.rate_limit_per_min
|
|
111
|
+
FROM virtual_keys k JOIN projects p ON p.id = k.project_id
|
|
112
|
+
ORDER BY k.created_at DESC`
|
|
113
|
+
).all();
|
|
114
|
+
if (results.length === 0) console.log("no keys issued");
|
|
115
|
+
for (const row of results) {
|
|
116
|
+
const limit = row.rate_limit_per_min === null ? "none" : `${row.rate_limit_per_min}/min`;
|
|
117
|
+
console.log(
|
|
118
|
+
`${row.prefix} ${row.slug.padEnd(8)} ${(row.revoked_at ? "revoked" : "active").padEnd(8)} ${row.cache_pool.padEnd(8)} ${limit.padEnd(8)} ${row.name ?? ""}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
default:
|
|
124
|
+
console.error("usage: issue | revoke | list (see scripts/keys.ts)");
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
import {
|
|
4
|
+
compressionBySession,
|
|
5
|
+
compressionSavings,
|
|
6
|
+
savedShare
|
|
7
|
+
} from "./chunk-HQDQTXRX.mjs";
|
|
8
|
+
import {
|
|
9
|
+
effectiveRates,
|
|
10
|
+
spendBy
|
|
11
|
+
} from "./chunk-XOH3XKM3.mjs";
|
|
12
|
+
import "./chunk-BNP63KB2.mjs";
|
|
13
|
+
import "./chunk-SYTYOTO4.mjs";
|
|
14
|
+
import "./chunk-O7TNRKSA.mjs";
|
|
15
|
+
import "./chunk-T3ICD43U.mjs";
|
|
16
|
+
import {
|
|
17
|
+
formatUsd
|
|
18
|
+
} from "./chunk-THEXUHQC.mjs";
|
|
19
|
+
import "./chunk-5TVD3ZAI.mjs";
|
|
20
|
+
import {
|
|
21
|
+
connect,
|
|
22
|
+
environmentOf,
|
|
23
|
+
parseArgs,
|
|
24
|
+
run
|
|
25
|
+
} from "./chunk-7NQHBXVZ.mjs";
|
|
26
|
+
import "./chunk-JZPUTXM7.mjs";
|
|
27
|
+
|
|
28
|
+
// scripts/ledger.ts
|
|
29
|
+
run(async () => {
|
|
30
|
+
const args = parseArgs(process.argv.slice(2));
|
|
31
|
+
const db = connect(environmentOf(args));
|
|
32
|
+
const month = typeof args.month === "string" ? args.month : (/* @__PURE__ */ new Date()).toISOString().slice(0, 7);
|
|
33
|
+
if (args.rates === true) {
|
|
34
|
+
const rows2 = await effectiveRates(db, month);
|
|
35
|
+
if (rows2.length === 0) return console.log(`no priced requests in ${month}`);
|
|
36
|
+
console.log(`effective rate vs table rate, ${month}
|
|
37
|
+
`);
|
|
38
|
+
console.log("model reqs observed/Mtok table in/Mtok");
|
|
39
|
+
for (const r of rows2) {
|
|
40
|
+
const observed = r.observedInputMicrosPerMtok;
|
|
41
|
+
const table = r.tableInputMicrosPerMtok;
|
|
42
|
+
const drift = observed !== null && table !== null && table > 0 ? ` (${((observed - table) / table * 100).toFixed(0)}%)` : "";
|
|
43
|
+
console.log(
|
|
44
|
+
`${r.model.slice(0, 40).padEnd(42)} ${String(r.requests).padStart(4)} ${(observed === null ? "-" : formatUsd(observed, 4)).padStart(13)} ${(table === null ? "-" : formatUsd(table, 4)).padStart(13)}${drift}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
console.log("\nObserved blends input and output, so it is not comparable to the table rate");
|
|
48
|
+
console.log("directly \u2014 a change in the ratio over time is the signal, not the level.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (typeof args.session === "string") {
|
|
52
|
+
const rows2 = await compressionBySession(db, args.session);
|
|
53
|
+
if (rows2.length === 0) return console.log(`no ledger entries for session ${args.session}`);
|
|
54
|
+
console.log(`session ${args.session}
|
|
55
|
+
`);
|
|
56
|
+
console.log("project reqs ran applied before after saved spend");
|
|
57
|
+
for (const r of rows2) {
|
|
58
|
+
const saved = savedShare(r);
|
|
59
|
+
console.log(
|
|
60
|
+
`${r.projectId.slice(0, 42).padEnd(44)}${String(r.requests).padStart(4)} ${String(r.ran).padStart(4)} ${String(r.applied).padStart(7)} ${String(r.bytesBefore).padStart(10)} ${String(r.bytesAfter).padStart(10)} ${(saved === null ? "-" : `${(saved * 100).toFixed(1)}%`).padStart(6)} ${formatUsd(r.micros, 4).padStart(11)}`
|
|
61
|
+
);
|
|
62
|
+
console.log(`${"".padEnd(44)}${r.first} \u2192 ${r.last}`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (args.compression === true) {
|
|
67
|
+
const rows2 = await compressionSavings(db, month);
|
|
68
|
+
if (rows2.length === 0) return console.log(`compression did not run in ${month}`);
|
|
69
|
+
console.log(`request compression, ${month}
|
|
70
|
+
`);
|
|
71
|
+
console.log("project reqs applied before after saved");
|
|
72
|
+
for (const r of rows2) {
|
|
73
|
+
const saved = savedShare(r);
|
|
74
|
+
console.log(
|
|
75
|
+
`${r.projectId.slice(0, 42).padEnd(44)}${String(r.requests).padStart(4)} ${String(r.applied).padStart(7)} ${String(r.bytesBefore).padStart(10)} ${String(r.bytesAfter).padStart(10)} ${(saved === null ? "-" : `${(saved * 100).toFixed(1)}%`).padStart(6)}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const dimension = typeof args.by === "string" ? args.by : "project_id";
|
|
81
|
+
const rows = await spendBy(db, dimension, month);
|
|
82
|
+
if (rows.length === 0) return console.log(`no ledger entries in ${month}`);
|
|
83
|
+
console.log(`spend by ${dimension}, ${month}
|
|
84
|
+
`);
|
|
85
|
+
const byComponent = dimension === "component";
|
|
86
|
+
console.log(`${dimension.padEnd(44)} reqs ${byComponent ? " units" : " tokens"} spend`);
|
|
87
|
+
let totalMicros = 0;
|
|
88
|
+
let totalRequests = 0;
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
totalMicros += row.micros;
|
|
91
|
+
totalRequests += row.requests;
|
|
92
|
+
console.log(
|
|
93
|
+
`${String(row.bucket ?? "-").slice(0, 42).padEnd(44)} ${String(row.requests).padStart(4)} ${String(byComponent ? row.units ?? 0 : row.tokens).padStart(10)} ${formatUsd(row.micros, 4).padStart(11)}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
console.log(`${"".padEnd(44)} ${String(totalRequests).padStart(4)} ${"".padStart(10)} ${formatUsd(totalMicros, 4).padStart(11)}`);
|
|
97
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
import {
|
|
4
|
+
ROLES,
|
|
5
|
+
grantMembership,
|
|
6
|
+
listMemberships,
|
|
7
|
+
revokeMembership
|
|
8
|
+
} from "./chunk-2NVDAK5J.mjs";
|
|
9
|
+
import {
|
|
10
|
+
connect,
|
|
11
|
+
environmentOf,
|
|
12
|
+
parseArgs,
|
|
13
|
+
requireString,
|
|
14
|
+
run
|
|
15
|
+
} from "./chunk-7NQHBXVZ.mjs";
|
|
16
|
+
import "./chunk-JZPUTXM7.mjs";
|
|
17
|
+
|
|
18
|
+
// scripts/member.ts
|
|
19
|
+
run(async () => {
|
|
20
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
21
|
+
const args = parseArgs(rest);
|
|
22
|
+
const db = connect(environmentOf(args));
|
|
23
|
+
const tenant = typeof args.tenant === "string" ? args.tenant : "bytesbrains";
|
|
24
|
+
switch (command) {
|
|
25
|
+
case "grant": {
|
|
26
|
+
const role = typeof args.role === "string" ? args.role : "viewer";
|
|
27
|
+
if (!ROLES.includes(role)) throw new Error(`--role must be one of: ${ROLES.join(", ")}`);
|
|
28
|
+
const uid = requireString(args, "uid");
|
|
29
|
+
await grantMembership(db, uid, tenant, role, requireString(args, "actor"));
|
|
30
|
+
console.log(`granted ${role} on ${tenant} to ${uid}`);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "revoke": {
|
|
34
|
+
const uid = requireString(args, "uid");
|
|
35
|
+
const outcome = await revokeMembership(
|
|
36
|
+
db,
|
|
37
|
+
uid,
|
|
38
|
+
tenant,
|
|
39
|
+
requireString(args, "actor"),
|
|
40
|
+
requireString(args, "reason")
|
|
41
|
+
);
|
|
42
|
+
switch (outcome) {
|
|
43
|
+
case "revoked":
|
|
44
|
+
console.log(`revoked membership of ${uid}`);
|
|
45
|
+
break;
|
|
46
|
+
case "not_found":
|
|
47
|
+
console.log(`no change \u2014 ${uid} was not a member`);
|
|
48
|
+
break;
|
|
49
|
+
case "last_owner":
|
|
50
|
+
console.log(`refused \u2014 ${uid} is the last owner of ${tenant}. Grant another owner first.`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "list": {
|
|
57
|
+
const members = await listMemberships(db, typeof args.tenant === "string" ? tenant : void 0);
|
|
58
|
+
if (members.length === 0) {
|
|
59
|
+
console.log("no memberships. Nobody can call the control plane until one exists.");
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
console.log("uid tenant role granted by");
|
|
63
|
+
for (const m of members) {
|
|
64
|
+
console.log(
|
|
65
|
+
`${m.uid.slice(0, 40).padEnd(42)}${m.tenantId.padEnd(14)}${m.role.padEnd(9)}${m.createdBy ?? "-"}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
console.error("usage: grant | revoke | list (see scripts/member.ts)");
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
});
|